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
+23 -2
View File
@@ -15,10 +15,20 @@ import OrderForm from "../../components/store/OrderForm.astro";
import { site } from "../../lib/page.ts";
import { formatDate } from "../../lib/site.ts";
import { canOrder, channelOf, storeState } from "../../lib/store.ts";
import { errorMessages, parseDraft, parseErrorCode } from "../../lib/forms.ts";
import { turnstileSiteKey } from "../../lib/forms-server.ts";
const s = await site();
const st = storeState(s);
const selected = Astro.url.searchParams.get("line") ?? undefined;
// A post that was turned down comes back with its reason and the answers given.
const draft = parseDraft(Astro.cookies.get("gk-order-draft")?.value);
if (draft) {
Astro.cookies.delete("gk-order-draft", { path: "/" });
Astro.response.headers.set("Cache-Control", "private, no-store");
}
const errorCode = draft?.code ?? parseErrorCode(Astro.url.searchParams.get("error"));
const turnstileKey = turnstileSiteKey();
const selected = draft?.values.line ?? Astro.url.searchParams.get("line") ?? undefined;
const orderable = st.lines.filter((l) => canOrder(l, st));
const selling = st.lines.filter((l) => channelOf(l) === "here");
const shown = orderable.length ? orderable : selling;
@@ -26,6 +36,7 @@ const turnarounds = [...new Set(st.groups.map(([c]) => st.category(c).turnaround
const reopens = st.closed.filter((c) => c.reopens).map((c) => `${c.name} reopens ${formatDate(c.reopens)}.`);
---
<PageShell title="Order request" family="store" current="store" description="Send an order request for something from the Store.">
{turnstileKey && <script slot="head" src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer is:inline></script>}
<section class="intro" aria-labelledby="order-h1">
<Breadcrumb here="Order request" />
<h1 id="order-h1">Order request</h1>
@@ -34,9 +45,16 @@ const reopens = st.closed.filter((c) => c.reopens).map((c) => `${c.name} reopens
<div class="layout">
<div class="main">
{errorCode && (
<div class="problem">
<Notice role="alert" title={errorMessages[errorCode]}>
{draft && Object.keys(draft.errors).length > 0 ? "The marked answers are below." : ""}
</Notice>
</div>
)}
{
orderable.length > 0 ? (
<OrderForm lines={orderable} selected={selected} storeTerms={st.terms} />
<OrderForm lines={orderable} selected={selected} storeTerms={st.terms} values={draft?.values} errors={draft?.errors} turnstileKey={turnstileKey} />
) : (
<div class="closed">
{st.anyOpen ? (
@@ -104,6 +122,9 @@ const reopens = st.closed.filter((c) => c.reopens).map((c) => `${c.name} reopens
flex: 3 1 600px;
min-width: 0;
}
.problem {
margin-bottom: 16px;
}
.closed {
display: flex;
flex-direction: column;