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:
@@ -0,0 +1,22 @@
|
||||
# Sync test fixtures
|
||||
|
||||
Small sample payloads written by hand from each API's documented response
|
||||
shape, for the parsing tests. They are **not** live responses: no request was
|
||||
made to Archidekt, BoardGameGeek, Open Library, TMDB, AniList, Suwayomi,
|
||||
Kavita, Bambuddy or Homebox to make them,
|
||||
and ids, names and image addresses are illustrative.
|
||||
|
||||
| File | Shape of |
|
||||
| --- | --- |
|
||||
| `archidekt-deck.sample.json` | `GET https://archidekt.com/api/decks/<id>/` |
|
||||
| `bgg-thing.sample.xml` | `GET https://boardgamegeek.com/xmlapi2/thing?id=…` |
|
||||
| `openlibrary-search.sample.json` | `GET https://openlibrary.org/search.json?title=…` |
|
||||
| `tmdb-search.sample.json` | `GET https://api.themoviedb.org/3/search/movie?query=…` |
|
||||
| `anilist-media.sample.json` | `POST https://graphql.anilist.co` with the `Media` query |
|
||||
| `suwayomi-library.sample.json` | `POST <SUWAYOMI_URL>/api/graphql` with `LIBRARY_QUERY` (Suwayomi-Server's GraphQL schema) |
|
||||
| `anilist-page.sample.json` | `POST https://graphql.anilist.co` with the batched `Page { media(id_in:) }` query |
|
||||
| `kavita-series.sample.json` | `POST <KAVITA_URL>/api/Series/all-v2` (SeriesDto list) |
|
||||
| `kavita-metadata.sample.json` | `GET <KAVITA_URL>/api/Series/metadata?seriesId=`, one answer per series id |
|
||||
| `bambuddy-spools.sample.json` | **A guess.** Bambuddy's API is not documented anywhere this project has; this only exercises `mapBambuddySpools`' tolerant reading. |
|
||||
| `homebox-labels.sample.json` | `GET <HOMEBOX_URL>/api/v1/labels` |
|
||||
| `homebox-items.sample.json` | `GET <HOMEBOX_URL>/api/v1/items?labels=…` |
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"_sample": "Hand-written from AniList's GraphQL Media response shape; not a live response.",
|
||||
"data": {
|
||||
"Media": {
|
||||
"id": 30013,
|
||||
"title": { "english": "One Piece", "romaji": "ONE PIECE" },
|
||||
"coverImage": {
|
||||
"extraLarge": "https://s4.anilist.co/file/anilistcdn/media/manga/cover/large/sample-xl.jpg",
|
||||
"large": "https://s4.anilist.co/file/anilistcdn/media/manga/cover/medium/sample-l.jpg"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"data": {
|
||||
"Page": {
|
||||
"media": [
|
||||
{ "id": 100001, "title": { "english": "Sample Tower Climber", "romaji": "Sample Tou" }, "format": "MANGA", "countryOfOrigin": "KR",
|
||||
"coverImage": { "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/manga/cover/large/sample-100001.jpg", "large": null } },
|
||||
{ "id": 100003, "title": { "english": null, "romaji": "Sample Owari" }, "format": "MANGA", "countryOfOrigin": "JP",
|
||||
"coverImage": { "extraLarge": null, "large": "https://s4.anilist.co/file/anilistcdn/media/manga/cover/medium/sample-100003.jpg" } },
|
||||
{ "id": 100004, "title": { "english": "New Sample Series", "romaji": "Shin Sample" }, "format": "MANGA", "countryOfOrigin": "KR",
|
||||
"coverImage": { "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/manga/cover/large/sample-100004.jpg" } },
|
||||
{ "id": 100005, "title": { "english": "Sample Dropped" }, "format": "MANGA", "countryOfOrigin": "JP", "coverImage": {} }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"_sample": "Hand-written from Archidekt's deck response shape; not a live response.",
|
||||
"id": 1234567,
|
||||
"name": "Sample Atraxa Superfriends",
|
||||
"deckFormat": 3,
|
||||
"featured": "https://storage.googleapis.com/topdekt-user/images/sample-featured.jpg",
|
||||
"customFeatured": "",
|
||||
"private": false,
|
||||
"categories": [
|
||||
{ "id": 1, "name": "Commander", "isPremier": true, "includedInDeck": true, "includedInPrice": true },
|
||||
{ "id": 2, "name": "Ramp", "isPremier": false, "includedInDeck": true, "includedInPrice": true },
|
||||
{ "id": 3, "name": "Maybeboard", "isPremier": false, "includedInDeck": false, "includedInPrice": false }
|
||||
],
|
||||
"cards": [
|
||||
{
|
||||
"quantity": 1,
|
||||
"categories": ["Commander"],
|
||||
"card": {
|
||||
"uid": "d0d33d52-3d28-4635-b985-51e126289259",
|
||||
"oracleCard": { "name": "Atraxa, Praetors' Voice", "colorIdentity": ["White", "Blue", "Black", "Green"] }
|
||||
}
|
||||
},
|
||||
{
|
||||
"quantity": 1,
|
||||
"categories": ["Ramp"],
|
||||
"card": { "uid": "aaaa1111-0000-0000-0000-000000000000", "oracleCard": { "name": "Sol Ring", "colorIdentity": [] } }
|
||||
},
|
||||
{
|
||||
"quantity": 34,
|
||||
"categories": ["Land"],
|
||||
"card": { "uid": "bbbb2222-0000-0000-0000-000000000000", "oracleCard": { "name": "Forest", "colorIdentity": ["Green"] } }
|
||||
},
|
||||
{
|
||||
"quantity": 1,
|
||||
"categories": ["Maybeboard"],
|
||||
"card": { "uid": "cccc3333-0000-0000-0000-000000000000", "oracleCard": { "name": "Lightning Bolt", "colorIdentity": ["Red"] } }
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"_comment": "GUESSED SHAPE. Bambuddy's API is undocumented here; this only exercises mapBambuddySpools' tolerant reading.",
|
||||
"items": [
|
||||
{ "id": 1, "material": "PLA", "color_name": "Jade White", "color_hex": "F5F5DCFF", "remaining_weight": 640 },
|
||||
{ "id": 2, "material": "PLA", "color_name": "Jade White", "color_hex": "F5F5DC", "remaining_weight": 20 },
|
||||
{ "id": 3, "material": "PLA", "colorName": "Bambu Green", "colorHex": "#00AE42", "remainingWeight": 15 },
|
||||
{ "id": 4, "material": "PETG", "color": "Translucent Orange", "hex": "#FF6A13", "remaining": 900 },
|
||||
{ "id": 5, "material": "PLA", "color_name": "Old Spool", "archived": true, "remaining_weight": 500 },
|
||||
{ "id": 6, "material": "PLA", "color_name": "Charcoal", "color_hex": "#2b2b2b" }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Hand-written from BoardGameGeek's XML API2 "thing" response shape; not a live response. -->
|
||||
<items termsofuse="https://boardgamegeek.com/xmlapi/termsofuse">
|
||||
<item type="boardgame" id="266192">
|
||||
<thumbnail>https://cf.geekdo-images.com/sample__thumb/img/wingspan.jpg</thumbnail>
|
||||
<image>https://cf.geekdo-images.com/sample__original/img/wingspan.jpg</image>
|
||||
<name type="primary" sortindex="1" value="Wingspan" />
|
||||
<description>Attract a beautiful and diverse collection of birds &amp; more.</description>
|
||||
<yearpublished value="2019" />
|
||||
<minplayers value="1" />
|
||||
<maxplayers value="5" />
|
||||
<playingtime value="70" />
|
||||
<minplaytime value="40" />
|
||||
<maxplaytime value="70" />
|
||||
<minage value="10" />
|
||||
</item>
|
||||
<item type="boardgame" id="13">
|
||||
<image>http://cf.geekdo-images.com/sample__original/img/catan.jpg?a=1&b=2</image>
|
||||
<name type="primary" sortindex="1" value="CATAN" />
|
||||
<minplayers value="3" />
|
||||
<maxplayers value="4" />
|
||||
<playingtime value="120" />
|
||||
<minplaytime value="60" />
|
||||
<maxplaytime value="120" />
|
||||
</item>
|
||||
<item type="boardgame" id="99">
|
||||
<name type="primary" sortindex="1" value="No Data Yet" />
|
||||
<minplayers value="0" />
|
||||
<maxplayers value="0" />
|
||||
<playingtime value="0" />
|
||||
</item>
|
||||
</items>
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"page": 1,
|
||||
"pageSize": 100,
|
||||
"total": 3,
|
||||
"items": [
|
||||
{ "id": "a1", "name": "Veg tan, Walnut", "quantity": 2, "description": "Swatch #5c3a21, 3-4 oz", "archived": false },
|
||||
{ "id": "a2", "name": "Chrome tan, Black", "quantity": 0, "description": "", "archived": false },
|
||||
{ "id": "a3", "name": "Retired hide", "quantity": 4, "description": "", "archived": true }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
[
|
||||
{ "id": "6f1c0a52-0000-4000-8000-000000000001", "name": "Leather", "description": "" },
|
||||
{ "id": "6f1c0a52-0000-4000-8000-000000000002", "name": "Paper", "description": "" },
|
||||
{ "id": "6f1c0a52-0000-4000-8000-000000000003", "name": "Tools", "description": "" }
|
||||
]
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"11": { "seriesId": 11, "publicationStatus": 0, "webLinks": "https://www.novelupdates.com/series/sample/,https://anilist.co/manga/200001/Sample-Light-Novel/" },
|
||||
"12": { "seriesId": 12, "publicationStatus": 0, "webLinks": "" },
|
||||
"13": { "seriesId": 13, "publicationStatus": 2, "webLinks": "https://anilist.co/manga/200003" }
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
[
|
||||
{ "id": 11, "name": "Sample Light Novel", "localizedName": "", "pages": 400, "pagesRead": 400, "latestReadDate": "2026-10-02T21:15:00", "libraryId": 2 },
|
||||
{ "id": 12, "name": "Sample Ongoing Novel", "localizedName": "Sample Ongoing Novel (EN)", "pages": 300, "pagesRead": 120, "latestReadDate": "2026-10-09T08:00:00", "libraryId": 2 },
|
||||
{ "id": 13, "name": "Sample Unread Novel", "pages": 250, "pagesRead": 0, "latestReadDate": "0001-01-01T00:00:00", "libraryId": 2 }
|
||||
]
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"_sample": "Hand-written from Open Library's search.json response shape; not a live response.",
|
||||
"numFound": 2,
|
||||
"docs": [
|
||||
{ "title": "The Way of Kings (study guide)" },
|
||||
{ "title": "The Way of Kings", "cover_i": 8221255, "cover_edition_key": "OL24395542M" }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"data": {
|
||||
"categories": {
|
||||
"nodes": [
|
||||
{ "name": "Default", "mangas": { "nodes": [
|
||||
{ "title": "Uncategorised Series", "inLibrary": true, "latestReadChapter": null, "trackRecords": { "nodes": [] } }
|
||||
] } },
|
||||
{ "name": "Reading", "mangas": { "nodes": [
|
||||
{ "title": "Sample Tower Climber (Source Title)", "inLibrary": true,
|
||||
"latestReadChapter": { "chapterNumber": 142, "lastReadAt": "1759900000" },
|
||||
"trackRecords": { "nodes": [
|
||||
{ "trackerId": 1, "remoteId": "555", "title": "Sample Tower Climber MAL", "lastChapterRead": 140 },
|
||||
{ "trackerId": 2, "remoteId": "100001", "title": "Sample Tower Climber", "lastChapterRead": 142 }
|
||||
] } },
|
||||
{ "title": "New Sample Series", "inLibrary": true,
|
||||
"latestReadChapter": { "chapterNumber": 3, "lastReadAt": "1759990000" },
|
||||
"trackRecords": { "nodes": [ { "trackerId": 2, "remoteId": "100004", "title": "New Sample Series", "lastChapterRead": 3 } ] } },
|
||||
{ "title": "Untracked Sample", "inLibrary": true,
|
||||
"latestReadChapter": { "chapterNumber": 7, "lastReadAt": "0" },
|
||||
"trackRecords": { "nodes": [] } }
|
||||
] } },
|
||||
{ "name": "Plan to read", "mangas": { "nodes": [
|
||||
{ "title": "Sample Later", "inLibrary": true, "latestReadChapter": null,
|
||||
"trackRecords": { "nodes": [ { "trackerId": 2, "remoteId": "100002", "title": "Sample Later", "lastChapterRead": 0 } ] } }
|
||||
] } },
|
||||
{ "name": "Completed", "mangas": { "nodes": [
|
||||
{ "title": "Sample Tower Climber (Source Title)", "inLibrary": true,
|
||||
"latestReadChapter": { "chapterNumber": 142, "lastReadAt": "1759900000" },
|
||||
"trackRecords": { "nodes": [ { "trackerId": 2, "remoteId": "100001", "title": "Sample Tower Climber", "lastChapterRead": 142 } ] } },
|
||||
{ "title": "Sample Finished", "inLibrary": true,
|
||||
"latestReadChapter": { "chapterNumber": 88.5, "lastReadAt": "1750000000000" },
|
||||
"trackRecords": { "nodes": [ { "trackerId": 2, "remoteId": "100003", "title": "Sample Finished", "lastChapterRead": 88 } ] } }
|
||||
] } },
|
||||
{ "name": "Dropped", "mangas": { "nodes": [
|
||||
{ "title": "Sample Dropped", "inLibrary": true,
|
||||
"latestReadChapter": { "chapterNumber": 12, "lastReadAt": "1740000000" },
|
||||
"trackRecords": { "nodes": [ { "trackerId": 2, "remoteId": "100005", "title": "Sample Dropped", "lastChapterRead": 12 } ] } }
|
||||
] } }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"_sample": "Hand-written from TMDB's search/movie response shape; not a live response.",
|
||||
"page": 1,
|
||||
"results": [
|
||||
{ "id": 1, "title": "Arrival (short)", "poster_path": null },
|
||||
{ "id": 329865, "title": "Arrival", "release_date": "2016-11-10", "poster_path": "/x2FJsf1ElAgr63Y3PNPtJrcmpoe.jpg" }
|
||||
],
|
||||
"total_results": 2
|
||||
}
|
||||
Reference in New Issue
Block a user