mirror of
https://github.com/Patiun/com.gregkilmer.www.git
synced 2026-11-08 04:08:24 +00:00
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:
@@ -5,6 +5,7 @@
|
||||
*/
|
||||
import type { Note } from "./content.ts";
|
||||
import type { SiteIndex } from "./site.ts";
|
||||
import { formatDate } from "./site.ts";
|
||||
|
||||
export type Item = Note<"library-item">;
|
||||
export type Game = Note<"board-game">;
|
||||
@@ -47,6 +48,29 @@ export function byStanding(a: Item, b: Item): number {
|
||||
);
|
||||
}
|
||||
|
||||
/** Progress PersonalReader syncs onto a linked item (see withSynced in content.ts). */
|
||||
interface ReaderSynced {
|
||||
chapterProgress?: { chapter?: number; readAt?: string };
|
||||
bookProgress?: { percent?: number; readAt?: string };
|
||||
}
|
||||
|
||||
/** "Chapter 42, read Oct 3, 2026" or "62% read", from PersonalReader; undefined when nothing is synced. */
|
||||
export function readerProgress(item: Item): string | undefined {
|
||||
const d = item.data as ReaderSynced;
|
||||
const c = d.chapterProgress;
|
||||
const b = d.bookProgress;
|
||||
const what = c?.chapter ? `Chapter ${c.chapter}` : b && typeof b.percent === "number" && b.percent > 0 ? `${b.percent}% read` : undefined;
|
||||
if (!what) return undefined;
|
||||
const at = (c?.chapter ? c.readAt : b?.readAt)?.slice(0, 10);
|
||||
return at ? `${what}, read ${formatDate(at)}` : what;
|
||||
}
|
||||
|
||||
/** The row's details line. The note's own `progress` wins over the synced one. */
|
||||
export const itemDetails = (item: Item): (string | number)[] => {
|
||||
const d = item.data;
|
||||
return [d.creator, d.year, d.platform, d.progress || readerProgress(item), d.group].filter((x): x is string | number => x !== undefined && x !== "");
|
||||
};
|
||||
|
||||
export interface LibraryFilter {
|
||||
type?: MediaKey;
|
||||
q: string;
|
||||
@@ -152,6 +176,31 @@ export function gameShows(g: Game, f: ShelfFilter, text: string): boolean {
|
||||
return !f.q || text.includes(f.q);
|
||||
}
|
||||
|
||||
/** Values BoardGameGeek syncs onto a game (see withSynced in content.ts). */
|
||||
interface GameSynced {
|
||||
minPlayers?: number;
|
||||
maxPlayers?: number;
|
||||
playTime?: number;
|
||||
minPlayTime?: number;
|
||||
maxPlayTime?: number;
|
||||
}
|
||||
|
||||
const range = (lo?: number, hi?: number) => (lo && hi && lo !== hi ? `${lo}–${hi}` : String(lo || hi || ""));
|
||||
|
||||
/** "2–4 players", "1 player" or undefined when BoardGameGeek has not been synced. */
|
||||
export function gamePlayers(g: Game): string | undefined {
|
||||
const d = g.data as GameSynced;
|
||||
const r = range(d.minPlayers, d.maxPlayers);
|
||||
return r ? `${r} ${r === "1" ? "player" : "players"}` : undefined;
|
||||
}
|
||||
|
||||
/** "60–90 min", "45 min" or undefined. */
|
||||
export function gamePlayTime(g: Game): string | undefined {
|
||||
const d = g.data as GameSynced;
|
||||
const r = d.minPlayTime && d.maxPlayTime ? range(d.minPlayTime, d.maxPlayTime) : range(d.playTime);
|
||||
return r ? `${r} min` : undefined;
|
||||
}
|
||||
|
||||
/** Every "how it plays" value on the shelf, A to Z. */
|
||||
export const playStyles = (games: Game[]) =>
|
||||
[...new Set(games.flatMap((g) => g.data.howItPlays ?? []))].sort((a, b) => a.localeCompare(b));
|
||||
|
||||
Reference in New Issue
Block a user