AGENTS.md
Rama main · réplica de lectura del repositorio original.
# Agent instructions
This is the canonical repository harness. Read [CONTRIBUTING.md](CONTRIBUTING.md) before
planning, editing, testing or reviewing. Its PR checklist is mandatory. The user removed
the alternate agent instruction files; do not recreate copies or links.
## Workflow
- Use focused conventional commits. When merging is authorized, use a merge commit with
a conventional subject and PR number (for example `feat(desktop): add native application (#123)`),
not the default merge message.
- Use descriptive `feat/` or `fix/` branches, conventional commits and PRs against
`ohne-b/twitch-drops-miner:main`. Keep branches, commits and documentation free of assistant branding.
- Preserve existing user changes, data, credentials, logs and backups. Ask before significant
refactoring unless the current task already authorizes it. A full rewrite authorization
covers its necessary cleanup. Never change a running deployment without authorization.
- Use one implementation agent when requested. Required independent adversarial review uses
a separate read-only reviewer; the author cannot approve their own work.
- Integrate current main before final validation/review and again before merge if it advances.
Keep incomplete PRs in draft. Record tested/reviewed revisions and actual results; never
describe unrun checks, live progress or review as successful.
- Add backend unit/regression tests; cover frontend changes where practical. Update this
harness when behavior/architecture changes. Update README only when setup, essential usage,
updates or troubleshooting change; a new feature alone does not require more README text.
Update `lang/English.json` for UI or console changes. The app is English only for now;
do not add translations, additional locales or language settings.
- Keep README as a short entry point and user guides in the GitHub wiki. Contributor policy
stays in CONTRIBUTING. Review wiki revisions with the related PR and verify published links
before removing README instructions. Keep wiki editing restricted to collaborators.
Do not create `docs/`, `plans/` or repository planning documents.
- GitHub release publication uses the repository owner's scoped `prod` `RELEASE_TOKEN`;
verify its identity before publishing images. Registry publication retains the workflow's
built-in token. Keep tokens out of source, logs and chat; never reuse a local CLI credential
as an Actions secret.
- Release and edge callers explicitly pass only `DOCKERHUB_TOKEN` to the reusable mirror
workflow, which declares it required and reads it from `prod`. Do not inherit all secrets.
## Architecture
The product is Twitch Drops Miner (dashboard: Drops Miner), repository and
Cargo package/binary are twitch-drops-miner. Preserve the existing Compose service/container name,
data/log directories, TDM log prefix, auth cookie and CSRF header for upgrade compatibility.
A Cargo workspace separates the shared core in `crates/core/` from the root server package
and the Tauri wrapper in `desktop/`. The core owns mining, application commands and durable
storage; it has no production HTTP-server or graphical dependencies. Both wrappers use its
owned runtime and credential-safe logging. Use concrete structs with methods and composition
for domain/services (the repository's OOP requirement), typed enums and DRY shared policies.
Do not add forwarding hierarchies or speculative traits with one implementation.
| Module | Responsibility |
| --- | --- |
| `src/main.rs` | Server CLI/env, HTTP lifecycle and healthcheck |
| `crates/core/src/runtime.rs`, `logging.rs` | Shared owned miner shutdown and credential-safe logging |
| `crates/core/src/accounts/` | Installation-owned account registry, runtimes, onboarding, bulk commands and shared Connection settings |
| `config.rs`, `dto.rs` | Typed settings, compatible migration, API snapshots |
| `domain.rs`, `policy.rs` | Campaign/drop/channel eligibility and dependency-aware ignores |
| `store.rs` | Exclusive data directory, atomic settings/history/archive/claim journal |
| `auth.rs`, `origin.rs` | Dashboard passwords/sessions, origin and cookie policy |
| `twitch/` | OAuth, bounded HTTP/GQL, inventory/catalog, playlist/segment checks, beacon telemetry, PubSub shards |
| `app/` | Application commands/settings, typed Activity, revisioned snapshot publication and patches |
| `miner/` | One session supervisor and owned jobs; session, watch, inventory and claim responsibilities |
| `store/records.rs` | Frozen version-one archive/journal records, independent of runtime presentation fields |
| `web/` | Axum HTTP, Socketioxide, protected snapshots and embedded frontend |
| `fixture.rs`, `bin/dashboard-fixture.rs` | Feature-gated offline browser fixture |
| `frontend/`, `lang/English.json` | React/TypeScript/Tailwind and one message catalog |
The domain/application/storage/Twitch modules below live in `crates/core/src/`; web and
dashboard authentication remain in the root server package. Workspace package metadata owns
the shared version. Default Cargo members validate the server and core without desktop system libraries.
Build frontend assets before backend/static tests. `web/` is ignored output. Release binaries
embed it and run without a build tool/runtime companion. Production builds never enable
`dashboard-fixture`; fixture routes must return 404 in production.
## Desktop contracts
- `desktop/` is a Tauri wrapper around the same core and frontend. No HTTP listener,
sidecar, shell companion or alternate miner. It owns the same multi-account manager as
the server. Keep Docker behavior intact.
- Store native data/logs in the platform app directories, separate from the server. Keep
the production identifier `app.twitch-drops-miner.local` and a separate `.fixture` identity.
Do not migrate or delete the former `dev.ohneb.dropsminer` directories.
Keep existing record formats; never import, delete or rewrite an installation automatically.
Store desktop preferences separately and report unreadable storage instead of resetting it.
- Native IPC is limited to the bundled main window, typed commands and bounded requests.
Cancel reads, preserve accepted writes, coalesce snapshot patches and fence stale replies.
Restrict navigation; validated external HTTP(S) links open in the system browser.
The main-window opener capability permits only HTTP(S) URLs through the default browser;
do not grant arbitrary programs, file URLs or the broader opener default permission set.
Native CSP permits bundled fonts from local assets and data URLs, including Vite's inlined subsets.
- Use official Tauri tray, single-instance, window-state, autostart, notification and updater
plugins. Reopening restores the existing window. Tray pause changes the shared saved setting.
Default close-to-tray off on Linux, where tray hosts vary. Failed tray creation must leave
the window reachable. Quit/restart/install own and drain core and native work.
The window title and supported tray tooltip use the same frontend mining-title formatter,
including confirmed percentages, pause and disconnect state. Apply both through one bounded,
local-window command; never derive a separate estimated tray counter.
The tray menu begins with a disabled status row and separator. Update it through that same
command, omit only the trailing app-name suffix, and escape native menu mnemonic markers.
Retain game-only, channel-watching, paused, idle, disconnected and account-unavailable states.
- Desktop-only `keep_awake` defaults false, including older preferences. Request idle-system
sleep prevention only for a logged-in, unpaused Watching/AwaitingProgress/ManualWatching
snapshot; release on idle, pause, logout, disable and shutdown. Never inhibit the display
or explicit user sleep. Keep platform requests off async/UI threads and acquire/release on
one owned blocking thread (required by Windows), coalescing state changes and draining on
exit/install. Report acquisition failures without stopping mining. Offline fixtures and
unit tests use fake guards, never real power requests. Linux uses systemd idle inhibition
with a five-second bound covering D-Bus connection/authentication and Inhibit; hold only
the returned owned descriptor. Document that desktop power-manager support varies.
- Enable Tauri's native zoom hotkeys and mouse-wheel support for the bundled main window,
with its scoped webview-zoom permission. Use Ctrl +/−/0 (Command on macOS) and Ctrl+wheel;
keep the browser dashboard's own zoom behavior unchanged.
- Updates download and verify before stopping mining. Require signed versions, no downgrades,
one operation at a time and cancellable bounded downloads. A failed install stays visible
with restart/manual recovery. Only an explicit install action may replace the application.
Show a quiet, clickable Update v… link beside the sidebar's GitHub icon when
an update version is known; keep it beside the brand in compact windows without adding height.
Truncate long labels while retaining the full accessible name and tooltip. Reuse the existing
update state across navigation. The link opens Settings > Maintenance, where the installed
version sits beside the icon-only update check and inline status. Tray update checks open
the same section; never open a separate update dialog. Keep download/cancel, verified install
and restart recovery inline, using Tauri's updater. Show its reported download bytes in a
custom progress bar with percentage and size when the total is known; otherwise show only
bytes received. Reserve the progress row while an update version is known so download,
cancellation and readiness do not shift the controls below. Download completion is not
signature verification or permission to install.
Checking/current states do not add a sidebar link. Show Cancel only during downloads;
checks use the busy icon and inline status without an extra action row.
Navigation never installs automatically.
Desktop Maintenance omits the server's Advanced actions / Shut down miner controls; native
Quit remains in the tray. Omit the redundant application-description sentence in both wrappers.
Desktop folder shortcuts use Data / Logs with folder icons and descriptive accessible names.
Label the download action Update with the MDI Download icon; retain explicit verified install.
- Ship Windows x64 NSIS, universal macOS DMG plus the updater app archive, and Linux x64
AppImage/DEB. No MSI or portable Windows distribution. Keep one backward-compatible
`latest.json` for server and desktop. Platform-specific updater entries choose the right
package; both Mac architectures share the same universal update archive.
Release filenames use `twitch-drops-miner-VERSION-PLATFORM`: `windows-x64-setup.exe`,
`macos.dmg`, `macos-universal.app.tar.gz`, `linux-x64.AppImage` and `linux-x64.deb`.
Keep `latest.json` and `SHA256SUMS` unversioned; generate manifest URLs from the same names.
- Validation builds the release bundles once. Publication only verifies/signs/promotes those
exact artifacts from current-main validation. The updater private key stays in `prod`;
pull requests never receive signing secrets. OS signing is separate from updater signing.
macOS bundles use Tauri's ad-hoc identity before the DMG and update archive are created;
verify the extracted archive's bundle signature and both architectures in CI.
- `desktop-fixture` is temporary/offline test code, excluded from all production builds and
installers. Native smoke checks exercise the real webview and IPC without Twitch access.
## Multiple accounts
- One `AccountManager` owns independent account applications and their runtimes. Switching
the viewed account never pauses another account. Bound active profiles to 20 and stagger
startup. An account worker/storage failure remains local; process shutdown drains all workers,
accepted writes, onboarding, public catalog work and the one Telegram worker.
- `accounts.json` binds opaque local IDs to verified immutable Twitch user IDs. The original
profile keeps its files at the data root; additional profiles live in `accounts/<id>/`.
Preserve existing history/archive/journal formats. Old data without a known identity stays
unassigned until explicitly associated. Duplicate identities reuse the existing profile.
Sign out retains settings/history; Remove drains the account, removes its session and hides
the registry entry without deleting retained data. Readding restores that identity's profile.
- Onboarding uses the existing device-code OAuth flow, one owned attempt at a time. Verified
credentials stay in memory until Finish, expire after ten minutes and never enter public
snapshots. No mining or claiming starts before Finish. Reauthentication cannot change the
bound Twitch user. Signed-out managed profiles wait without starting hidden login flows.
Sign-out/removal invalidates targeted onboarding; Connection changes cancel pending onboarding
with retry feedback. Old verification results cannot restore a removed account.
- Every account HTTP/IPC request and full-state subscription has an explicit profile ID.
Legacy unscoped requests work only with one visible account. No mutable server-wide viewed
account exists. Snapshots/patches carry account and process identities; old subscriptions and
delayed requests cannot follow a UI switch. Account overview publications contain summary
data only. Global auth, Connection, Telegram and shutdown routes remain separate.
- `connection.json` owns proxy, quality and inventory refresh interval for the installation,
including Telegram's proxy. Per-account settings expose the effective values for compatibility
but cannot change them. Persist validated legacy Connection values once on first load, preserving
the original settings. Save shared values atomically before fanout; inject them on restart.
Unreadable primary settings remain isolated when the shared configuration is available.
A shared public catalog cache coalesces downloads for at least five minutes anonymously or
one minute with an optional installation-owned Sunkwi API key, including failures. Honor longer
RateLimit-Reset/Retry-After cooldowns without immediate retries, across accounts, refreshes and
Connection changes. Retain the last valid feed only within the existing freshness window.
Store the optional key in connection.json, never snapshots, logs or per-account settings;
expose only whether it is configured. Omitted keys preserve it; an explicit empty key clears it.
Send X-API-Key only on catalog requests. Inventory refresh timing remains separate,
while account parsing and evidence stay separate. Shared HTTP scheduling reserves capacity
for beacons, progress, playback tokens and claims separately from discovery. Never share
authenticated clients, cookies, account inventory or claim evidence.
- Bulk pause/resume and preference copying capture target IDs and settings revisions. Apply
each independently and report partial failures. Copy games/order, priority, automatic reward
rules, ignores and reward types; copying auto-claim is explicit. Do not copy pause, manual
selection, credentials, history, display filters, Connection or Telegram settings.
- One Telegram bot handles all accounts. `/accounts` offers an owner-only paginated selector,
independent of the viewed UI account; existing commands target that saved selection. Inline
selections require a fresh issued nonce, matching private owner and bot identity and a valid
current profile. Configuration changes/restarts discard them. Multi-account replies and
claim notifications name the account. Account logout fences only that account's deliveries.
Claim alerts default to all accounts, including newly added accounts. An explicit selection
persists Twitch user IDs in `telegram.json`; an empty selection sends no claim alerts.
Selection does not restrict commands or tests. Configuration changes cancel pending alerts
through the configuration epoch. Keep the selector beside the claim-alert switch.
- The account switcher lives in the existing sidebar/compact header; management is in Settings >
Accounts. The compact header shows a 36px round avatar, equipped badges and the colored
name; retain the sidebar's 32px avatar. Truncate long names without shrinking the artwork,
and scope mobile logo sizing to the logo itself. Below 640px, a native update link may
replace the app-name text beside the logo to leave room for the account identity.
Switching keeps the current page and section; restore an account's filters and
scroll only on that same page. Preserve account drafts and Up next disclosures. Keep IDs
in multi-account navigation/deep links. Global Connection/Telegram drafts and in-flight saves
survive switching; dashboard logout clears private drafts. Suspend unavailable subscriptions,
fence old publications and reconnect with bounded backoff after recovery.
Done stays disabled after accepted login confirmation until verification ends, including
after reopening the dialog. Account rows expose rename/sign-out/remove icon buttons;
profile and badges stay expanded without a separator above them. Keep Add account at the
top right of the account table, with icon-only bulk actions on the same row, and its
login help beside the dialog title. With one account, omit the table and bulk controls,
retain account actions beside the profile, and link the sidebar/compact profile directly
to Accounts settings. Use the switcher and table when more accounts are added. Dialog help
uses the existing anchored desktop placement with content-sized height to avoid clipping;
retain viewport placement on phones and browsers without anchor positioning.
Bulk actions stay quiet on success; show only failures for retained accounts with a retry action.
Put explanatory text in the existing info popover,
keeping errors and destructive-action confirmations visible.
- Desktop tray actions distinguish the viewed account from all accounts and capture revisions.
Keep-awake is installation-wide: any connected, unpaused, actively watching account may hold
the single owned guard. Closing the current native subscription clears viewed tray selection.
Quit, restart and installation drain all accounts.
## Mining contracts
- Discovery does not select games. `games_to_watch` is the ordered Unicode-casefolded
automatic allowlist. Opt-in auto_mine_badges/auto_mine_emotes default false and add matching
watch rewards from other games, with dependency closure and existing benefit/ignore rules.
Selected games rank first; other campaigns rank by soonest end. Use the shared mining
policy for discovery, selection, estimates, progress eligibility and Up next. Display
filters remain separate. Empty with both options off means no automatic watch events. Mine selects a game across
eligible campaigns. An explicitly chosen manual channel overrides automatic selection
without changing saved games, filters or priorities.
- `mining_priority_mode` defaults to `manual` and preserves the saved game order.
`short_events` promotes active selected-game reward windows of at most 24 hours total;
`ending_soonest` ranks active selected-game reward deadlines regardless of duration.
Use campaign/drop date intersections, earliest deadline then saved game order, and
preserve equal-priority watching. Required earnable prerequisites inherit target deadlines
(bounded by their own expiry); impossible, ignored or filtered target branches cannot
boost unrelated rewards. Confirmed completion ends a boost without inventing a claim;
estimates do not. These modes do not predict completion feasibility, reorder saved games,
preempt manual channels or promote optional other-game rewards above selected games.
Share ranking across discovery before channel truncation, selection, fallback display and
Up next; retain its grouping and upcoming visibility without preempting before opening.
- Preserve campaign/drop timing, prerequisites, claim state, benefit filters and ignore rules.
Literal ignore substrings cascade through dependents while retaining shared prerequisites.
Zero-minute subscription rewards are omitted from Campaigns/Up next; expired drops leave
the queue without hiding upcoming or sequential rewards. Ignore/skip is never completion.
Up next omits rewards whose confirmed watch time is complete, even before a claim; estimates
cannot remove them. This display rule never claims rewards, unlocks prerequisites or stops
background claim/reconciliation work. Campaign details retain their real claim state and
the existing game-account connection action.
- Active campaigns with existing progress appear first. Campaign completion requires all watch rewards
claimed; expiry alone never qualifies. Persistent completion archives are display-only,
survive cache clears, and can be invalidated by newer contradictory account evidence or
changed rewards. Older claim-only history remains completion-unverified.
- Fetch Twitch account Inventory and `https://drops.sunkwi.com/api/twitch/drops` concurrently.
SunkwiBOT is the catalog source; remove Twitch catalog/detail operations and live-channel
campaign scans. Inventory wins as whole records, including explicit unclaimed evidence;
malformed account records must not fall back to public account assumptions. Public HTTP
uses an isolated client with no Twitch credentials, cookies or identifiers, no redirects,
the configured proxy/timeouts, and one attempt bounded to 30 seconds. Cap bodies at 16 MiB
and campaigns at 2000. Reject timestamps older than 30 minutes or over 5 minutes ahead.
Strip public campaign/drop `self` records; preserve real ACLs, dependencies and timing.
The public adapter normalizes an omitted `allow.channels` to an empty list only when
`allow.isEnabled` is explicitly false. Never infer that from absent/invalid flags or
replace supplied channel values; account inventory still requires its channel field.
Reject mixed-null enabled ACLs. Shared domain parsing rejects campaign/drop dates without
room for the scheduler's one-hour lead and the claim journal's 24-hour grace period.
Missing restrictions/dependencies, malformed/null entries and duplicate IDs are partial,
not empty success. Shared account parsing preserves explicit null/empty ACL and dependency
lists and nullable/default ACL flags, while rejecting missing collections and wrong types.
Exclude every occurrence of duplicate account campaign IDs and reserve rejected IDs against
public replacement and award-only pending-claim recovery; confirmed durable receipts still recover.
Keep known active/upcoming records on partial refresh; valid empty
feeds are authoritative. A feed 401/403 never logs out Twitch; Twitch auth/cancellation
failures propagate. Do not turn partial coverage or a normal cache cooldown into a blanket UI warning or a
failed inventory refresh; log actual public-request failures. Coverage can vary, and restarts need the feed for non-inventory
campaigns. Never claim relogin/cache clearing repairs feed coverage. Unknown
PubSub/CurrentDrop progress shares a coalesced, at-most-once-per-minute inventory refresh
with confirmed watch completion and reported successors awaiting prerequisite claim evidence.
Retry unresolved evidence through the claim grace period, including after failed refreshes,
retaining unresolved expired campaigns when a partial refresh omits them,
without blocking watch cadence. Only account claim evidence unlocks prerequisites or History.
- Unknown linkage is null and unknown progress has no confirmed timestamp. Infer claims from
awards only when every benefit has evidence in the drop's time window and no explicit
account record contradicts it.
- Special Events (`509663`) and IRL (`509672`) cross categories only with a nonempty enabled
actual ACL. Automatic drops need matching category and drops-enabled status; all need live
channels, selected games or opted-in reward types, and eligible rewards. Offline/ineligible streams yield even at tied
fallback priority. Preserve nullable viewer counts and the watching row during rebuilds.
- Channels publishes currently eligible selected-game or automatic-reward streams plus the explicitly selected
manual channel. Directory discovery requests and consumes at most 100 streams per eligible game;
retain the overall 199-channel bound and existing PubSub limits. Rank automatic candidates before the channel limit using matching campaign
priority, including actual-ACL special-category streams. Mine channel accepts a validated
Twitch login/root URL and resolves it with owned bounded work independently of inventory.
Channel search uses authenticated Helix search with at most 20 live/offline results, sharing
the owned, bounded game-query queue and generation cancellation through HTTP and native IPC.
Search only while the manual picker is open; debounce and cancel superseded requests. Keep
exact-login/URL submission available when search misses a channel or fails. Search results
do not expand mining subscriptions. Validate Twitch CDN avatars and show them in the picker
and Channels, with a fallback for missing art; never persist this metadata.
Manual selection accepts an existing offline identity and waits for it to go live. Watching
requires a live identity, not catalog coverage, selected games, a drops tag or local reward eligibility. Do not persist game selections or fabricate reward progress.
Direct lookup and manual stream refresh use a short best-effort metadata attempt;
missing/failed metadata never blocks watching, but auth/cancellation still propagates.
Show a known manual reward only after Twitch reports it; do not estimate manual rewards.
Preserve pending lookups and the confirmed channel separately across network generations;
late/superseded results cannot restore manual mode. Manual stream metadata never overrides
real ACLs or account records. Pending stream refresh pauses watching; retry failed refreshes.
An optional 1..1440-minute timer uses a monotonic deadline starting at selection, survives
network renewal/browser reconnect, and returns to automatic selection at expiry. Offline
manual channels wait without switching targets or stopping the timer. Exit, logout, cache
clear and process restart end manual mode. Viewer counts use channel-only events.
- Watch events use validated Twitch beacon URLs and a base64 minute-watched payload every
59 seconds. HTTP clients expire idle pooled connections after 15 seconds. Beacons use the
shared five-attempt transport/429/5xx retry policy with cancellable backoff (1/2/4/8 seconds,
or Retry-After bounded to 1..60 seconds). Retry the identical payload, never an acknowledged
204 or ordinary 4xx, and never count retries as progress. Exhaustion returns to owned watch
recovery. Independently poll the selected broadcast's lowest-bandwidth HLS playlist about
every 10 seconds and HEAD each unseen media segment; never GET video/audio bodies.
Request PlaybackAccessToken with the existing Smart TV session and use the isolated public HTTP
client for validated HTTPS Twitch/CDN playlist and segment URLs, with no redirects or
account headers/cookies. Bound playlist bodies to 512 KiB, entries and successful segment
cache to 256, each poll to 10 seconds, playlist requests to 5 seconds and HEADs to 3 seconds.
Retry failed segments on the next poll; renew expired URLs on 401/403/404 without logging out.
Keep signed URLs/tokens out of logs, snapshots and storage. Retain successful segment checks
across same-broadcast metadata refreshes; reset on stream/selection/network generation changes.
Own and cancel playback work on pause, deselection, offline/changed streams, cache clear and
shutdown. Manual timers and shared watch eligibility still apply. Playback acknowledgements
never confirm or estimate minutes, accelerate beacon/progress polling or imply claim success.
Confirm via PubSub or CurrentDrop, distinguish
estimates, and recover at 15 unconfirmed estimates. Only currently eligible drop progress
suppresses fallback. Failed/unacknowledged beacons invalidate the owned channel's cached
address. Three consecutive current-stream watch failures renew the network generation;
successful acknowledgements reset this count. Automatic selection then avoids that stream
for ten minutes, across the renewal: it ranks the remaining eligible streams normally,
keeps the current one among equals, and may mine a lower-priority game until the cooldown
ends. Several streams can be avoided at once; when all eligible streams are, one is still
watched. Avoidance survives renewal only for the same account. Manual choices are never
avoided and their failures never start avoidance; cache
clear or logout forget it.
Late request results cannot overwrite newer
account/stream events or restore invalidated beacon addresses.
Successful inventory refreshes clear stale estimate ceilings on refreshed and retained
drops, preserving confirmed progress and evidence newer than the request. Failed
refreshes must not clear the ceiling or fabricate confirmation.
Fresh account inventory may correct conflicting live progress for the same campaign/drop
and unchanged watch requirement. Keep the disputed live counter separate; use inventory
minutes/timestamps for display and eligibility, without estimates advancing them. Reconcile
through the existing once-per-minute refresh through the claim grace period. Older requests,
public metadata and partial-refresh retained records cannot resolve a dispute. Preserve
disputes across same-account network renewal; cache clear, logout and account changes cannot
restore them. Retain fresh account claims and issued instance IDs independently, including
completion backed by an issued instance. Claims clear disputes; runtime evidence never
changes stored history/archive formats. Disputed live completion cannot stop watching.
Confirmed watch completion is no longer watch-eligible, but remains unclaimed until account
evidence arrives. The automatic Mining card prefers current eligible Twitch reports;
reported successor progress may display before prerequisite claim reconciliation, without
granting watch eligibility. Validate CurrentDrop's returned channel and reject polls older
than the current watch, stream/account refresh or accepted progress. Unrelated/regressive
progress must not restore an old card; duplicate PubSub progress cannot displace a reported
successor or postpone polling. Stream replacements fence old polls even on the same channel.
A null CurrentDrop session or the empty object (explicit null channel/game, empty drop ID,
zero current/required minutes) is no result; retain validation for malformed nonempty sessions.
Estimates alone never imply completion or a claim.
- Claims require account-issued instance IDs, skip upcoming campaigns and stop at the strict
campaign-end + 24-hour deadline. Earned claims are independent of mining/ignore selection.
Per-account `auto_claim_drops` defaults true, including existing settings. When false, continue
watching, inventory imports and confirmed receipt recovery, but do not start claim mutations
or replay unconfirmed journal intents. Leave Twitch drop reminders in place while disabled.
Recheck the saved setting after intent persistence; an already-started claim may finish
and must retain durable results and duplicate protection.
Re-enabling resumes eligible claims within their existing deadlines. Only confirmed claims
unlock dependent rewards; disabling automatic claims never marks a reward claimed.
Persist the account-scoped intent before RPC, then its success receipt and history. Keep the
receipt through the claim window after durable history/archive recovery. Settled receipts
suppress the same account/drop/instance across refresh, restart and cache clear without
overriding fresh unclaimed inventory. A new instance or an explicit false-to-true account
linkage permits another attempt, without repeating the reward announcement. Recover unfinished
writes even without catalog metadata; prune settled receipts only at the strict claim deadline.
- History imports Twitch-confirmed claims during inventory refresh, independent of mining
selection and reward type. Require account claim state or complete in-window award evidence;
stale local progress must not override newer award evidence. Deduplicate by drop ID and use
known award timestamps, labeling unknown claim times as first observed.
Persist cleared IDs in the compatible version-1 history file so imports and late claim
receipt recovery cannot resurrect cleared entries. Imports never issue claim RPCs or
fabricate missing campaign metadata.
- Every session/job/socket task is owned and drained. Logout coalesces and removes only Twitch
credentials after drainage; concurrent shutdown cannot interrupt removal in either queue order.
Validate Twitch tokens at startup and hourly with owned, cancellable work independent of
mining and inventory scheduling. Valid checks keep the current network generation, inventory
deadline, progress, manual selection/deadlines and sockets. Failed checks retain credentials
and retry after one minute using bounded HTTP requests; rejected tokens drain the generation
before the existing authentication path renews them. Account/client mismatches still reject
the session. Network reconfiguration preserves manual selection/deadlines and queues new
channel choices until fresh channel eligibility is available. Cache clear
preserves settings, credentials and completed campaigns, and clears local claim history with
durable cleared-ID tombstones before refreshing; failure to persist must fail the clear.
- Requests use bounded concurrency/rate, retries and cancellation. Connection Quality defaults
to 3 for new or missing settings; preserve explicit saved values. Quality 1..6 controls connect
timeout 5×quality and total 10×quality seconds; the saved refresh interval actually schedules
inventory work. Slow discovery must not block watch cadence. Duplicate idle prompts collapse.
A network generation that fails within a minute of starting is replaced after 5 seconds,
doubling per consecutive failure to 320 seconds, so a persistent fault that ends generations
cannot repeat authenticated requests every few seconds; a session that cannot be saved
backs off the same way. Longer-lived generations, routine validation, proxy or connection
quality changes, reauthorization and logout reset the delay. A waiting replacement never
blocks commands, and Refresh starts it at once with the delay reset; the session-save delay
ends only by elapsing, logout or such a connection change. Other login and validation
errors keep their bounded request retries and 5-second pause.
Nonfatal notification dismissal failures remain visible but never extend or clear the shared
scheduling retry deadline; authentication and cancellation still propagate.
Interrupted response bodies retry within the existing five-attempt budget only for replayable
GETs and known persisted read-only GraphQL operations/batches. Never replay successful OAuth
exchanges, mutation responses or acknowledged 204 beacons. Preserve known failure statuses
when error bodies fail, including authenticated 401/403 versus ordinary public HTTP failures.
- Upstream diagnostics use server tracing only, never dashboard console/socket payloads.
Record operation/status/attempt, JSON syntax positions, rejected field types, typed network
causes and catalog rejection summaries. Basic logs allowlist known GraphQL messages and
fingerprint unknown messages. Advanced `TDM_DIAGNOSTICS=true`/`--diagnostics` is explicitly
opt-in and stays off with ordinary verbosity. The isolated tracing target may record bounded,
redacted JSON response previews and nested transport causes; never raw bodies, requests,
arbitrary headers, URLs or authenticated WebSocket frames. Collect request/proxy/cookie and
response credential values before redaction; redact sensitive fields and credential-bearing
text, fail closed on credential-inventory limits, and withhold non-JSON bodies and oversized
input strings. Bound redaction allocations and never reprocess inserted markers. Preserve
correlation, timing/status and hashes; mark capture omissions. Keep public errors unchanged,
and bound retry-body capture to one second without replacing retry/cancellation decisions.
## Telegram notifications
- Optional claimed-only Telegram notifications use the shared core notification channel and
an owned runtime worker, separate from Twitch work. Publish after successful durable claims;
never notify on estimates, imported history or cache clears. Browser/native notification
payloads stay unchanged. A bounded queue and account/drop deduplication make delivery best
effort; no persistent outbox or restart replay. Twitch validation/renewal does not restart it.
- `telegram.json` stores enabled/remote_control/token/numeric chat ID separately from public settings. Use
atomic private writes and revision conflicts; unreadable files remain untouched with Telegram
disabled. Tokens are write-only, omitted from snapshots/IPC reads, logs and browser drafts.
Blank input retains the saved token; explicit Remove configuration clears it. Accepted saves
are owned and drained. Configuration changes and Twitch logout fence/cancel pending delivery.
- The isolated Telegram client uses the configured proxy, fixed HTTPS API host, no redirects,
no Twitch headers/cookies, bounded messages/responses/timeouts and serialized paced sends.
Only explicit bounded 429 responses retry once; ambiguous delivery failures never replay.
Automatic messages wait cancellably behind a test; tests are single-flight. Failures cannot
stop mining or undo claims. Activity failures collapse; successful deliveries/tests recover them.
- Claim alerts use three consecutive lines, with a plain heading and the game and reward bold in HTML.
Escape and bound upstream names before formatting; never truncate completed markup.
- Include image defaults off, including existing Telegram configuration. Enabled claim alerts
keep the same text and request a compact Telegram link preview for validated public
HTTPS Twitch artwork; missing/unsafe artwork stays text-only. Telegram controls rendering.
Never upload/download image bodies or add another message; tests remain text-only.
- Settings > Notifications shares the same save/remove/test application methods through guarded
server HTTP and scoped desktop IPC. Keep setup help compact; no additional providers or
automatic ready/progress alerts. Tests/fixtures never contact real bots.
Form errors stay inline without a reload-settings action. Revision conflicts retain edits
and explain that reopening Notifications loads the current saved settings.
- Show the saved bot name as a chat link beside Telegram's heading. Resolve it with a
separate best-effort `getMe` read through the isolated client, bounded to three seconds
and 64 KiB. Cache validated name/username in memory per configuration; serialize lookups
and fence cancelled/stale replies. Keep tokens server-side and lookup failures separate
from saves/delivery. Fixtures disable lookup traffic. Save uses the MDI save icon and its
disabled state confirms success; omit the saved banner. Remove configuration is an
accessible, icon-only delete action; retain inline errors and test-send feedback.
- Remote control defaults off independently of claim alerts. The same owned runtime joins
a separate bounded long-poll loop using the isolated Telegram client. Only fresh, original
private messages from the configured positive chat ID and identical non-bot sender may
run `/status`, `/pause`, `/resume`, `/mine Game` or `/help` (`/start` also shows help).
Match known game names with shared casefolding; append without replacing games or changing
priorities, pause or manual mode. Reuse owned revision-checked settings saves; replies
report saved intent and confirmed progress only. Ignore forwarded, edited and group messages.
Discard startup/reconfiguration backlog and messages older than two minutes. Advance the
in-memory offset before execution; failed replies never replay commands. Config changes,
logout and shutdown cancel/fence pending work; accepted writes still drain. Register the
command menu for the owner chat only. Do not alter webhooks. Pace/retry failed polls with
bounded backoff and report receiver conflicts separately; keep failures out of mining.
Replies share paced delivery with claims/tests, work with claim alerts disabled, and never
contain tokens, raw requests or response bodies. Offline fixtures disable all bot traffic.
## Authentication and storage
- Original in-app device code only, Smart TV client identity, `twitch_oauth2` types/requests.
Keep explicit empty scopes, pending/slow-down/expiry/denial handling and token validation.
Channel pages use the web client URL. No browser-import or browser-renewal code/services.
HTTP 401/403 from unauthenticated pages, settings scripts or device discovery are ordinary
HTTP failures, never grounds to discard the saved session. Authenticated API/OAuth and
PubSub authentication failures still propagate to the session owner.
An interrupted unauthorized token-validation response still attempts the saved refresh token
before rejecting the session; refresh failures and account mismatches retain existing handling.
- New sessions use `twitch_session.json`; keep old credentials/backups untouched for rollback.
Invalid new sessions are preserved separately before reauthorization. Never log OAuth tokens,
device secrets, proxy credentials, cookie values or raw authenticated transport frames.
- Existing settings, version-1 history, completion and dashboard-auth formats remain compatible.
Atomic replacement commits before memory/publication. Corrupt auth fails closed; corrupt
history/archive are preserved read-only; unreadable settings never silently reset. One miner
process per data directory. Keep retired credential filenames ignored to protect old installs.
- Optional password-only dashboard auth uses compatible scrypt and SHA-256 token digests,
fixed 30-day sessions, HttpOnly/SameSite=Strict cookies and Secure on HTTPS. Password change
revokes other sessions; disable requires current password. Twitch logout is separate.
- Outer middleware protects HTTP and both Socket.IO transports. Writes require `X-TDM-Request: 1`,
same-origin/Fetch Metadata checks and bounded bodies. Rate-limit hashing by peer and globally.
Recheck socket authorization for events/broadcasts and expire idle sessions. Enabling auth
evicts anonymous sockets before private publication. Detached password/settings writes are owned.
- `PUBLIC_BASE_URL` is one normalized HTTP(S) root origin. Reject credentials, paths, queries,
fragments, lists/wildcards and ambiguous numeric IPv4 forms. It controls browser origin/cookie
scheme, not forwarded client-IP trust. Do not trust proxy headers implicitly.
- Every dashboard response forbids content sniffing. HTML documents carry a content security
policy, the browser counterpart of the desktop one: bundled scripts only, same-origin
connections and form targets, images from the bundle, HTTPS or data URIs, and no objects
or frames in either direction. Extend it deliberately when the frontend needs a new source.
- Serve SPA only on explicit dashboard routes. Preserve API/socket 404s. Public login code/fonts
do not make account data public. HTML revalidates; hashed assets are immutable; private data
is no-store. Dashboard protection recovery removes only web_auth.json while stopped/restricted.
## Dashboard design and contracts
- The browser dashboard is an online PWA. Its root-scoped manifest and four exact PNG icon
routes are public for GET/HEAD with revalidation; API and Socket.IO authorization stays intact.
Generate icons from the existing SVG using `.github/scripts/app-icons.mjs`. Keep a separate
maskable icon. Do not add a service worker, offline cache, queued writes, install prompts,
buttons or installation guidance inside the app. Dashboard password protection remains
optional and off by default, including over HTTPS. Desktop continues to use native IPC.
On browser foreground/BFCache restoration, refresh both account overview and selected-account
sockets, retaining navigation and drafts and fencing stale replies. Respect all four safe-area
insets, including dialogs, popovers and the bottom navigation. The compact header stays
at the top viewport edge and paints its top safe-area inset with the same opaque background;
theme-color hints match it. Retain explicit sticky positioning for WebKit's edge-color
sampling even though only the main content scrolls. Keep its controls below the inset,
campaign details above the header, bottom navigation reachable, and focused page content
clear of both. Below 1024px,
the shell fills the dynamic viewport and only its main content scrolls between the header
and bottom navigation; never draw a root scrollbar over those controls. Lock root overscroll
while the shell is mounted, leaving the login page and desktop document scrolling intact.
Use the shared page scroll element for route resets, account switching, detail restoration
and drag autoscroll. Drag edges follow the visible content bounds outside the navigation.
Available and History list views fit their bordered results to the remaining height, keeping
controls outside the list. Very short viewports may scroll the content to keep controls reachable.
Detail overlays lock that content scroller; their own body and bottom navigation stay usable.
- HTTPS is optional and supplied by a host reverse proxy. `nginx.conf.example` forwards both
Socket.IO transports to the loopback server. PUBLIC_BASE_URL must match the browser's root
origin. The outgoing proxy setting is separate. Wiki instructions cover certificate bootstrap
and renewal; never change a running deployment as part of code validation.
- All displayed times, including tooltips, use the 24-hour `h23` clock (midnight is `00:00`),
preserving the browser's timezone and locale date format. Use the shared date/time formatter;
Activity's time-only display retains seconds. Confirmed progress times show HH:mm today,
add month/day on other days and retain years outside the current year; refresh the label
at local midnight. Tooltips and accessible labels retain the full date/time. Keep API/storage timestamps unchanged.
- Use `frontend/src/assets/twitch-drops-miner-logo.svg` for the app, login, favicon and README.
Preserve its artwork and aspect ratio; Vite emits one hashed asset for browser caching.
Keep adjacent brand text accessible and sidebar navigation reachable in short windows.
- Keep the subtle charcoal/Manrope design, individual `@mdi/js` paths rendered with `@mdi/react`, and shared native controls.
Campaign Games and Filters use separate button-anchored overlays without moving results.
Games retains saved multi-selection and has one scrolling checklist below its search.
History shows Games only. Bound both overlays to the viewport, support older webviews,
and retain keyboard access, outside dismissal and Escape focus return. Keep touch-button
focus inside each overlay so All games and Clear filters work on the first tap.
Both action footers have a divider. Keep Games compact; let Filters fit its contents
within the available viewport, scrolling its options only when space is limited.
Hide selected games defaults false when absent and affects Available display only, using
the same folded game selection as Mine. Remember the filter, preserve History and direct
detail links, and recover focus/pagination when selecting a game removes its cards.
Filters contains Hide selected games and Twitch badges / Twitch emotes / Game items.
All reward types selected means no type restriction, including unknown types; none selected
means no matches. Narrowed type selections match unclaimed, unexpired watch rewards using
core eligibility, independent of mining selection and ignores. Details retain all rewards
and prerequisites. Remove retired status, linkage and Other controls; ignore their old saved
values and query flags. Preserve existing game/type choices without a settings migration.
Do not use middle-dot (U+00B7) separators in authored UI text or documentation,
except the README's Quick start / Dashboard / Updating / Troubleshooting navigation.
Use wording, commas, colons or parentheses instead.
Icon-only actions use borderless transparent buttons with circular hover/keyboard backgrounds,
accessible names and tooltips.
History pagination uses chevrons beside the page count; channel watch/entry, return-to-auto,
Activity follow and error retries use play-circle/plus, refresh-auto, arrow-down and reload icons.
Inset search-clear circles within the field border; game-priority
drag grips stay visible at all times and retain their plain six-dot pattern without pointer
hover backgrounds. Retain native
selects for icon-only sorting, with explicit legible dark select/option colors on light OS themes.
Mining > Edit uses the native icon-only Priority High selector beside Add Game for
Default (manual order), Short events first and Ending soonest. Its tooltip reports the
selected mode; accessible help explains that drag order is preserved for ties. Reuse
autosave/conflict handling and disable the selector until reconnect hydration.
Mining preferences has no inline priority or ignore-name hints. Native info popovers retain
the selected priority mode's full rules, other-game reward rules and ignore/dependency details, with
click/tap/keyboard access and outside-click/Escape dismissal. Keep priority rules associated
with the selector for assistive technology, and help available during reconnects. Use concise
labels: Also mine from other games, Allowed reward types and Ignore rewards by name. Keep
the two reward groups distinct and preserve all existing settings/mining behavior.
Mining preferences also contains the global Automatically claim drops checkbox, using shared
autosave and reconnect handling. Its info popover explains manual claiming on Twitch, claim
expiry, prerequisites, outstanding claims on re-enable and already-started requests.
Progress display has independent percentage, watched/required time and countdown checkboxes
in Mining preferences; any combination, including all off, is valid. Defaults remain time on,
percentage and countdown off; preserve existing saved choices. Use one readout in Now mining
and unfinished campaign details, leaving progress bars and window/tray titles independent.
When time is visible, append the confirmed percentage in parentheses; otherwise show it alone.
Share rounding with the window/tray title: truncate to one decimal place, omit trailing
.0, and never round incomplete progress up to 100%. Omit percentages for unknown progress
or invalid watch requirements; do not interpolate between confirmed updates.
Progress time format is an account display preference in Mining preferences, defaulting
to Minutes. Hours and minutes uses compact durations such as 5h 30m / 6h in Now mining
and campaign details, including unknown-progress zero values. Share formatting across both
wrappers, preserve numeric progress/claims, and exclude it from mining-preference copying.
The optional HH:MM:SS countdown is explicitly estimated and never changes progress or claims.
Use the core's remaining seconds and watch eligibility for the current reward; unknown
progress shows --:--:--. Only confirmed, eligible automatic rewards may tick, never manual,
disputed, completed or prerequisite-blocked rewards. Follow acknowledged settings and worker
state, never an unsaved pause draft. Freeze on pause/disconnect; preserve
the clock across navigation and reset for new accounts, server instances, rewards or progress
corrections. Interpolate at most 59 seconds beyond a core estimate, including background tabs;
subsequent minutes require core updates. Other rewards show static confirmed remaining time.
Keep countdown help in the existing info popover and all display preferences out of copying.
Render strings as React text, validate external links/artwork, expand Twitch image placeholders.
No injected HTML or CDN scripts. Art provides safe missing/broken-image fallbacks.
Reward thumbnails contain their artwork; game covers retain cover sizing. Phone checkbox
labels, game search results and drag grips have at least 44px touch targets without enlarging
glyphs or adding pointer hover backgrounds to grips.
- Sidebar: 32px visible GitHub artwork above an equally sized, aligned Twitch avatar
(compensate for the SVG's internal whitespace), with 4px between their controls, followed by
equipped badges and the chat-colored name, with no divider above the footer. The identity
links to Settings > Twitch account. Settings shows a 48px avatar, colored name, equipped
badges and adjacent logout action, followed by the collection labeled Badges. Badge
buttons have no borders; click/tap/keyboard reveals descriptions. Use the same account
settings page on phones, without a separate profile popup. Omit banner, bio, account date,
followers, roles, social links and the separate account ID row.
Lighten dark chat colors only enough for readable contrast, preserving their hue.
Profile metadata uses bounded, cancellable GraphQL reads owned by the authenticated network
generation, concurrently with mining, using the existing login with no new scopes. Refresh
at generation startup and after successful hourly validation. Preserve same-account metadata
during renewal and clear it on logout/account changes. Never persist it or accept late data
for another account.
Query the full selectable global badge collection separately; permission/schema/network
failures leave it explicitly unavailable while retaining equipped badges. Do not include
channel subscription badges or infer badge ownership from the global artwork catalog.
Authenticated 401/403 and cancellation retain existing session handling. Validate identities,
bounded metadata, image URLs and colors; fetch only identity, color and badge fields.
Connection status lives in Settings > Connection and is labeled Dashboard connected, separate
from Twitch. The Twitch profile external-link stays beside the profile identity; logout
lives in the account row beside rename and remove. Use the validated login from the existing account query for the Twitch URL,
never the display name; omit the link when that login is unavailable. Reuse the activation
link's Open In New icon, shared icon-button style and native external-link handling.
Logged-out authorization status stays visible. Done adds no second waiting notice;
retain its pending/disabled state and request errors. Device authorization keeps the
copyable code, Twitch Activate and Done on one unboxed wrapping row with equal-height controls
(36px desktop, 44px phone). Activate and Done use the same outlined button style. Successful
copying shows a tick, tooltip and accessible status for three seconds; another successful copy
restarts the timer. Restore the copy icon on expiry or failure, and clear timers and stale
clipboard results when the code changes or the component unmounts. Success feedback never shifts the
row or following sections; visible failure feedback retains its spacing below the row.
- Navigation is Mining (`/`), Campaigns, Activity and Settings. Keep the compact charcoal
sidebar and the four-item phone bottom navigation, safe-area spacing and short-window access.
Mining preferences replace the overview from Up next > Edit (`/?edit=priorities`), with a
Back to Mining icon action. Only the game list scrolls on desktop; search/priority controls
and the settings beside it stay fixed. In short windows the settings column scrolls independently
to keep every field reachable, without scrolling the whole preferences panel or page.
When error/reconnect notices leave too little height, allow the games column to scroll as
well; controls and selected games must remain reachable after dismissing search.
Game search uses an input-anchored native popover over the selected games, without moving
or resetting the list. Cap results at 320px and the available visual viewport; open above
when there is insufficient room below. Keep input focus with listbox/combobox semantics,
arrow-key navigation and Enter selection. Selection, Escape, outside click and leaving the
picker dismiss results; Escape preserves the query. Preserve manual-name entry and retry.
Webviews without Popover API use the same fixed-position dropdown with outside-click dismissal.
Search Twitch's Helix category directory through the current Smart TV session with no
added scopes. Use debounced, cancellable queries and a bounded generation-owned queue;
keep search separate from campaign discovery and watch scheduling. Pending searches stay
quiet without a loading row or empty results box. Authentication failures
retain session-owner handling; optional lookup failures leave manual-name entry available.
Save selected game IDs, names and validated Twitch cover URLs in compatible `game_metadata`
settings; this metadata never changes mining eligibility. Remove metadata for deselected
games. Resolve missing metadata on opening preferences and when selected names change,
retaining order and manual names. Match saved artwork with the shared Unicode game keys.
Search results and selected games show covers without requiring an active campaign; late
responses cannot replace newer queries or write metadata after logout/unmount.
`/settings#mining` redirects there without losing the selected account. Settings includes a
Mining preferences shortcut; its back button returns to Settings and restores focus.
Desktop keeps section tabs. Below 1024px, Settings shows a full-width icon/chevron list;
opening a section replaces it with a single heading and the shared icon-only back button.
Deep links and resizing preserve the section; account switching preserves the current page.
Use distinct link and content IDs so hash changes do not scroll the header out of view.
Returning to the mobile list restores the previous link's focus. Keep 24px below the mobile
header (32px below desktop tabs), 16px between form fields/actions and 4px between related
checkboxes, with existing touch targets. Preserve current account controls and shared
connection drafts; Telegram retains its explicit clear-on-leaving-Notifications policy.
Dashboard access keeps protection status visible and puts its explanation in the shared
info popover. Hide inactive form wrappers without adding trailing separators.
Game search offers Add "name" manually inside empty/failed search results, after loading,
instead of a separate plus button. Keep exact-name/Enter resolution and confirmation before
saving unknown names; channel search retains its own explicit submit control.
- Pause is a saved `mining_paused` setting, default false. It gates automatic and manual
watch events and progress polling, cancels an in-flight watch and fences late results.
Inventory refreshes and enabled automatic claims continue. Preserve selected games and the current
reward; manual timers still expire. The Now mining header becomes Paused with a resume
action beside the selection mode. Settings save failures must not pretend mining stopped.
A saved resume request keeps the card and title paused, with the control busy, until the
worker publishes its resumed state. Never combine resumed settings with a paused subtitle.
Browser titles show confirmed percentage and game, or Paused/Idle/Disconnected, separated
from Drops Miner by a hyphen. Never use estimates or invent unknown progress.
- Mining: watching information only in Now mining, no status subtitle or Recent activity. Channels
and Up next have equal desktop dimensions and internal scrolling; stack on narrow screens and
preserve access on short windows. Show confirmed values without redundant labels
or the Watching for this reward caption. Put the current reward's Last confirmed timestamp
inside Now mining beside the selection mode, in smaller, darker text with readable contrast.
Omit it when confirmation is unknown; let the card header wrap on phones with an 8px row gap.
Now mining uses 24px vertical/20px horizontal desktop padding and 16px phone padding.
Header-to-reward and reward-to-progress gaps are 20px desktop/16px phone, with an 88px
contained reward thumbnail centered beside its text with a 16px desktop/12px phone gap.
Now mining omits streamer avatars, including manual watching and offline waiting. Retain
Watching text, with only the streamer name linking to their Twitch channel using the login.
Keep the 6px progress track and 8px count gap. Group the manual selection label, return-to-auto action and timer
in the Now mining header, including pending lookups, preserving phone hit targets.
Do not add a separate manual-controls footer below reward progress. Its empty state adds 20px vertical padding.
Known rewards without confirmed progress show an empty progress bar and `0 / required minutes`
in Mining and Campaigns,
with an unconfirmed tooltip and accessible description. Suppress the redundant awaiting-progress
subtitle when a reward is shown. Keep unknown evidence unchanged; never display estimates as
confirmed minutes or invent a confirmation timestamp or a duration for an unknown reward.
Bordered lists keep their scrollbar inside a stationary frame. Mining preferences Games
and Campaigns/History list view put the border on the scroll container, not the moving rows.
Cap these lists at 440px on smaller screens; retain their remaining-height desktop layout.
Campaign grid cards keep their existing layout. Channels, Up next and game-priority rows
retain inset separators and modest 4px scrollbar padding. Up next groups collapse independently.
Compact game headers show artwork, name, reward count and a chevron; keep saved order and
nearest deadline in the header tooltip and accessible description. Use the shared circular
icon-button hover/focus style on the chevron, with the whole heading remaining actionable.
Pin each game's header
within its own group while scrolling, with bounded name wrapping and scroll padding for keyboard
focus. Use a responsive reward grid without nested cards, retaining one column on phones.
Keep priority explanations in the campaign link tooltip and full campaign details, without
repeating the deadline below it. Expand the first game initially and whenever a different game
becomes first; retain manual choices while it stays first and across campaign navigation.
Keep this UI state in memory, keyed by game identity and scoped to the account and runtime
instance, pruning removed games. Do not change saved priorities or move keyboard focus.
Show a shared date once beside the campaign name when space permits, including a single reward;
wrap on narrow screens. Share dates only when reward windows match by instant and their
upcoming state agrees; retain distinct per-drop dates.
Channel views may fill missing game artwork from catalog records with the exact same game
ID. Never change stream identity/eligibility or add network requests for this fallback.
Channels paginates 25 rows with header chevrons. Search covers the full list and resets the page;
page links survive navigation, clamp to available results and keep the watched stream first.
Pagination does not change mining selection or fetch unrelated streams.
The Channels plus action opens one anchored picker over the list without changing row positions
or scroll. Search results and an optional return-to-auto timer share that picker; do not nest
another results popup. Focus search on open, support arrow/Enter selection, Escape/outside/Tab
dismissal, preserve drafts and restore trigger focus after selection or Escape. Bound the
picker to the visual viewport, open above when needed, and support older webviews without
the Popover API. Search only while open and cancel/fence late replies on close.
Close after an accepted selection; failed requests retain the picker and edits. Keep later
lookup errors visible in Channels when closed, and beside the input when reopened.
Preserve error-row space while the picker is open. Picker selection, row Watch and return-to-auto
share HTTP busy/error state; accepted pending lookups remain cancellable with return-to-auto.
Manual lookup has no preparing message; use a play-circle icon submit button with the Mine
accessible name/tooltip, preserving its busy/disabled state, inline
errors and return-to-auto control while a lookup is pending.
Channel name/URL and optional timer use accessible input placeholders. Settings autosave
has no saving/saved notices; preserve errors, edits and Retry.
- Mining and Campaigns share inventory refresh feedback inside the button; Maintenance has no
inventory refresh action. Track queued
and running work through publication, coalesce requests, preserve state on reconnect and
ignore stale completion events. An acknowledgement is not completion. Keep failed inventory
requests retryable without clearing previous results or adding notices. A successful inventory
refresh with partial catalog coverage retains known campaigns and reports success without an error.
Use the shared fixed-size icon button: refresh/spinner, brief success tick, or retryable error.
Keep its position and size stable across states, respect reduced motion, and preserve accessible
state labels/live feedback, accessible request errors and the catalog source/update-time tooltip.
- History lives in Campaigns in place of Finished; the old /history route redirects. Display
recorded claims grouped by campaign (25 per page), independent of completion/catalog coverage.
Share search, game filters, sorting and list/grid controls. History defaults to newest recorded
claim; loading stays quiet without a Loading history caption, while errors retain Retry.
History detail panels render recorded claims only: saved campaign/game identity, rewards,
additional benefit names, optional artwork and claim/first-observed timestamps. Live metadata
may supply artwork but must never add account linkage, eligibility, channels, progress,
prerequisites, earning dates or unclaimed rewards. Deep links resolve against recorded claims;
missing records and request failures stay explicit, with Retry available inside the panel.
Most Drops counts recorded claims, campaign date sorts place unknown dates last. No
CSV/JSON export, Since filter or separate history clear action. Clear all cache clears history
and publishes the durable change to open dashboards; archives never recreate cleared rows.
- History artwork is optional; retain old rows and use matching live benefits as display fallback.
Telegram credentials never appear in responses; no server-dashboard installer/updater.
- Campaign details contain rewards and account/campaign links, with no streamer lists or status
lookups. Streamer browsing and manual Watch actions belong in Mining's existing Channels panel.
Omit per-reward progress confirmation timestamps. Rewards with the backend's watch-complete
status show that status without a progress bar or minute count; keep unfinished/unknown
progress and actual claim timestamps. Watch completion never implies a claim.
Omit the redundant Ready to mine subtitle; eligible rewards can already have concurrent
confirmed progress. Show Mining only for the active eligible reward, matching both
campaign and reward IDs from mining status. Require a connected, logged-in, unpaused watch
state; retained progress, idle/offline states and History must never imply active mining.
Group game-account status and its Connect/Check action in one wrapping row. Keep the
external Twitch campaign link in the detail header, separate from account linking.
Place the concise priority reason beside the campaign dates without repeating its deadline;
retain that deadline in the tooltip and omit the redundant list of priority-target rewards.
- Campaigns starts with Settings-style Available/History icon tabs and the campaign count
immediately left of refresh on the right. Show matching/total counts when filtering either tab;
otherwise show the total. Match Last confirmed's
12px/#888888 text. Keep the page heading screen-reader-only. Clear filters stays inside Filters;
All games stays inside Games. On phones put pagination to the right of the tabs, then a full-width search
row, followed by filter/sort/view controls and the count/refresh pair, wrapping when needed.
Below 375px omit the decorative tab icons to make room; retain both text labels.
Native icon-styled sorting offers Default, Newest (campaign start descending),
Ending Soonest (end ascending), Most Drops (total descending), and A-Z (campaign name).
Default retains progress-first ordering; ties use that same deterministic order. Sort is
URL state preserved by searches, filter resets and tab changes, never a mining setting.
Available excludes expired and completed campaigns from results, counts and game choices;
retain selected game filters so they can be cleared. There is no Expired filter, and legacy
saved/query flags cannot restore expired results. Keep backend records and pending claims
intact, and retain confirmed claims in History. Summary dates omit the year and use Starts/Ends
without a colon; campaign details retain full dates. Show claimed/total counts without an
Active status, with an accessible claimed-reward description and tooltip.
- Campaign summaries open one detail panel, alongside the list on wide screens and as a full
page on smaller screens. Lock page scrolling while the full-page detail is open and contain
its body scrolling; release the lock on close, navigation or return to the desktop layout.
At desktop widths tabs, search and filters stay full width above the list and detail columns.
Split those columns equally with a 12px layout gap. Grid results retain their outer
scrollbar gutter and 1px card clearance; list results keep the gutter inside the border.
Both columns fill the remaining height with a compact 12px bottom margin; details keep the
same height for short and long campaigns, with a fixed header and separately scrollable body.
Scroll campaign results independently with a small scrollbar gutter; allow the controls area
to scroll in short windows and the workspace to scroll when notices or long titles leave too
little room for usable controls and detail content. Omit selection stripes and
confine row hover to the side-panel icon circle; preserve a visible title/icon keyboard focus cue.
Use the Dock Right icon for opening details, with a tooltip, current-item state and panel
control association rather than disclosure semantics.
Mine icon hover stays separate, without a selected background strip behind it. Apply the
same shared summary component and collection layout for Available and History: 48px artwork,
matching text spacing/count typography and card borders, with inset separators between list rows.
Stretch grid cards to equal row heights without clipping names. Use each summary's width
to place counts and actions below the identity in narrow desktop cards/rows, with footer
actions aligned at the bottom. On phones, Available keeps a compact claimed/total count
above the side-by-side Details/Mine icons beside the identity, without a separate footer.
Center the cover beside the title, game and date; allow long names to wrap and preserve
44px phone action targets. Closing details restores the opening title or icon's focus.
Keep History's recorded claims/date and Available's live status/mining action distinct.
Preserve search, filters, layout, sorting, page, scroll and trigger
focus when closing. Keep the selected campaign visible when opening narrows the results or
changes their layout. Restore offsets only for the matching results width and layout;
otherwise reveal the selected item instead of depending on browser scroll anchoring. Switching
details replaces the current detail history entry. Campaign/drop
IDs form deep links; missing IDs and unknown account progress/linkage remain explicit.
Links from Mining and Activity return to their originating route, scroll and trigger focus
on Close/Back. Preserve their search/filter URL state and the return state through in-place
Campaigns query edits; restore matching-width offsets and reveal the trigger after resizing.
Activity uses the same Dock Right detail icon. Reward deep links retain
scrolling and current-item semantics without a selection stripe or indentation.
Show the campaign date range once; only show per-drop dates when the effective window differs
from the campaign, comparing instants rather than timestamp strings. Keep claim timestamps.
Available details have no appended History section: show claim/first-observed times inline
for account-confirmed claimed rewards only. Never let old history mark a live unclaimed
reward as claimed. Omit benefit names identical to the reward title, retaining other names.
List filters/layout are shareable URL state; loading a shared URL does not autosave it.
Available and History paginate 25 campaign groups, with pagination centered across the
top toolbar on desktop and beside the tabs on phones, wrapping above search when needed.
Results fill the remaining height without a pagination
footer. Preserve full-size pagination hit targets. Mine remains a game-wide action.
Eligibility, normalized game identity, saved ranks and priority reasons come from shared
backend policy; prerequisite boosts identify their actual target rewards.
- Activity is a bounded session buffer of typed events with category, severity, safe message
arguments, entity references, first/last time, repeats and explicit recovery. A matching
successful operation recovers its failures; unrelated messages never imply recovery.
Keep category filtering, but omit the This session heading suffix and per-event category tags.
Inset row separators to align with the content instead of touching the panel edges.
Center Activity row contents and reserve the same minimum height with or without details
actions; wrapped messages and recovery text may grow. Retain full-size phone action targets.
Fit the list to the remaining viewport on desktop and phone; keep notices and controls
reachable in short windows. Distinguish an empty event buffer from no filter matches.
Count adjacent repeats in Activity while suppressing duplicate server log lines.
- Maintenance checks the latest stable release's `latest.json`, compares SemVer precedence
without build metadata, and distinguishes failure from up-to-date status. Keep requests
bounded/coalesced and release links within this repository. No install/download execution.
Desktop and server share a compact installed-version badge (`vVERSION`) beside the update
check and inline result, with actions or manual-update guidance below. The badge always
uses the muted accent; only the adjoining status reports check results. Errors never
appear up to date.
Desktop and server use the same checking, current, available and check-failure wording.
Check for updates uses the fixed-size MDI Update icon button, with a checking label/tooltip,
busy state and spinning icon during requests (respect reduced motion); let inline results wrap
below the controls when space is limited.
- Shared Field content starts at the top; helper text cannot stretch neighboring label rows.
No focus rings, but visible keyboard background/border changes must outrank utility layers;
keep system focus in forced colors. Verify computed field/button/checkbox focus and axe checks.
- The application owns mining state independently of HTTP/socket transport. Publish related
fields atomically; clone snapshots before emitting and never await network/disk under a view lock.
Protocol 2 snapshots/patches carry process identity and monotonic revisions; per-subscriber
patches use the last delivered revision and coalesce slow consumers. Retain the legacy event
adapter for old tabs and recheck authorization for every emission. A gap requests a full resync;
disconnects/process changes disable commands until hydration. Viewer-only patches retain
frontend campaign identity. Incompatible clients can reload with a temporary unsaved draft.
- Every durable History insertion and clear publishes a history revision under the history
lock. HTTP responses include process/revision/clear identity; stale responses cannot restore
rows after a clear. Runtime DTO additions must not change version-one archive/journal JSON.
- One typed provider hydrates complete snapshots and incremental events. Commands stay disabled
until reconnect hydration. Editable settings drafts are separate from live data; serialize
autosaves with original revisions. HTTP409 preserves edits for Retry and only changes touched fields,
including individual nested filter/benefit fields. Draft errors remain reachable across pages.
## Validation and release
Use the complete commands in CONTRIBUTING. Rust tests use temporary files and mock transports;
Playwright starts its own loopback:8765 fixture, verifies readiness/reset, and refuses reuse.
Never use live credentials, a live miner or real notifications for automated checks.
CI requires Rust fmt/Clippy/tests, frontend format/type/build/unit/browser/axe, automation
contracts, version/lock agreement, and amd64/arm64 production image builds plus isolated health.
PRs limited to README.md, CONTRIBUTING.md and AGENTS.md may skip code/image jobs. Release
PRs may also skip them when only Cargo.toml, Cargo.lock and CHANGELOG.md change: verify
exactly the shared version and all three workspace lockfile versions increase together,
with canonical SemVer and valid release notes. Dependencies, other manifest content,
file modes/additions/deletions and all other paths require full validation. Scope uses
committed merge/base blobs; scope/whitespace checks and the final Validation gate still run.
All main pushes and manual
runs require the full baseline. Cache Rust dependencies, never workspace binaries or test
results; a cache hit never replaces running the checks. CI installs only Chromium's headless shell.
Build one fixture binary/dashboard bundle for both CI browser shards; each shard owns a
separate runner/server and one worker. Preserve readiness/reset checks and refused server
reuse. The Validation gate requires both shards; local Playwright still starts Cargo itself.
Use focused local checks and the final revision's CI baseline without repeating the whole
suite locally. Image builds run alongside tests. Main retains the exact tested OCI archives
for seven days; publishing downloads them from the latest successful exact-main validation,
checks both platforms/version/revision before registry writes, and copies them by digest.
Failed or pending newer validation cannot fall back to an older run. Missing/expired artifacts
require rerunning validation; release and edge publishing never rebuild images. PR artifacts
cannot be promoted. Ordinary merges only retain artifacts, never publish registry images.
Keep the project's PolyForm Noncommercial 1.0.0 license in `LICENSE.md` and the full upstream MIT
license in `NOTICE.md`; third-party components retain their original licenses;
include both with frontend asset notices in production images. Preserve 1000:1000 ownership,
mounts and port. Health does not prove earning.
Cargo manifest/lock own the version. Prepare release opens a draft PR; a maintainer can use
the same local release script and draft-PR process if its token is unavailable. Publish is manual from
validated main and uses reviewed CHANGELOG notes/GHCR. Every release attaches and verifies
`latest.json` before publishing; stable releases alone move the latest pointer. Use scoped
conventional commit messages and concise release change lists, with relevant PR links in parentheses.
The release script owns the single `Changelog:` link and issue footer; do not duplicate them
in CHANGELOG entries. Accept exact-commit push or manual validation.
Publish first to GHCR at ghcr.io/ohne-b/twitch-drops-miner using the scoped workflow token.
Then mirror the published image to Docker Hub through the shared main-only `prod` workflow.
Use the configured repository/username variables and token secret; never expose credentials
to PR jobs. Copy all platforms by immutable source digest, preserve and verify that digest,
and never rebuild or recreate releases for mirroring. Only the current stable release matching
GHCR's latest digest may advance Docker Hub's latest; older releases and prereleases cannot.
Failed mirrors leave GHCR intact and can be retried through the manual mirror action.
Advance latest only after the stable release and its manifest are public. Preserve
published old-name images and document promotion recovery and first-package visibility.
Keep Buildx action pins identical between validation and publishing. README keeps its
centered title/tagline/license opener, screenshot, download/wiki links and project scope.
Write README and wiki guides in plain, concise language. Omit exhaustive control/status
lists, filler and repeated explanations of UI labels. Keep setup, recovery and privacy
guidance in the wiki, without an Updating and rollback page. Put release changes in
CHANGELOG and technical contracts in this harness.
Keep upstream attribution in License and credits; do not add a contributor/PR table or automation that
rewrites README after merges. No ordinary code merge may publish a release or bypass
independent review/checks.
Manual Publish edge image may publish only `:edge` from exact validated current main, using
the scoped workflow token and the same image platforms/action pins, then mirror it to Docker Hub.
It creates no GitHub
release, version tag or version bump and never changes `:latest`; the OCI revision identifies
the build. Ordinary merges do not publish edge images either.
For home-server work, inspect the current checkout/Compose/image before assumptions. Ask before
changing the running deployment. Build while it runs, back up data and Compose before replacement,
retain rollback, and require interactive sudo in the user's terminal. Always provide the actual
PowerShell update command in the handoff; never ask for a sudo password in chat.