Sync local state with remote after untracked local evolution

Local site-ai/ had diverged significantly from the last commit pushed
to Gitea (new remote-env-setup/remove targets, SITE_CONFIG_DIR/MAIN_REPO
split, credential cleanup in server-install.sh, migration docs) without
ever being committed. This catches the repo up to what's actually on disk.

Also untracks the legacy user/ subtree left over from before content was
split into its own standalone repo (natascha-rieter.nl-user) — user/ is
gitignored here and stays a separate git repo, unaffected by this commit.
This commit is contained in:
2026-08-30 14:48:54 +02:00
parent 9f798daaa3
commit 8c93dfd7c9
106 changed files with 1056 additions and 1081 deletions
+171
View File
@@ -0,0 +1,171 @@
# Grav 1.7 → 2.0 Migration Simulation — Handover
**Status as of 2026-08-25:** Investigation in progress, not concluded. Nothing has
touched production. This document is for a future session to pick up where this
one left off.
## What this is
The user asked (advisory, then "let's look into it — zero impact on production")
what the best strategy is to upgrade local Grav from 1.7.49.5 to the latest 2.x,
and to try installing a 2.x admin UI. Per explicit instruction, instead of
upgrading in place, a **second, fully isolated Docker container** running Grav
2.0.21 was stood up next to the existing 1.7 dev container, using a **copy** of
the real site content, to see what breaks before committing to a real migration.
**Nothing about the live dev container, the `user/` git content repo, or
production has been changed.** All `make remote-*` targets remain untouched.
## Current environment state
| | Container | Image | Port | Data | State |
|---|---|---|---|---|---|
| existing dev | `natascha_grav` | `lscr.io/linuxserver/grav:1.7.49.5-ls244` | 8080 | `./user` (live mount) | **Up**, compose project `site-ai` (`docker-compose.yml`) |
| migration sandbox | `natascha_grav2_test` | `lscr.io/linuxserver/grav:2.0.21-ls271` | 8081 | `./migration-test/user` (copy, no `.git`) | **Stopped** (exited cleanly, `restart: "no"` so it doesn't auto-start) |
Docker runs via **Colima** (no Docker Desktop). Colima must be running before any
of this works: check with `colima status`, start with `colima start`.
### To resume the sandbox container
```bash
cd /Users/mischa/Nextcloud/Projects/Natascha/natascha-rieter.nl/site-ai
docker compose -f docker-compose.grav2.yml -p natascha-grav2-test up -d
```
Then it's reachable at **http://localhost:8081** (front-end) and
**http://localhost:8081/admin** (admin login).
### To tear it down completely (once done investigating)
```bash
cd /Users/mischa/Nextcloud/Projects/Natascha/natascha-rieter.nl/site-ai
docker compose -f docker-compose.grav2.yml -p natascha-grav2-test down
rm -rf migration-test
```
`docker-compose.grav2.yml` itself can also be deleted once the simulation is
finished and a real decision is made — it was created purely as a scratch
sandbox definition and is separate from the production-mirroring
`docker-compose.yml`.
## Files created this investigation (all under `site-ai/`)
- `docker-compose.grav2.yml` — the sandbox compose file (image `2.0.21-ls271`,
port 8081, mounts `./migration-test/user`, `restart: "no"`).
- `migration-test/user/``rsync -a --exclude='.git'` copy of the live `user/`
folder (~50M, `.git` excluded on purpose since `user/` is itself a separate
git content-sync repo — copying `.git` would have created a confusing nested
repo). **This copy is stale** as of whenever it was taken; re-sync before
further testing if `user/` content has changed since:
```bash
rsync -a --exclude='.git' ./user/ ./migration-test/user/
```
- `~/.docker/cli-plugins/docker-compose` (outside repo, machine-level) —
symlink to the Homebrew `docker-compose` binary, registering it as the
`docker compose` CLI plugin. This was a durable fix for `make start` (which
calls `docker compose up -d` and previously failed with
`unknown shorthand flag: 'd'` because no compose plugin was registered).
Chosen explicitly by the user over editing the Makefile. Not something a
future session needs to redo, but worth knowing about if `make start` ever
breaks again on a different machine.
## Findings so far
1. **Grav 2.0 is not an in-place upgrade.** Official docs say so explicitly.
Also, in this project's setup, Grav core lives inside the Docker image, not
in the `./user` volume mount — so an in-place `gpm selfupgrade` wouldn't
persist across container recreation anyway. The real upgrade path is an
**image tag swap** (`1.7.49.5-ls244` → `2.0.21-ls271` or whatever is current)
combined with a content/plugin compatibility pass.
2. **Front-end renders correctly** on Grav 2.0.21 core against the unmodified
1.7-era `user/` content and theme — verified via curl against
`http://localhost:8081/`, including the `langswitcher` redirect (`/` → `/nl`).
3. **Classic `admin` plugin (v1.10.55) login page renders correctly** on Grav
2.0.21 — confirmed via `curl http://localhost:8081/admin`: full CSS/JS asset
loading, Dutch-localized labels, working nonce, no visible PHP errors. This
is a **better result than official Grav 2.0 docs suggest** — the docs imply
classic admin is incompatible with 2.0 and that migration should go through
`admin2` (the alpha SvelteKit rewrite) instead.
4. **No errors, exceptions, fatals, deprecation notices, or warnings** appeared
in `docker logs natascha_grav2_test` for either the `/` or `/admin` requests.
5. `bin/grav`/`bin/gpm` CLI layout is the same path as 1.7
(`/app/www/public/bin/`, must `cd` there first — `FATAL: Must be run from
ROOT directory of Grav!` otherwise), but the **CLI command set differs**
between 1.7 and 2.0 — e.g. `php bin/grav plugins` doesn't exist in 2.0
(confirm via `php bin/grav list`).
6. Grav's built-in `page-system-validator` tool (meant for before/after
render-diffing across an upgrade) **cannot be used from the CLI on this
setup** — it throws a PHP fatal
(`Call to a member function param() on null in .../admin/admin.php:354`)
because the `admin` plugin's `onPageInitialized()` handler expects an HTTP
request object that doesn't exist under CLI invocation. This happened on the
**old 1.7 container**, so it's an artifact of the admin plugin's CLI
handling generally, not a 2.0-specific regression. Don't waste time trying
to make this tool work as a validation method — it needs a different
approach (see "Next steps").
## What has NOT been tested yet
- **The actual admin dashboard post-login** — page editing, media management,
anything behind auth. This requires real admin credentials, which were
deliberately not obtained/guessed. If the user provides a throwaway
password (or creates one via `bin/grav user create` in the sandbox
container, which is safe since it's isolated test data), this is the
obvious next concrete step.
- **Individual plugin compatibility**, specifically the ones most likely to
break because they hook deep into core APIs that changed in 2.0:
- `git-sync`
- `flex-objects`
- `admin-media-move`, `admin-media-replace`, `admin-media-actions`
- `automagic-images`
- `social-meta-tags`
- `langswitcher` (front-end redirect already confirmed working, but not
deeper admin-side behavior)
- `draft-preview`
Grav 2.0's plugin blueprints can declare a `compatibility:` key; check each
plugin's `blueprints.yaml` under `migration-test/user/plugins/<name>/` for
this key, and/or check each plugin's upstream repo/changelog for 2.0
support statements. `bin/gpm info <plugin>` may also surface something once
run from inside the sandbox container (untested).
- **`admin2`** (the alpha SvelteKit/Vite/Tailwind admin rewrite) has not been
installed or tested in the sandbox at all. It's not GPM-installable — it
requires manually git-cloning `grav-plugin-api` and `grav-plugin-admin2`
into `user/plugins/`. Given it's explicitly alpha/non-production-ready
upstream, and classic admin already renders fine on 2.0 in this test, it's
worth explicitly asking the user whether they still want this installed
before spending time on it, rather than assuming yes from the original
advisory question.
## Suggested next steps (pick up here)
1. Bring the sandbox back up (command above), re-sync `migration-test/user`
from `./user` if content has changed since.
2. Either:
- Ask the user for a throwaway admin password to log in and click through
the real dashboard/page-editing/media flows, **or**
- Create a fresh test admin user inside the sandbox container only
(`docker exec -it natascha_grav2_test sh -c "cd /app/www/public && php bin/grav user create"`)
— safe since it only affects the isolated `migration-test/user` copy.
3. Go through the plugin list above one by one: check blueprint
`compatibility:` declarations, and exercise each plugin's actual feature
(git-sync a commit, upload/replace media, edit a flex-objects-backed page)
inside the sandbox to see what actually breaks vs. what merely lacks a
compatibility flag.
4. Once a real compatibility picture exists, come back to the original
question — decide whether the production upgrade path should be:
(a) image-tag swap + fix whatever plugins broke, or
(b) use Grav's official `migrate-grav` GPM plugin (side-by-side staged
migration with Reset/Restart/Promote controls) instead of doing it by hand.
This official tool was identified during research but deliberately not
used yet, since the user asked specifically for a separate-container
simulation first.
5. Revisit whether `admin2` is still wanted given classic admin's
better-than-expected 2.0 compatibility.
+561
View File
@@ -0,0 +1,561 @@
# Deployment Toolchain Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Build a complete Make-based deployment toolchain for the natascha-rieter.nl Grav CMS site, covering initial server setup, content sync via Gitea, remote Grav management, temporary credential handling, and maintenance mode.
**Architecture:** Two git repos (root config repo + user content repo) are cloned on both local machine and server. The Makefile is the single interface for all operations, building SSH commands from local `.env`. Gitea credentials exist on the server only when explicitly written by `remote-env-setup` and are always removed with `remote-env-remove`. Content sync goes through Gitea; the Grav Sync plugin handles automatic server-side pulls. The `user/` directory is a standalone nested git repo (replacing git subtree).
**Tech Stack:** GNU Make, Bash (scripts only where Make is insufficient), SSH, Grav CMS 1.7, Gitea, Git, Docker (local only)
---
## File Map
| Action | File |
|--------|------|
| Modify | `.env.example` |
| Modify | `Makefile` |
| Modify | `scripts/server-install.sh` |
| Create | `scripts/server-maintenance.sh` |
| Modify | `README.md` |
---
### Task 1: Update `.env.example`
Add `USER_REPO`, `MAIN_REPO`, `SITE_CONFIG_DIR`. Rename `REPO``USER_REPO`. Add comments.
**Files:**
- Modify: `.env.example`
- [ ] **Step 1: Replace `.env.example` content**
```
# SSH connection
REMOTE_USER=root
REMOTE_HOST=example.com
REMOTE_HOME=/home/example.com
# Server paths (derived from REMOTE_HOME in Makefile; override here if needed)
WEBROOT=/home/example.com/public_html
SITE_CONFIG_DIR=/home/example.com/site-config
# Grav
GRAV_VERSION=1.7.49.5
# Repos
USER_REPO=https://gitea.example.com/org/natascha-rieter.nl-user.git
MAIN_REPO=https://gitea.example.com/org/natascha-rieter-nl.git
# Gitea credentials — never commit these; only ever in .env (local) or ~/.env-natascha (server, temporary)
GITEA_HOST=gitea.example.com
GITEA_USER=natascha-deploy
GITEA_TOKEN=your-token-here
```
- [ ] **Step 2: Verify `.env` (your local copy) has matching keys**
Open `.env` and add any keys missing compared to `.env.example`. Fill in real values. Do not change `.env.example` values — they stay as placeholders.
- [ ] **Step 3: Commit `.env.example`**
```bash
git add .env.example
git commit -m "config: update env.example with USER_REPO, MAIN_REPO, SITE_CONFIG_DIR"
```
---
### Task 2: Migrate `user/` from git subtree to standalone nested repo
The `user/` directory is currently tracked by the main repo via git subtree. We convert it to a standalone git repo so `content-push`/`content-pull` are simple `git push`/`git pull` operations.
**Files:**
- Modify: `.gitignore` (root of `site-ai/`)
- No code files changed
- [ ] **Step 1: Push current user/ state via old method (safety sync)**
```bash
cd /path/to/site-ai
git subtree push --prefix=user user-deploy main
```
Expected: pushes current user/ state to Gitea. If this fails because nothing changed, that's fine.
- [ ] **Step 2: Remove user/ from main repo tracking**
```bash
git rm -r --cached user/
```
Expected: output like `rm 'user/config/site.yaml'` for each file. The files stay on disk.
- [ ] **Step 3: Add user/ to .gitignore**
Create or edit `.gitignore` in the `site-ai/` root and add:
```
/user/
```
- [ ] **Step 4: Remove the old user-deploy git remote**
```bash
git remote remove user-deploy
```
Expected: no output, no error.
- [ ] **Step 5: Commit the removal**
```bash
git add .gitignore
git commit -m "chore: remove user/ from main repo tracking (now standalone git repo)"
```
- [ ] **Step 6: Clone user repo fresh into user/**
Replace `$USER_REPO` with the value from your `.env`.
```bash
rm -rf user/
git clone $USER_REPO user/
```
Expected: `user/` is now a proper git repo with its own `.git/` directory, tracking Gitea.
- [ ] **Step 7: Verify**
```bash
git -C user status
git -C user remote -v
```
Expected: clean working tree, remote `origin` pointing to Gitea user repo.
---
### Task 3: Update Makefile — content sync and variable defaults
Replace subtree-based targets with direct git push/pull. Allow `.env` to override `WEBROOT` and `SITE_CONFIG_DIR`.
**Files:**
- Modify: `Makefile`
- [ ] **Step 1: Replace the full Makefile**
```makefile
-include .env
export
SSH := $(REMOTE_USER)@$(REMOTE_HOST)
WEBROOT ?= $(REMOTE_HOME)/public_html
SITE_CONFIG_DIR ?= $(REMOTE_HOME)/site-config
# ── Local dev ──────────────────────────────────────────────────────────────────
start:
docker compose up -d
stop:
docker compose down
setup: start install-plugins
install-plugins:
docker exec natascha_grav php /app/www/public/bin/gpm install $(shell cat plugins.txt | tr '\n' ' ') -y
# ── Content sync (user repo ↔ Gitea) ──────────────────────────────────────────
content-push:
git -C user push origin main
content-pull:
git -C user pull origin main
# ── Remote credentials ─────────────────────────────────────────────────────────
remote-env-setup:
@ssh $(SSH) "printf 'GITEA_HOST=%s\nGITEA_USER=%s\nGITEA_TOKEN=%s\n' \
'$(GITEA_HOST)' '$(GITEA_USER)' '$(GITEA_TOKEN)' > ~/.env-natascha && chmod 600 ~/.env-natascha"
@echo "Credentials written to server. Run 'make remote-env-remove' when done."
remote-env-remove:
@ssh $(SSH) "rm -f ~/.env-natascha"
@echo "Credentials removed from server."
# ── Remote: initial install ────────────────────────────────────────────────────
remote-install:
ssh $(SSH) "WEBROOT=$(WEBROOT) \
SITE_CONFIG_DIR=$(SITE_CONFIG_DIR) \
USER_REPO=$(USER_REPO) \
MAIN_REPO=$(MAIN_REPO) \
GRAV_VERSION=$(GRAV_VERSION) \
PLUGINS='$(shell cat plugins.txt | tr '\n' ' ')' \
GITEA_HOST=$(GITEA_HOST) \
GITEA_USER=$(GITEA_USER) \
GITEA_TOKEN=$(GITEA_TOKEN) \
bash -s" < scripts/server-install.sh
# ── Remote: ongoing maintenance ────────────────────────────────────────────────
remote-fetch:
ssh $(SSH) "git -C $(SITE_CONFIG_DIR) pull"
remote-install-plugins:
ssh $(SSH) "cd $(WEBROOT) && php bin/gpm install $(shell cat plugins.txt | tr '\n' ' ') -y"
remote-upgrade-grav:
ssh $(SSH) "cd $(WEBROOT) && php bin/grav upgrade"
remote-clean:
ssh $(SSH) "cd $(WEBROOT) && php bin/grav clearcache"
remote-maintenance-on:
ssh $(SSH) "bash -s on $(WEBROOT)" < scripts/server-maintenance.sh
remote-maintenance-off:
ssh $(SSH) "bash -s off $(WEBROOT)" < scripts/server-maintenance.sh
```
> Note: indentation in Makefiles must be tabs, not spaces.
- [ ] **Step 2: Verify Make parses without errors**
```bash
make -n start
```
Expected: prints `docker compose up -d`, no errors.
- [ ] **Step 3: Commit**
```bash
git add Makefile
git commit -m "feat: overhaul Makefile with full deployment toolchain"
```
---
### Task 4: Update `scripts/server-install.sh`
Add `SITE_CONFIG_DIR` and `MAIN_REPO` variables, clone the main repo to `SITE_CONFIG_DIR`, rename `REPO``USER_REPO`, print SSH public key at the end for Gitea deploy key setup.
**Files:**
- Modify: `scripts/server-install.sh`
- [ ] **Step 1: Replace the full script**
```bash
#!/bin/bash
set -e
: "${WEBROOT:?WEBROOT is not set}"
: "${SITE_CONFIG_DIR:?SITE_CONFIG_DIR is not set}"
: "${USER_REPO:?USER_REPO is not set}"
: "${MAIN_REPO:?MAIN_REPO is not set}"
: "${GRAV_VERSION:?GRAV_VERSION is not set}"
: "${PLUGINS:?PLUGINS is not set}"
: "${GITEA_HOST:?GITEA_HOST is not set}"
: "${GITEA_USER:?GITEA_USER is not set}"
: "${GITEA_TOKEN:?GITEA_TOKEN is not set}"
echo "==> Setting up credentials (temporary)"
printf 'machine %s\nlogin %s\npassword %s\n' "$GITEA_HOST" "$GITEA_USER" "$GITEA_TOKEN" > ~/.netrc
chmod 600 ~/.netrc
echo "==> Downloading Grav $GRAV_VERSION"
cd "$WEBROOT"
wget -q "https://getgrav.org/download/core/grav-admin/$GRAV_VERSION" -O grav-admin.zip
unzip -q grav-admin.zip
mv grav-admin/* grav-admin/.htaccess .
rm -rf grav-admin grav-admin.zip
echo "==> Cloning user repo"
rm -rf user
git clone "$USER_REPO" user
echo "==> Cloning main config repo to $SITE_CONFIG_DIR"
mkdir -p "$SITE_CONFIG_DIR"
git clone "$MAIN_REPO" "$SITE_CONFIG_DIR"
echo "==> Installing plugins"
php bin/gpm install $PLUGINS -y
echo "==> Setting permissions"
find "$WEBROOT" -type f -exec chmod 664 {} \;
find "$WEBROOT" -type d -exec chmod 775 {} \;
echo "==> Removing temporary credentials"
rm -f ~/.netrc
echo ""
echo "==> Done."
echo ""
echo "NEXT STEP — add this server's SSH public key to both Gitea repos as a deploy key"
echo "so that 'make remote-fetch' and future git pulls work without credentials:"
echo ""
cat ~/.ssh/id_rsa.pub 2>/dev/null || cat ~/.ssh/id_ed25519.pub 2>/dev/null || \
echo " No SSH key found. Generate one on the server: ssh-keygen -t ed25519 -C 'server-deploy'"
echo ""
echo "Visit your domain to complete Grav setup."
```
- [ ] **Step 2: Commit**
```bash
git add scripts/server-install.sh
git commit -m "feat: server-install clones both repos, removes credentials after use"
```
---
### Task 5: Create `scripts/server-maintenance.sh`
Toggle Grav's built-in maintenance mode by setting `pages.offline` in `user/config/system.yaml`. Grav serves its built-in offline page when this is `true`.
**Files:**
- Create: `scripts/server-maintenance.sh`
- [ ] **Step 1: Create the script**
```bash
#!/bin/bash
set -e
MODE="$1"
WEBROOT="$2"
CONFIG="$WEBROOT/user/config/system.yaml"
if [ "$MODE" != "on" ] && [ "$MODE" != "off" ]; then
echo "Usage: server-maintenance.sh on|off <webroot>"
exit 1
fi
[ -f "$CONFIG" ] || { echo "Not found: $CONFIG"; exit 1; }
VALUE="false"
[ "$MODE" = "on" ] && VALUE="true"
if grep -q "^\s*offline:" "$CONFIG"; then
sed -i "s/^\(\s*\)offline: .*/\1offline: $VALUE/" "$CONFIG"
else
printf '\npages:\n offline: %s\n' "$VALUE" >> "$CONFIG"
fi
echo "Maintenance mode: $MODE (offline: $VALUE)"
```
- [ ] **Step 2: Make executable**
```bash
chmod +x scripts/server-maintenance.sh
```
- [ ] **Step 3: Verify the script parses cleanly**
```bash
bash -n scripts/server-maintenance.sh
```
Expected: no output, exit 0.
- [ ] **Step 4: Commit**
```bash
git add scripts/server-maintenance.sh
git commit -m "feat: add server-maintenance.sh to toggle Grav offline mode"
```
---
### Task 6: Update `README.md`
Full rewrite. Covers every Make command (one-line description each), setup guide, content sync workflow, and security notes.
**Files:**
- Modify: `README.md`
- [ ] **Step 1: Replace README.md**
```markdown
# natascha-rieter.nl — Grav CMS
Grav CMS site for natascha-rieter.nl. Local dev via Docker; production on a VPS managed entirely through `make`.
---
## Repository structure
Two git repos:
| Repo | Contents | Location |
|------|----------|----------|
| `natascha-rieter-nl` (this repo) | Docker setup, Makefile, scripts, plugins.txt | `site-ai/` |
| `natascha-rieter.nl-user` | Site config, pages, theme | `user/` (nested git repo) |
The `user/` directory is a standalone git repo — its changes are pushed/pulled independently to Gitea. The Grav Sync plugin on the server automatically pulls from Gitea when content is pushed.
---
## Prerequisites
- Docker (for local dev)
- SSH access to the server
- Both Gitea repos created
- A Gitea personal access token with repo read/write access
---
## Local development setup
```bash
cp .env.example .env # fill in your values
make setup # start Docker, install plugins
```
Site runs at http://localhost:8080.
Clone the user content repo into `user/` if not already present:
```bash
git clone $USER_REPO user/
```
---
## First-time server setup
1. **Fill in `.env`** — copy `.env.example`, set all values.
2. **Run the install:**
```bash
make remote-install
```
This SSHes into the server, downloads Grav, clones both repos, installs plugins, and prints the server's SSH public key.
3. **Add the SSH key to Gitea** — copy the printed public key and add it as a deploy key to both Gitea repos (read access is enough for `remote-fetch`; the user repo also needs write if Git Sync pushes back).
After this, `make remote-fetch` works without credentials.
---
## Content sync workflow
Editors push content via the Grav Admin panel (or directly edit files in `user/`). The Grav Sync plugin on the server syncs automatically to Gitea.
To pull those changes locally:
```bash
make content-pull # pull latest user content from Gitea → local user/
```
To push local changes back to Gitea (and trigger server sync):
```bash
git -C user add -A && git -C user commit -m "content: ..."
make content-push # push local user/ changes → Gitea
```
---
## All commands
### Local
| Command | Description |
|---------|-------------|
| `make start` | Start the local Docker container |
| `make stop` | Stop the local Docker container |
| `make setup` | Start container and install all plugins |
| `make install-plugins` | (Re)install plugins from `plugins.txt` in the local container |
| `make content-push` | Push local `user/` commits to Gitea |
| `make content-pull` | Pull latest `user/` content from Gitea |
### Remote credentials
| Command | Description |
|---------|-------------|
| `make remote-env-setup` | Write Gitea credentials to `~/.env-natascha` on the server |
| `make remote-env-remove` | Delete `~/.env-natascha` from the server |
Always run `make remote-env-remove` when done with operations that required it.
### Remote server management
| Command | Description |
|---------|-------------|
| `make remote-install` | First-time install: download Grav, clone both repos, install plugins |
| `make remote-fetch` | Pull latest main repo (Makefile, scripts, plugins.txt) on the server |
| `make remote-install-plugins` | Install/update plugins from local `plugins.txt` on the server |
| `make remote-upgrade-grav` | Upgrade Grav core on the server |
| `make remote-clean` | Clear Grav cache on the server |
| `make remote-maintenance-on` | Enable Grav maintenance mode (site shows offline page) |
| `make remote-maintenance-off` | Disable Grav maintenance mode |
### Typical upgrade workflow
```bash
make remote-maintenance-on
make remote-env-setup
make remote-fetch
make remote-upgrade-grav
make remote-install-plugins
make remote-env-remove
make remote-maintenance-off
make remote-clean
```
---
## Plugins
Plugins are not committed to git. The full list is in `plugins.txt` — one plugin name per line.
- Locally: `make install-plugins`
- On server: `make remote-install-plugins`
---
## Security
- `.env` is gitignored. Never commit it.
- `GITEA_TOKEN` exists only in `.env` locally and in `~/.env-natascha` on the server during active sessions. Always run `make remote-env-remove` after use.
- `~/.env-natascha` has `chmod 600` — readable only by the SSH user.
- The server pulls from Gitea using its SSH key (deploy key, read-only). No long-lived token is stored on the server.
- `scripts/server-install.sh` writes `~/.netrc` for the initial clone and deletes it immediately after.
- Credentials are never passed as command-line arguments (they would appear in `ps` output). They are passed as environment variables in the SSH session.
```
- [ ] **Step 2: Commit**
```bash
git add README.md
git commit -m "docs: rewrite README with full command reference, setup guide, security notes"
```
---
## Self-Review
**Spec coverage:**
- ✅ Update initial install to set up both repos
- ✅ content-push/pull interact with Gitea via git (Grav Sync handles server side)
- ✅ remote-fetch (pull main repo changes on server)
- ✅ remote-upgrade-grav
- ✅ remote-install-plugins
- ✅ remote-maintenance-on / remote-maintenance-off
- ✅ remote-env-setup / remote-env-remove
- ✅ No persistent env files on server
- ✅ Make-first, bash only in scripts
- ✅ SSH built from env vars
- ✅ README updated with every command
**Placeholder scan:** None found.
**Type consistency:** N/A (shell/make, no typed interfaces).