5.0 KiB
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 <brut@bincio.wiki>"
git update-ref refs/heads/main HEAD
The committer is always bincio-wiki <wiki@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 <SHA> 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:
{ "slug": "gps-e-ciclocomputer", "content": "...", "base_hash": "a85a2ee" }
The editor holds this hash. On save, it sends it back:
{ "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.
# 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:
- Deletes
.astro/data-store.json— Astro's persisted content cache. Without this, changes to files inpages/andblog/(which live outsidesite/) are not picked up by a cached build. - Runs
npm run build -- --forceinsite/. The--forceflag tells Astro to ignore any remaining stale cache. - If
WIKI_WEBROOTis set, rsyncssite/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.