Re-lands 901b01d, which was reverted wholesale by 28974d1 (BR-2 committed
from the shared index). The agent UI files 901b01d had rolled back are
left at their AGUI-2 versions; contracts, routes, db types and the runbook
part already reached main through later commits, so this carries the rest.
HHClub (憨憨) was checked read-only against a real account (one detail
request; list, user data and health came from the product's own fetch).
Parsing fixes:
- list titles carried the site's new-torrent marker "[新]" (58 of 100
rows; the detail page title has none); stripped with a replace filter
- refreshing a detail overwrote seeders / leechers / snatched / category
name / H&R with null because the HHClub div detail page lacks them;
detail values now fall back to the list snapshot (mergeDetailOverList),
and an unknown detail promotion keeps the list promotion
- level "贵宾" is VIP (umt.vipLevels)
- sanitized minimal fixture HHANCLUB_LIST_PAGE_2026 + tests; README marks
HHClub as verified 2026-09-29
Size, counts, category ids/names, +08:00 time and subtitle were correct.
Request restraint:
- site definitions may carry umt.defaults; merged with settings.sites
(longer intervals, fewer pages): new settings defaultSnapshotMaxPages 1,
defaultHealthCheckIntervalSec 6 h, defaultDetailCacheSec 24 h; default
burst 2. HHClub: snapshot 60 min page 1 only, health 6 h, search
concurrency 1, detail cache 24 h, 1 token / 3 s burst 2
- periodic / manual snapshots are capped to that page count; scheduled
health checks skip sites checked within the interval; detail refresh is
served from cache inside the window; live searches on one site queue
- every rate-limiter token is counted per site per hour
(site_request_count, migration 0046); GET /sites/{id}/request-stats;
site overview and resource list show requests in the last 24 hours
Resource list: two-layer rows. Main line: title + spec tags (episodes,
resolution, source, codec, HDR, group, 国语/中字, site tags) + promotion
with time left + H&R. Second line: subtitle (one line, ellipsis) and
fixed-width category / size / seed-leech-snatched / upload time
(relative, absolute on hover) columns, stale badge, inventory overlay;
folds into a card on phones.
Also aligns identity_candidate.normalized_title order in db types with
the migrations (schema drift test).
Takes effect after the stack is rebuilt (migration 0046).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- Inject the site account Cookie (manual + auto-login session, session wins)
into the camofox profile before the first tab opens; password accounts
without a session run refresh-login first; API Key sites are skipped.
- Cookie expired after injection -> credential.status=expired; cookie
accounts go to waiting/login_required ("log in once, applies site-wide").
- Write back site-domain cookies from the browser on handover, session close,
every 10 min of activity, and from refresh-login when the stored cookie is
invalid (verified before saving). user_info.cookieOrigin records
source=browser; source shown as manual/login/browser in the origin view.
- New camofox plugin umt-cookies (GET/POST /umt/sessions/:userId/cookies
under the access key); profile dir 0700/files 0600 in the desktop image.
- Single entry: sources-identity/service/credentials.ts.
- Real check: HHClub cookie injected into a fresh profile -> home page
logged in (logout link present), 5 cookies read back.
Requires rebuilding umt-browser-desktop (docker compose build camofox &&
docker compose up -d camofox) to take effect.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
HHClub (憨憨) was checked read-only against a real account (one detail
request; list, user data and health came from the product's own fetch).
Parsing fixes:
- list titles carried the site's new-torrent marker "[新]" (58 of 100
rows; the detail page title has none); stripped with a replace filter
- refreshing a detail overwrote seeders / leechers / snatched / category
name / H&R with null because the HHClub div detail page lacks them;
detail values now fall back to the list snapshot (mergeDetailOverList),
and an unknown detail promotion keeps the list promotion
- level "贵宾" is VIP (umt.vipLevels)
- sanitized minimal fixture HHANCLUB_LIST_PAGE_2026 + tests; README marks
HHClub as verified 2026-09-29
Size, counts, category ids/names, +08:00 time and subtitle were correct.
Request restraint:
- site definitions may carry umt.defaults; merged with settings.sites
(longer intervals, fewer pages): new settings defaultSnapshotMaxPages 1,
defaultHealthCheckIntervalSec 6 h, defaultDetailCacheSec 24 h; default
burst 2. HHClub: snapshot 60 min page 1 only, health 6 h, search
concurrency 1, detail cache 24 h, 1 token / 3 s burst 2
- periodic / manual snapshots are capped to that page count; scheduled
health checks skip sites checked within the interval; detail refresh is
served from cache inside the window; live searches on one site queue
- every rate-limiter token is counted per site per hour
(site_request_count, migration 0046); GET /sites/{id}/request-stats;
site overview and resource list show requests in the last 24 hours
Resource list: two-layer rows. Main line: title + spec tags (episodes,
resolution, source, codec, HDR, group, 国语/中字, site tags) + promotion
with time left + H&R. Second line: subtitle (one line, ellipsis) and
fixed-width category / size / seed-leech-snatched / upload time
(relative, absolute on hover) columns, stale badge, inventory overlay;
folds into a card on phones.
Also aligns identity_candidate.normalized_title order in db types with
the migrations (schema drift test).
Takes effect after the stack is rebuilt (migration 0046).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Adding a feed from a site page now only asks for the URL, purpose
(discover / track / promotion) and an optional note. POST /feed-sources
with siteId infers the site account (passkey holder first), records the
site definition version as parserVersion, defaults the name to
"<site> RSS" and polling to 30 minutes, and returns non-blocking
warnings when the URL host does not match the site. The dialog shows the
redacted URL, fetches once right after saving and reports the item count
or the error.
The generic feed form on the sources page suggests linking a registered
site when the URL host matches (can be undone). name is now optional.
Takes effect after the stack is rebuilt.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Migration 0041_rate_limit adds rate_limit_bucket. PgTokenBucket (@umt/db)
refills and takes tokens under SELECT ... FOR UPDATE on the DB clock and
polls with jitter within a wait budget; penalize() carries Retry-After.
Site adapters get createSharedSiteRateLimiter (key site:<id>), shared by
api / worker / worker:agent, with an in-process serial queue in front and
a local fallback when the bucket is unavailable.
Co-Authored-By: Claude Opus 5.5 <[email protected]>