mirror of
https://github.com/Patiun/com.gregkilmer.www.git
synced 2026-11-07 19:58:24 +00:00
e3b0cf60c5
- 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
87 lines
4.2 KiB
TypeScript
87 lines
4.2 KiB
TypeScript
/**
|
|
* PersonalReader progress on Library rows and synced stock on Store options.
|
|
* Synced values are the shapes the content service's jobs store (see
|
|
* services/content/src/sync/jobs/); a note's own values always win, and a
|
|
* note with nothing synced renders as before.
|
|
*/
|
|
import { describe, expect, it } from "vitest";
|
|
import { withSynced, type Note } from "../src/lib/content.ts";
|
|
import { itemDetails, readerProgress, type Item } from "../src/lib/library.ts";
|
|
import { optionGroups, type Line } from "../src/lib/store.ts";
|
|
|
|
const note = <T extends Note>(type: string, data: Record<string, unknown>, synced?: Record<string, unknown>) =>
|
|
withSynced({ path: `${type}/x.md`, type, name: "x", slug: "x", title: "X", body: "", acceptedAt: "", data: { type, ...data }, synced } as unknown as T);
|
|
|
|
describe("Library progress from PersonalReader", () => {
|
|
const base = { medium: "manhwa", state: "now", creator: "Someone", year: 2020 };
|
|
|
|
it("adds the chapter line to the details", () => {
|
|
const i = note<Item>("library-item", base, { chapterProgress: { title: "X", status: "now", chapter: 142, readAt: "2026-10-03T05:00:00.000Z" } });
|
|
expect(readerProgress(i)).toBe("Chapter 142, read Oct 3, 2026");
|
|
expect(itemDetails(i)).toEqual(["Someone", 2020, "Chapter 142, read Oct 3, 2026"]);
|
|
const book = note<Item>("library-item", { ...base, medium: "light novel" }, { bookProgress: { title: "X", percent: 62 } });
|
|
expect(readerProgress(book)).toBe("62% read");
|
|
});
|
|
|
|
it("lets the note's own progress win, and shows the same line when nothing is synced", () => {
|
|
const own = note<Item>("library-item", { ...base, progress: "16/18 volumes" }, { chapterProgress: { chapter: 3 } });
|
|
expect(itemDetails(own)).toEqual(["Someone", 2020, "16/18 volumes"]);
|
|
const bare = note<Item>("library-item", base);
|
|
expect(itemDetails(bare)).toEqual(["Someone", 2020]);
|
|
expect(readerProgress(note<Item>("library-item", base, { bookProgress: { percent: 0 } }))).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe("Store options with synced stock", () => {
|
|
const lineData = (options: unknown[]) => ({ category: "Tabletop", mode: "made to order", channel: "here", summary: "", priceTiers: [], options });
|
|
const stock = {
|
|
filamentStock: [
|
|
{
|
|
group: "Filament",
|
|
options: [
|
|
{ name: "Jade White", hex: "#f5f5dc", inStock: true },
|
|
{ name: "Bambu Green", hex: "#00ae42", inStock: false },
|
|
{ name: "Charcoal", hex: "#2b2b2b", inStock: true },
|
|
],
|
|
},
|
|
],
|
|
};
|
|
|
|
it("is exactly the note's options when nothing is synced", () => {
|
|
const opts = [
|
|
{ group: "Filament", name: "Jade White", hex: "#ffffff", inStock: true },
|
|
{ group: "Finish", name: "Matte", inStock: false },
|
|
];
|
|
expect(optionGroups(note<Line>("product-line", lineData(opts)))).toEqual([
|
|
["Filament", [opts[0]]],
|
|
["Finish", [opts[1]]],
|
|
]);
|
|
// Left out with nothing synced: in stock, as before the field was optional.
|
|
expect(optionGroups(note<Line>("product-line", lineData([{ group: "Finish", name: "Gloss" }])))[0]![1][0]!.inStock).toBe(true);
|
|
});
|
|
|
|
it("sits under the note: note values win, gaps take the synced value, unlisted in-stock colors follow", () => {
|
|
const l = note<Line>(
|
|
"product-line",
|
|
lineData([
|
|
{ group: "Filament", name: "Jade White" },
|
|
{ group: "Filament", name: "Bambu Green", inStock: true },
|
|
{ group: "Filament", name: "Galaxy Black" },
|
|
{ group: "Finish", name: "Matte", inStock: true },
|
|
]),
|
|
stock,
|
|
);
|
|
const [[g, filament], [, finish]] = optionGroups(l) as [string, ReturnType<typeof optionGroups>[number][1]][];
|
|
expect(g).toBe("Filament");
|
|
expect(filament).toEqual([
|
|
{ group: "Filament", name: "Jade White", hex: "#f5f5dc", inStock: true },
|
|
// The note says in stock: the note wins over the source.
|
|
{ group: "Filament", name: "Bambu Green", hex: "#00ae42", inStock: true },
|
|
// Not at the source: out of stock.
|
|
{ group: "Filament", name: "Galaxy Black", inStock: false },
|
|
{ group: "Filament", name: "Charcoal", hex: "#2b2b2b", inStock: true },
|
|
]);
|
|
expect(finish).toEqual([{ group: "Finish", name: "Matte", inStock: true }]);
|
|
});
|
|
});
|