Phase 6: sync jobs, inquiry and order forms

- Sync scheduler in the content service with a per-job cache (sync_cache),
  sync_runs reporting and Healthchecks pings. Jobs: Archidekt, BoardGameGeek,
  cover art (Open Library, TMDB, AniList), Suwayomi and Kavita reading
  progress, Bambuddy filament and Homebox material stock. A note's own value
  always wins over a synced one.
- PersonalReader: updates only Library notes with a matching anilistId,
  changes to Greg's notes are held for review; Reading series with no note
  show as suggestions on the admin Overview.
- /api/inquiries and /api/orders: Turnstile, rate limits, a saved copy in the
  content service (intake token) plus email over SMTP. Orders are re-checked
  against the live notes: accepted only when the category's ordersOpen is
  true and the line has capacity.
- Admin Inbox for saved submissions. Options' inStock is optional so synced
  stock can fill it. All settings from process.env; .env.example per service.
- Sources could not be reached from the build environment; parsers are tested
  against fixtures labelled as not live.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012aSbo2D1qP8cMFQCykf7kx
This commit is contained in:
Claude
2026-10-11 23:50:06 +00:00
parent 60dfeecd7f
commit e3b0cf60c5
76 changed files with 6050 additions and 86 deletions
+11 -3
View File
@@ -4,8 +4,10 @@
* cannot change who may write or what the rules allow.
*/
import { resolve } from "node:path";
import { syncSettings, type SyncSettings } from "./sync/settings.ts";
export type Role = "reader" | "agent" | "editor" | "admin";
/** `intake` can only file submissions (the site's forms); it can read nothing. */
export type Role = "reader" | "agent" | "editor" | "admin" | "intake";
export interface Config {
/** The vault's Site folder. */
@@ -22,9 +24,13 @@ export interface Config {
notifyWebhookUrl?: string;
allowedEmails: string[];
scanMinutes: number;
/** Which sync jobs run, their intervals, Healthchecks URLs and keys. */
sync: SyncSettings;
/** Largest attachment a submission may carry, in bytes. */
submissionMaxBytes: number;
}
const roles = new Set<Role>(["reader", "agent", "editor", "admin"]);
const roles = new Set<Role>(["reader", "agent", "editor", "admin", "intake"]);
/** CONTENT_TOKENS is a comma separated list of role:token pairs. */
export function parseTokens(raw: string | undefined): Map<string, Role> {
@@ -53,12 +59,14 @@ export function loadConfig(env: NodeJS.ProcessEnv = process.env): Config {
notifyWebhookUrl: env.NOTIFY_WEBHOOK_URL || undefined,
allowedEmails: (env.ALLOWED_EMAILS ?? "").split(",").map((s) => s.trim().toLowerCase()).filter(Boolean),
scanMinutes: Number(env.SCAN_MINUTES ?? 5),
sync: syncSettings(env),
submissionMaxBytes: Math.round(Number(env.SUBMISSION_MAX_MB ?? 10) * 1024 * 1024),
};
}
/** What each role may do. Checked by the HTTP API and the MCP server alike. */
export const can = {
readDrafts: (r: Role) => r !== "reader",
readDrafts: (r: Role) => r !== "reader" && r !== "intake",
write: (r: Role) => r === "agent" || r === "editor" || r === "admin",
review: (r: Role) => r === "editor" || r === "admin",
rollback: (r: Role) => r === "admin",