mirror of
https://github.com/Patiun/com.gregkilmer.www.git
synced 2026-11-08 04:08:24 +00:00
32cf13a5cb
services/admin is a client of the content service's HTTP API, served on the tailnet only. It renders on the server with plain forms and no client scripts, and holds the admin token so the browser never sees it. - Review queue: reasons, line diff against the live version, accept, reject, and reject with the live version put back into Obsidian - Notes and drafts: search, preview, fields, backlinks, publish toggle - History with compare and roll back - Quick capture for a status or a Library item (optionally both) - Store: orders open or closed per category, open slots and stock - Overview: counts, last scan and error, scan now, sync job health - Same-origin check on every post, strict CSP, sanitized previews The content service gains /api/status, sync run reports (/api/syncs) and lookup of a note by name. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012aSbo2D1qP8cMFQCykf7kx
124 lines
6.6 KiB
Markdown
124 lines
6.6 KiB
Markdown
# Content service
|
|
|
|
The content service sits between the vault and everything that shows or
|
|
changes content. It is the only thing that reads or writes the Site folder.
|
|
|
|
```
|
|
Obsidian ──edits──▶ Site folder ──watch──▶ rules ──▶ accepted store ──▶ site, admin page
|
|
▲ ▲
|
|
└──── accepted API changes ────────┤
|
|
write API (HTTP)
|
|
▲ ▲
|
|
MCP server admin page, later editors
|
|
```
|
|
|
|
- **The vault is the source of truth.** Greg edits in Obsidian; the service
|
|
watches the folder and takes each change in.
|
|
- **Every change runs the same rules**, whether it came from Obsidian, an
|
|
agent or the admin page. A change that passes is accepted. A change that
|
|
fails is held for review, and the site keeps showing the last accepted
|
|
version, so a bad edit never reaches a page.
|
|
- **Changes through the API are written back into the vault** once accepted,
|
|
so Obsidian always shows what the site shows. The service marks its own
|
|
writes as seen before writing, so the watcher does not take them in twice.
|
|
- **Deleting or moving a file unpublishes the note.** Its history stays, and
|
|
any version can be rolled back.
|
|
|
|
## The rules
|
|
|
|
Pure functions in [`services/content/src/rules.ts`](../services/content/src/rules.ts),
|
|
run in this order. The rules live in code, not in the vault, so no agent can
|
|
loosen them.
|
|
|
|
| Code | Holds a change when |
|
|
| --- | --- |
|
|
| `invalid` | The note fails its type's schema. |
|
|
| `misplaced` | A settings note (`_site`, `Now/_kinds`, `Store/_settings`) is not at its fixed path. |
|
|
| `duplicate-name`, `duplicate-slug` | Another live note already has the file name, or the same type already uses the address. |
|
|
| `text-loss` | A body of 400 or more characters loses more than half in one edit. |
|
|
| `secret` | New text looks like a key, token or password. |
|
|
| `contact` | An agent adds an email address or phone number. |
|
|
| `agent-edit` | An agent changes a note it did not write, unless the note has `agentEdits: true`. |
|
|
| `agent-publish` | An agent publishes anything but a status or a Library item. |
|
|
| `batch` | An agent makes more than 10 changes in 10 minutes. |
|
|
|
|
Invalid input through the API is turned down at once with the problems, rather
|
|
than held, so callers fix it themselves. A change from Obsidian that fails is
|
|
held, because the file is already on disk.
|
|
|
|
## Addresses
|
|
|
|
A slug change or a rename keeps the old address working as a redirect. When a
|
|
note disappears and a note of the same type with the same text or title
|
|
appears within ten minutes, that is treated as a move: redirects to the old
|
|
path are pointed at the new one, so redirects never chain. A slug that comes
|
|
back into use belongs to its note, not to the redirect.
|
|
|
|
## The write API
|
|
|
|
`ContentService.apply` takes one change, `create` (type, fields, body) or
|
|
`update` (path, the fields to change, `null` to remove one, and optionally
|
|
the body), along with who made it. Every client uses it:
|
|
|
|
- the MCP server, as `agent`;
|
|
- the admin page and later editing tools, as `greg` (editor token) or `admin`;
|
|
- the review queue: `acceptHeld`, `rejectHeld` (optionally restoring the live
|
|
version into the vault), and `rollback` to any earlier revision.
|
|
|
|
An update first takes in any unsaved Obsidian edit to that file, so an API
|
|
change never overwrites one. A note with a change waiting for review takes no
|
|
other API change until that one is decided. A note's type never changes.
|
|
|
|
### HTTP
|
|
|
|
Every route but `/health` needs `Authorization: Bearer <token>`. Tokens map to
|
|
roles in `CONTENT_TOKENS`.
|
|
|
|
| Route | Who | What |
|
|
| --- | --- | --- |
|
|
| `GET /health` | anyone | Note, published and held counts. |
|
|
| `GET /api/schema` | all | Every note type with its JSON Schema. |
|
|
| `GET /api/notes?type=&q=&limit=&offset=&full=` | all | List or search. Readers see published notes only. |
|
|
| `GET /api/note?path=`, `?name=` or `?type=&slug=` | all | One note with its backlinks. An old slug answers with `{ redirect }`. |
|
|
| `GET /api/backlinks?name=` | all | Notes linking to a note. |
|
|
| `GET /api/redirects` | all | Every redirect. |
|
|
| `GET /attachments/<file>` | all | An image from `_attachments`. |
|
|
| `GET /api/note/history?path=`, `/api/note/revision?path=&rev=` | agent, editor, admin | History and old versions. |
|
|
| `GET /api/problems` | agent, editor, admin | Broken links and other whole-folder problems. |
|
|
| `GET /api/held`, `/api/held/<id>` | agent, editor, admin | The review queue; one entry comes with the current text for comparison. |
|
|
| `GET /api/status` | agent, editor, admin | When the service started, its last full scan and last error, counts, and each sync job's latest run. |
|
|
| `GET /api/syncs`, `?job=` | agent, editor, admin | Each sync job's latest run, or one job's recent runs. |
|
|
| `POST /api/syncs` | agent, editor, admin | A sync job reports a run: `{ job, ok, message?, counts? }`. The last 50 runs per job are kept. |
|
|
| `POST /api/changes` | agent, editor, admin | One change. 422 with reasons when turned down. |
|
|
| `POST /api/held/<id>/accept`, `/reject` | editor, admin | Decide a held change. `{ "restore": true }` on reject puts the live version back into the vault. |
|
|
| `POST /api/note/rollback` | admin | `{ path, rev }` |
|
|
| `POST /api/scan` | editor, admin | Compare the whole folder now. |
|
|
| `/mcp` | agent, editor, admin | The MCP server. |
|
|
|
|
### MCP
|
|
|
|
Streamable HTTP at `/mcp`, stateless. Tools: `search`, `get_note`,
|
|
`get_schema`, `list_held`, `set_status`, `add_library_item`, `update_fields`
|
|
and `draft_post` (always unpublished). There is no tool to delete, rename,
|
|
move or write a raw file.
|
|
|
|
## Running it
|
|
|
|
| Variable | Default | |
|
|
| --- | --- | --- |
|
|
| `SITE_DIR` | required | The vault's Site folder. |
|
|
| `DATA_DIR` | `data` | Holds `content.db`. Back it up: history and the review queue cannot be rebuilt from the vault. |
|
|
| `PORT`, `HOST` | `4310`, `0.0.0.0` | |
|
|
| `CONTENT_TOKENS` | none | `role:token` pairs, comma separated. Roles: reader, agent, editor, admin. Tokens are 24 characters or more. |
|
|
| `ALLOWED_EMAILS` | none | Addresses an agent may add without a hold. |
|
|
| `HEALTHCHECK_URL` | none | Pinged after every full scan. |
|
|
| `NOTIFY_WEBHOOK_URL` | none | Receives `{ event: "held", held }` when a change is held. |
|
|
| `SCAN_MINUTES` | `5` | Full scan interval, on top of the file watch. |
|
|
|
|
```sh
|
|
SITE_DIR=/path/to/Site CONTENT_TOKENS="reader:…,agent:…,editor:…,admin:…" npm run content
|
|
```
|
|
|
|
The store is SQLite (`node:sqlite`) with full-text search, so the service has
|
|
no database server to run.
|