# Edit Sidecar The edit sidecar (`edit/server.py`) is a FastAPI application that handles all write operations. It runs alongside the static Astro build and is the only process that modifies content on disk. ## API endpoints ### Auth | Method | Path | Auth | Description | |--------|------|------|-------------| | `GET` | `/api/me` | session | Return current user info | | `POST` | `/api/auth/login` | — | Create session, set cookie | | `POST` | `/api/auth/logout` | session | Destroy session, clear cookie | | `POST` | `/api/auth/register` | — | Register via invite token | ### Wiki pages | Method | Path | Description | |--------|------|-------------| | `GET` | `/pages` | List all page slugs | | `GET` | `/pages/{slug}` | Get page content + `base_hash` | | `POST` | `/pages/{slug}` | Save page (with 3-way merge) | | `DELETE` | `/pages/{slug}` | Delete page | ### Blog / stories Same as pages, with `/stories` prefix. ### Invites | Method | Path | Description | |--------|------|-------------| | `GET` | `/api/invites` | List own invites | | `POST` | `/api/invites` | Create a new wiki invite | | `DELETE` | `/api/invites/{code}` | Revoke an unused invite | ### Other | Method | Path | Description | |--------|------|-------------| | `POST` | `/api/assets` | Upload image (multipart) | | `POST` | `/rebuild` | Trigger Astro rebuild + rsync | | `GET` | `/api/log` | Last 50 web-editor commits | ## Git attribution Every page save or delete is committed to git with the editor's handle as the author: ``` git add pages/bincio-tech/gps-e-ciclocomputer.md git commit -m "brut: edited gps-e-ciclocomputer" \ --author="brut " git update-ref refs/heads/main HEAD ``` The committer is always `bincio-wiki `. The author field carries the human attribution. `GIT_COMMITTER_NAME` and `GIT_COMMITTER_EMAIL` are set in the environment passed to each subprocess. On the VPS, `GIT_DIR` points to the bare repo at `/opt/bincio-wiki-repo.git` and `GIT_WORK_TREE` to `/opt/bincio_wiki`. ### The `update-ref` fix The VPS post-receive hook runs `git checkout -f ` to check out the new tree. This detaches `HEAD` in the bare repo: subsequent sidecar commits advance the detached HEAD pointer but not `refs/heads/main`. On the next `git push` from a developer's machine, those sidecar commits become unreachable and are eventually garbage-collected. The fix: after every `git commit`, the sidecar immediately runs: ``` git update-ref refs/heads/main HEAD ``` This keeps `refs/heads/main` in sync with the detached HEAD, so sidecar commits are always reachable via the branch and survive a future push. ### Serialisation All git operations are serialised with a module-level `asyncio.Lock()`. This prevents two concurrent saves from interleaving `git add` + `git commit` calls and producing a corrupted index or commit history. ## Optimistic concurrency When a page is opened for editing, the sidecar returns the current HEAD commit hash alongside the content: ```json { "slug": "gps-e-ciclocomputer", "content": "...", "base_hash": "a85a2ee" } ``` The editor holds this hash. On save, it sends it back: ```json { "content": "...", "base_hash": "a85a2ee" } ``` The sidecar compares `base_hash` against the current HEAD for that file. If they differ, another edit landed while the user was writing. ## 3-way merge Rather than rejecting the save with a 409, the sidecar attempts a 3-way merge using `git merge-file`: ``` base = content at base_hash (what the user started with) current = content at HEAD (what's on disk now) user = content from request (what the user wrote) ``` If the edits touched different lines, `git merge-file` produces a clean merged result and the save proceeds transparently. If the same lines were changed by both, `git merge-file` exits with a non-zero code and embeds conflict markers (`<<<<<<< attuale / ======= / >>>>>>> handle`). The sidecar returns a 409 with the conflict-marked content so the user can resolve manually. ```python # exit code 0 → clean merge; > 0 → conflicts present proc = await asyncio.create_subprocess_exec( "git", "merge-file", "-L", "attuale", "-L", "base", "-L", handle, str(p_current), str(p_base), str(p_user), ... ) await proc.wait() merged_content = p_current.read_text() has_conflict = proc.returncode > 0 ``` ## Rebuild `POST /rebuild` does three things: 1. Deletes `.astro/data-store.json` — Astro's persisted content cache. Without this, changes to files in `pages/` and `blog/` (which live outside `site/`) are not picked up by a cached build. 2. Runs `npm run build -- --force` in `site/`. The `--force` flag tells Astro to ignore any remaining stale cache. 3. If `WIKI_WEBROOT` is set, rsyncs `site/dist/` to the webroot so nginx serves the updated build immediately. The rebuild is triggered automatically by the `PageEditor.svelte` after every successful save. ## Slug validation Slugs are validated against `^[a-zA-Z0-9_][a-zA-Z0-9_\-/]*$` and checked for path traversal before any file operation. The resolved path must be a child of the configured base directory.