Skip to content

Dashboard PoC implementation plan

  • Status: Five-card evaluation deployed and rotating on XT1058; broader appliance acceptance, live-game freshness, and multi-hour soak remain open
  • Branch: feat/dashboard-poc
  • Source verification: 2 October 2026
  • Stack: Java/platform Views, Android API 22+, ASP.NET Core Minimal API, .NET 10 / C# 14, Linux container on the existing OVH VPS

The original four-source design was the first deployed PoC. On 2 October 2026, a quota-aware API-Sports Baseball adapter and a BALLDONTLIE MLB adapter replaced the old MLB Stats card, whose OVH edge returned 406. Both adapters are now live as distinct cards in the five-source evaluation. The phone contract did not change; see Dashboard and the current deployment receipt.

The subsequent evaluation configuration adds BALLDONTLIE MLB beside API-Sports as a second Yankees card, with 15 seconds per card and a 75-second bulletin cap for five cards. This is a temporary comparison, not a fallback policy. Team IDs are provider-specific (25 and 19); the Android schema is unchanged. The earlier frozen-candidate receipt below remains historical evidence for the 1 October image, not acceptance of this later image. See the current deployment receipt. The Android bulletin-cap correction in 9be243c lets the phone honor the 75-second value; continuous Dashboard rotation already showed all five cards.

The direction

Keep the clock an appliance, not a notification center. One fixed bedroom profile supplies Bogotá weather, Yankees information, and two small headline cards. No account system, browser, private calendar integration, or admin website is needed. Backend configuration selects sources; simple on-device controls select how often to see them.

Choice Behavior Network behavior while foregrounded
Just Clock (default) Existing themes, sounds, schedules, and preview behavior remain intact No dashboard polling or image downloads
Clock + bulletins At :00 and :30, show available cards once, then return to the clock Conditional refresh every five minutes
Dashboard Cycle available cards until the user switches back Conditional refresh every five minutes
Highlights now (action) Show one bulletin, then return to the previous persistent mode One refresh; cached cards can appear immediately

The configured bedroom profile uses 08:00–20:00 in America/Bogota, 15 seconds per card, and no more than 75 seconds per bulletin. Cards follow Weather → API-Sports Yankees → BALLDONTLIE Yankees → NYT → Guardian order. These settings are server-configurable, so future card-count or timing changes do not require a new APK. Quiet hours suppress automatic bulletins and dashboard transition sounds; manual viewing remains possible. Reuse existing quiet-hours settings rather than introduce a second competing schedule. PoC cards are silent.

A tap reveals a small mode chooser; controls disappear again. No permanent toolbar, news ticker, login screen, or settings hierarchy on the clock canvas. “Highlights now” is allowed during Just Clock but must not enable ongoing polling. Exiting a one-shot restores the previous mode and its network policy.

Current implementation status

The current implementation includes these behaviors:

  • Dashboard updates in continuous mode are adopted at a lap boundary. A single-card feed counts each completed display interval as a lap.
  • A card is removed from view when its validUntil is reached, even if its normal display interval has not ended. A decoded image is checked again before an asynchronous result can be attached to the view.
  • Refresh work is revoked on pause, destroy, explicit mode changes, connection changes, and when one-shot polling ends. Outcomes and image decodes from an obsolete presenter request are discarded.
  • Manifest and asset 401/403 responses both clear the cached feed and report an authentication failure.
  • RSS clients do not follow redirects. Redirect responses are treated as an unavailable source, so a configured feed cannot redirect the backend to a different host.

Fresh validation on 1 October 2026 passed 846 backend tests and 831 Android JVM tests. All 31 instrumented tests passed on XT1058/Android 5.1/API 22 (T01130J4O9), with no failures or skips. Formatting checks and builds passed; .NET analysis reported zero warnings and errors, while Android lint reported zero errors and seven existing warnings. Deployment-helper regressions and Compose validation also passed. The debug APK SHA-256 was 8804e70c111891e46baf2079d7ef1faaa986481bf3189a191a5570737283ea2a.

The device suite includes 13 acceptance cases: six lifecycle/background, six mode/UI and one bitmap case. It fixed a transient 17×15 split-flap layout crash, immediate Showcase ownership and the idle bitmap pool bound. RGB_565 reuse was verified for opaque backend-style PNGs; transparent PNGs may decode as ARGB_8888 instead. The suite is automated acceptance; subsequent Step-6 manual force-stop, physical Wi-Fi loss/recovery and production-feed checks are recorded in the deployment receipt. See the procedure and results in testing and quality. Production now runs the frozen provider image; three sources pass public/device acceptance while MLB's HTTP 406 on OVH remains unresolved.

Full picture-frame mode, Shower Radio, stocks, exchange rates, emergency interruptions, meetings, OAuth/SAML, and account enrollment are later work. Radio should eventually be independent of the visual mode, but this PoC does not launch VLC, pair Bluetooth devices, or implement media streaming.

Provider access: what is actually verified

Source Read-only check Implementation consequence
Open-Meteo Bogotá request returned current conditions and 48 hourly entries Ready for weather integration
NYT RSS HTTP 200; 26 items in the checked response Ready for headline integration
Guardian RSS HTTP 200; 139 items in the checked response Ready for headline integration
MLB Stats API Unauthenticated team lookup confirmed Yankees ID 147; schedule with linescore hydration returned two games with status, start time, and available score/inning fields Implemented adapter; fixtures cover live-state mapping and provider errors

These were initial read-only provider probes. All four adapters and the provider-to-card pipeline are now implemented locally. A local API smoke check returned anonymous 401, authenticated 200, and cards from all four sources. The separate frozen-image rehearsal passed; see the candidate receipt. MySportsFeeds is deferred: the earlier credential probe returned HTTP 403, and the user does not want a paid subscription now. The cause of that response was not established, and no credential is retained in this document. Do not implement its adapter, troubleshoot its subscription, or spend money on it as part of this PoC. It remains one possible future source among many providers the user has used; there is no preferred future provider or automatic fallback.

MLB access required no key in the checked requests, but public access is not an unrestricted redistribution license or a service-level guarantee. Its payload links to MLB's usage notice, which permits individual, noncommercial, non-bulk use. Keep the personal PoC feed private and do not assume attribution alone authorizes public or commercial redistribution. Reassess applicable terms before wider use.

Architecture

flowchart LR
    W[Open-Meteo] --> A[Typed provider adapters]
    S[MLB Stats API] --> A
    N[NYT and Guardian RSS] --> A
    A --> C[Bounded normalized snapshots]
    C --> R[Card selection and PNG rendering]
    R --> M[Stable versioned manifest and immutable assets]
    M --> D[Foreground Android downloader]
    D --> K[Validated atomic disk cache]
    K --> V[Native image card view]
    T[Local mode and schedule] --> V
    T --> Q[Existing clock views]

The server handles provider credentials, XML/JSON normalization, text layout, and graphics. The phone handles local time, mode, scheduling, downloads, and bounded image display. No JavaScript, WebView, provider SDKs, or provider authentication on the old phone.

Provider refreshes are shared by the profile, not repeated for each phone request. A background service refreshes snapshots independently with bounded concurrency. One source failing must not take down other cards or API health. The API serves an already prepared snapshot, never a synchronous fan-out to all providers for each request.

Provider-independent linkage

The PoC builds the end-to-end pipeline, not a Yankees-specific phone feature. Each configured source has a stable source ID, category, provider type, and validated provider-specific settings. Adapters return normalized category models (weather, sports, headlines); selection and rendering do not consume raw upstream DTOs. The Android contract contains display cards, not MLB fields.

Make adapter registration/configuration explicit and replaceable using normal dependency injection, without a dynamic plugin framework. Future profiles may select tens of feeds per category; independent refresh policies and a bounded selection/rotation batch prevent that source count from becoming unbounded phone downloads or server fan-out. The eight-slide delivery limit is a device safety bound, not a maximum number of configured sources. Implement only the sources needed now, while testing these seams with fixture/fake adapters.

Minimal source projections

Bogotá weather

Use this small Open-Meteo forecast request:

GET https://api.open-meteo.com/v1/forecast
  ?latitude=4.6097&longitude=-74.0817
  &current=temperature_2m,weather_code
  &hourly=temperature_2m,precipitation_probability,rain
  &timezone=America%2FBogota&forecast_days=2

Display current modeled temperature in °C, a short condition label/icon from the weather code, and a compact next-six-hours rain summary. If hourly probability is summarized as a maximum, label it “highest hourly rain chance”; do not call it the probability of rain across the whole six-hour period. Rain amount in mm is optional; no pressure, historical series, or full charts. This is a forecast, not a measurement of bedroom temperature.

Validate parallel hourly arrays, units, missing values, and timestamps. Do not shift array indexes by guessing UTC offsets. Fetch every 15 minutes; a failed refresh may retain the previous weather card for up to two hours after its last successful fetch, visibly marked with its update time.

Include Open-Meteo attribution and document the reduced/derived presentation and CC BY 4.0 source license. This personal, noncommercial PoC fits the free service use described by its terms; revisit terms and quotas before broader distribution.

Yankees

Start with team, opponent, scheduled start in Bogotá time, and game status. Add score and inning from the hydrated linescore when the game is in progress or final. Do not show pre-game placeholder zeros or inning fields as live action. No play-by-play, player statistics, video, or logo licensing work; use team abbreviations.

Use the verified no-key schedule endpoint server-side:

GET https://statsapi.mlb.com/api/v1/schedule
    ?sportId=1&teamId=147
    &startDate={yyyy-MM-dd}&endDate={yyyy-MM-dd}&hydrate=linescore

The no-key team lookup is GET https://statsapi.mlb.com/api/v1/teams/147. Use schedule game IDs, UTC gameDate, structured status, home/away teams, nullable scores, and hydrated inning/half. Convert start times to Bogotá for display. Use a rolling date window instead of a hardcoded season or date; account for schedule date conventions and include adjacent dates where needed for overnight games. Handle postseason and doubleheaders explicitly. An empty successful result is not the same as a failed request.

Selection: live game first, next scheduled game, then a recent final within 24 hours. Preserve postponed/canceled states and null scores. Never convert missing scores into a fictitious 0–0. Label successful no-game results honestly.

Implemented polling: 15 minutes outside games, two minutes during a live game, with shared caching, conservative request volume, and backoff on denial or rate limiting; no published quota or uptime guarantee is assumed. This is a periodically updated scoreboard, not a real-time service. Hide live scores after five minutes without a successful refresh; scheduled/final information can have a longer explicit validity window. Stop rapid retries on 401/403 and report a sanitized operator diagnostic. Omit the unavailable card from the phone's rotation.

NYT and Guardian headlines

Fetch the supplied NYT homepage RSS and Guardian UK RSS every 15 minutes, honoring upstream ETags/Last-Modified where provided. Select up to five recent items per source; display at most three titles on each source's single card. Sort parseable publication dates newest first, with feed order as a stable fallback. This is “headlines,” not a verified breaking-news alert service.

Keep title, source, publication time, and canonical article link. Strip HTML, bound title length and line wrapping, and omit summaries, tracking images, full articles, advertisements, and embedded media. Include publisher attribution. Preserve links in metadata for a future detail action; a bedside bulletin does not need to open a browser. Remove the card after six hours without a successful source refresh.

Use a bounded XML reader: prohibit DTDs, disable external resolution, cap response bytes at 1 MiB and parsed items at 100. Configure fixed upstream URLs; this PoC is not an arbitrary RSS URL proxy.

API contract and asset delivery

Keep the existing GET /api/v1/dashboard/ route and schema-1 fields (schemaVersion, generatedAt, refreshAfterSeconds, slides, imageUrl, durationSeconds, accessibleDescription). Add optional fields rather than silently break an installed client:

Addition Purpose
profileId, revision One fixed profile, stable content/policy revision
presentation Bulletin minutes, window, timezone, maximum total duration
Slide id, kind Stable identity and weather/sports/headlines classification
Slide updatedAt, validUntil Visible age and enforceable expiry
Slide sha256, width, height, byteLength Validate immutable image content before use
Slide sourceName, sourceUrl Attribution and future detail navigation

The local selected mode always wins. A manifest may supply bulletin defaults but may not turn Just Clock into dashboard mode. Unknown optional fields are ignored; unsupported schema versions are rejected without losing the cache.

Return a stable ETag derived from the complete presentation snapshot, including policy, expiry metadata, and asset hashes. Do not regenerate timestamps on every GET and defeat conditional requests. A 304 preserves the cached expiry; it is not evidence that a provider refreshed. Build a new snapshot when a provider refresh changes its validity metadata, even if the visible image did not change. Immutable image paths include a content hash and support reuse.

One manually provisioned, revocable, read-only device token is sufficient; there is no user-account or enrollment UI. Protect both manifest and image endpoints, scope the token to bedroom, and avoid credentials in URLs. Store configuration in app-private storage; inject provider secrets into server runtime configuration, not the image. Do not confuse Compose's interpolation env file with actually supplying environment variables to the container.

Restrict downloads to the configured HTTPS origin and expected asset paths, reject cross-origin redirects, and validate chain/hostname/dates. Implement the scoped ISRG Root X1 + YR compatibility approach in ADR 0002; never use an accept-all trust manager. Modern Android should use its normal system trust when available. Test actual API and asset connections on API 22 before calling transport complete.

Rendering, caching, and device lifecycle

Server renders static 1280×720 PNG cards: black/dark background, amber accent, large text, generous margins, subtle split-flap-inspired headings. Do not rebuild animated Nixie tubes into every information card. Transition directly between cards; leave the existing clock animation untouched.

SkiaSharp 4.153.1 and matching NoDependencies Linux native assets are pinned; the image targets musl and bundles licensed IBM Plex Sans fonts. No headless browser or System.Drawing Linux workaround is used. The full-image rehearsal must verify the actual provider cards, including Bogotá, degree signs, publisher text, and bounded text layout; the earlier rendering spike is not its substitute.

Initial device bounds: manifest ≤64 KiB; ≤8 slides; image ≤2 MiB compressed; exact expected dimensions; ≤32 MiB disk cache including staging; one refresh worker; at most two decoded full-sized images. One ARGB image costs \(1280\times720\times4 = 3{,}686{,}400\) bytes, about 3.52 MiB, so two decoded images cost about 7.03 MiB before other UI resources. Measure the combined clock/image peak, not just this arithmetic.

Download into a staging generation, check lengths/hashes/dimensions, then publish it atomically. Failed or interrupted refreshes leave the previous generation intact. Clean abandoned staging and old generations within the bound; snapshots currently on screen cannot lose their files mid-cycle. Validate dimensions before full decode, bound decoded memory, and use native image fitting rather than stretching.

All requests, file operations, and decoding run off the main thread. A new manifest is adopted between bulletins, not halfway through one. Check expiry again before displaying each card. Skip unavailable/expired cards; if none remain, show the clock. Keep error details off the bedside screen.

Foreground/resumed only: pause polling, transitions, and dashboard callbacks when the Activity pauses; cancel obsolete requests and ignore late results. Just Clock cancels dashboard work immediately. No foreground service, alarm, wake lock, background refresh, boot receiver, or auto-wake is added here. The selected mode persists, but it takes effect only when the app is opened.

Use local wall time for schedule boundaries and monotonic time for card durations. Deduplicate bulletin slots, skip missed slots after resume, and avoid duplicate runs when clocks/timezones change. Manual viewing near a slot must not immediately produce a second bulletin. No “catch up” marathon. The existing keep-screen-on behavior only keeps a visible Activity awake.

Also address the documented Android 10 navigation inset using actual window insets/usable bounds, not a hardcoded 144-pixel shift or OS-version guess. Regression-test both old Moto X and Moto X Play geometry. Continue AMOLED brightness/burn-in mitigation and test accessibility descriptions.

Implementation decisions (30 September 2026)

These decisions refine the plan above after a pre-implementation review. They record facts checked against the live system and the chosen simplifications. Update them as implementation proves or changes them.

Findings that change the design

The production certificate chain already uses Let's Encrypt's new roots

A check of retroapi.everybodypackit.com found the chain leaf → YR2 → ISRG Root YR, with Root YR cross-signed by ISRG Root X1. Bundling only X1 works today, but only while Let's Encrypt keeps serving that cross-signature. The app therefore bundles both ISRG Root X1 and ISRG Root YR as additional trust anchors for the configured origin, each verified against the fingerprints published at letsencrypt.org/certificates. The server negotiates TLS 1.2 with suites Android 5.1 supports (ECDHE-RSA-AES128-GCM-SHA256 was confirmed).

  • Time zones in the container. The aspnet:10.0-alpine image runs .NET in globalization-invariant mode and has neither ICU nor tzdata, so looking up America/Bogota fails. The final image adds the tzdata package (about 1.5 MB). tzdata supplies IANA time-zone rules only; it does not provide ICU. Cards use fixed, culture-invariant formats, so invariant mode and its correct UTF-8 handling of text such as "Bogotá" are kept. Culture-specific text, such as Spanish weekday names, would additionally require icu-libs and disabling invariant mode, at a much larger image size.
  • SkiaSharp. The latest stable release checked was 4.153.1. Its SkiaSharp.NativeAssets.Linux.NoDependencies package includes a linux-musl-x64 native library, so rendering can stay on the Alpine image, subject to the in-container spike. The 4.x API differs from the widely documented 2.88 releases (text uses SKFont), so code must target the pinned version, not older examples. The renderer bundles its font and loads it by file, so the font-config-free NoDependencies assets fit.

Server simplifications

  • Assets in memory. Rendered cards are small (roughly 50–150 KB each), and a tmpfs counts against the same 256 MiB memory limit as the heap. Each immutable snapshot holds its PNG bytes directly, keyed by SHA-256; the current and two previous snapshots per profile are retained. An in-flight response keeps its bytes alive by reference, which replaces file staging, atomic renames, stream leases, and eviction on the server.
  • ETag. Computed once at publication from the serialized manifest bytes.
  • One retry layer. Providers use typed HttpClients from IHttpClientFactory, which pools and recycles handlers. Polly-based resilience handlers are not added: the background coordinator already owns per-source due times, minute-scale backoff, Retry-After, 401/403 stops, and expiry. Layering per-request retries under it would multiply requests against a failing provider and hide the real failure pattern. If the coordinator ever delegates retries to Polly, it must drop its own.
  • Rate limiting with bounded state. The bearer token is validated first by hashing it and comparing against the configured hashes. Valid requests are partitioned by the device's server-side ID, which is bounded by the number of provisioned devices. Missing or invalid tokens share one global bucket. This avoids an attacker-sized partition table and the eviction machinery a capped cache would need.
  • Garbage collection. The container uses the workstation GC to keep its footprint small under the 256 MiB limit.
  • Static files. UseStaticFiles is removed; cards are served only by the authorized asset endpoint.

Device credential

The device credential is an opaque bearer token, unrelated to provider access (none of the PoC sources needs a key):

  1. Generate 32 random bytes per phone on a workstation, for example with openssl rand -base64 32.
  2. Store only its SHA-256 hash in the VPS's private production.env, mapped to the bedroom profile. Removing the hash revokes that phone.
  3. Provision the token into the app's private storage. Because the APK is debuggable, a helper script can pass it on standard input through adb exec-in with run-as, so it never appears in command arguments, logs, or screenshots. A small paste dialog remains available on modern phones.
  4. Send Authorization: Bearer <token> with every manifest and card request. Health endpoints stay public.

Client simplifications

  • Content-addressed cache. Images are stored once by hash. Activating a manifest is one atomic write of a small pointer file with Android's AtomicFile (API 17+). Unreferenced images are removed by a reference sweep that never touches the displayed set.
  • Bitmaps. Opaque cards decode as RGB_565, with one visible bitmap and one idle reusable bitmap in steady state, about 1.76 MiB each. Transparent images may require ARGB_8888; transient decoding and the clock resources must still be measured rather than inferred from the steady-state bound.
  • Parser tests. org.json is added as a test-only dependency so the real parser runs against the shared fixtures in plain JUnit tests.
  • Insets. The Android 10 navigation-inset fix lands as its own early commit, because it changes the geometry cards share with the clock.

Order of work

The two largest unknowns are retired first, in parallel: a real HTTPS request from the XT1058 to retroapi using the bundled roots, and SkiaSharp drawing a card inside the actual Alpine container. The contract and fixtures are then frozen, followed by a thin end-to-end slice with one synthetic card.

Implementation sequence and completion gates

Every phase follows Testing and quality checks: format/review changed code, verify formatting, run affected lint/analyzers and regression suites before commit/handoff, and report missing checks. Shared contracts require both Android and backend tests. Routine provider tests use fixtures/fake HTTP, with live smoke tests separate. Application CI is planned, not implemented; its future gates must reuse the local checks without applying format fixes or automatically deploying production.

0. Access, fixtures, and configuration — implemented locally

Validated source/profile configuration and sanitized fixtures are implemented. Sports fixtures cover the game states below; a real live-game observation is still distinct from fixture coverage. The original completion checklist follows.

  • Confirm the MLB schedule/linescore mapping against the verified response; add fixtures for scheduled, pre-game, live, final, postponed, and empty states. Verify actual live-score behavior separately; no MySportsFeeds work or key provisioning is required.
  • Define one validated bedroom configuration; no database is needed.
  • Capture sanitized small provider fixtures for tests, respecting source terms; no secrets, full production dumps, or real personal data.
  • A blocked sports integration must not block weather/news development, but live Yankees functionality is not complete until live-state handling and end-to-end delivery are verified, even though schedule access already succeeded.

1. Local modes without disturbing the clock — automated API-22 gate passed

Modes, persistence, chooser, Highlights, inset-aware layout and preview/Showcase ownership are implemented. Fresh XT1058 tests passed. Physical Android-10 inset confirmation and a manual process-restart check remain outstanding.

  • Introduce testable mode/bulletin state separate from the Activity and themes.
  • Add minimal chooser, persistence, highlights-now action, and mocked static cards clearly marked as test content.
  • Correct the measured navigation inset and test all existing themes/previews.
  • Gate: Just Clock still works offline and never starts dashboard networking.

2. Contract and secure transport vertical slice — implemented locally

Shared schema-1 fixtures, auth/ETags, transport, atomic cache and native cards are implemented. The XT1058 TLS suite reaches the public scaffold and rejects bad certificates; local real cards were displayed through the debug loopback path. Production HTTPS manifest-plus-assets acceptance awaits the authorized rollout.

  • Extend typed API contract and tests; implement stable ETags, protected immutable assets, fixed profile authorization, and a deterministic test card.
  • Add API-22-compatible Java transport, bounded org.json parsing, cancellation, scoped certificate compatibility, atomic disk cache, and image rendering.
  • Gate: a real API-22 device can fetch an HTTPS manifest/image, reuse 304/cache, survive partial downloads, and return to its clock on failure.

3. Real providers and server rendering — implemented; final image gate below

All four real sources and the rendering/publication pipeline are implemented. Routine tests use fixtures; exact-image real-provider rehearsal is a separate release gate, not implied by the small SkiaSharp spike or the API unit suite.

  • Add typed HttpClient adapters, validated options, short timeouts, cancellation, TimeProvider, independent cached snapshots, bounded background refresh, exponential backoff/jitter, and upstream conditional requests.
  • Implement Open-Meteo, MLB Stats API, and each RSS adapter independently behind normalized category models. Render up to four cards and compose the stable feed; replacing the sports adapter must not require Android changes.
  • Test missing/null fields, invalid XML, timezones, postponed games, doubleheaders, 401/403/429/5xx, provider timeout, expiry, attribution, and Linux PNG rendering.
  • Gate: real weather/news reach the device; no provider secret reaches it; unavailable sports is skipped, never replaced by an unlabeled mock.

4. Scheduling and offline operation — automated API-22 gate passed

Foreground cancellation, schedule/expiry, auth clearing, continuous lap adoption and one-shot restoration passed the actual Activity tests. Physical Wi-Fi loss/recovery and a multi-hour soak remain separately pending.

  • Add foreground refresh coordinator and :00/:30 bulletin policy; use existing quiet-hours configuration. Add continuous dashboard and one-shot restore.
  • Share small contract fixtures between backend and Android tests; test unknown fields, unsupported schemas, bounds, checksums, stale manifests, resume, wall-clock changes, quiet hours, mode changes, and empty feeds.
  • Gate: offline cards show only while valid; expiry returns to the clock; leaving the app or choosing Just Clock stops its dashboard work.

5. OVH rollout, only with deployment authorization

  • Read-only preflight: current RAM/swap pressure, disk space, Docker/log usage, AndreaWeb health, existing Nginx listeners, and current release state. Do not reuse an old audit as evidence of today's capacity.
  • Use the existing commit-addressed release process, loopback 127.0.0.1:8088 behind Nginx, one API container, HTTPS hostname, and server-only runtime secrets. Reuse shared snapshots; no worker per device or new database/media server.
  • Rehearse the exact saved whole-application image under the existing 256 MiB, 0.5 CPU and 128-PID limits before rollout. Current-plus-two snapshot retention is process memory; there is no writable filesystem card cache. /tmp remains tmpfs, and Docker logs are bounded. Do not substitute a renderer-only spike.
  • Coordinate hostname/certificate/Nginx edits deliberately. Validate Nginx before reload and compare AndreaWeb health/resource use before and after.
  • Smoke-test health, token denial, manifest, asset, 304, phone TLS, and live cards. Keep old APK/schema compatibility and previous API release for rollback.
  • Update deployment documentation with measured requirements, secret variable names (not values), refresh policy, diagnosis, and rollback.

6. Appliance acceptance

Step 3: XT1058/API 22 acceptance method

Run the full instrumented suite and the three focused acceptance classes on the XT1058/API 22. The suite covers scheduled Bulletins at Bogotá boundaries and quiet hours; Highlights-now restoration and polling policy; selected-mode persistence across Activity relaunch; native connection-dialog/immersive behavior; Showcase takeover and clock-face access; pause/resume cancellation; cached display during a dropped connection; controlled expiry and recovery; manifest 401 and asset 403 handling; and steady-state card bitmap reuse and release. Use the exact build/install and full/focused runner commands in the testing procedure.

Schedule/expiry automation substitutes a test clock only inside the presenter and explicitly invokes its minute callback; never change the XT1058 or host system clock to force a test boundary. Its offline case drops a loopback HTTP connection, which is deterministic client failure injection, not a physical Wi-Fi-loss test. Test Wi-Fi loss and restoration separately on the device. The memory assertion samples the visible bitmap and reuse pool after cards are shown; it does not measure peak allocation or replace a long soak. The fixture uses a random loopback origin and synthetic token, restores connection/mode/theme preferences, and clears only its test-origin cache after the app workers stop.

The complete 31-test device run passed, including a real Home-key exercise that stops clock motion, audio and polling and resumes the same Activity. At that stage, manual force-stop and physical connectivity checks were pending; Step 6 subsequently passed those checks against production HTTPS. Keep the broader appliance gate open for the MLB provider rejection, a separate modern-device check, Bluetooth audibility and a multi-hour bedside soak. The Moto X Play / Android 10 check subsequently passed on 2 October 2026; see testing evidence.

Completion of this acceptance step means Just Clock remains poll-free; the selected persistent mode, schedule, pause/resume, expiry, auth failure and recovery behaviors pass their API-22 checks; the sampled image allocation bound holds; and no fixture state leaks into user preferences or cache. Real provider freshness/content, a physical force-stop/relaunch check, physical Wi-Fi loss/recovery, and broader compatibility remain separate gates. No user accounts, emergency-alert guarantee, photo album, Shower Radio, or automatic boot/wake is implied by this document.

Final local release gates (Steps 4 and 5)

Run fresh full backend and Android gates, relevant physical instrumented cases, script regressions, ShellCheck/shfmt, Compose, UTF-8/no-BOM/LF/EOF and diff checks. Export the reconciled existing docs and run strict MkDocs. Do not enable CI or commit hooks as an incidental part of this work.

Prepare one clean explicit commit with prepare-release.sh, then run rehearse-release.sh against its saved image, not a rebuild. Require all four real provider cards and verified PNG assets, repeated source refreshes and publication, cold/steady/failure/recovery cgroup memory evidence, and the actual production hardening/CPU/PID limits. The rehearsal accelerates source refresh to one minute for observation; normal checked-in policy remains unchanged. Kernel memory.peak is distinct from point-in-time memory.current samples. Record observed swap, OOM counters, source failures/recovery and immutable image identity. Resource measurements do not establish capacity for future profiles, new providers, or a multi-hour soak.

Freeze the tested image/source artifacts outside Git, then add its receipt here as a documentation-only commit. This does not deploy or publish anything. The deployment runbook explains how a later authorized rollout preserves that artifact rather than rebuilding it.

Release candidate receipt

Passed on 1 October 2026. The deployable API image was prepared from the clean, explicit commit a47e94caab8c1a33c1db33df7e27de95f9888a1e under umask 077. The rehearsal loaded that saved archive and ran from 14:26:55 to 14:31:34 UTC (09:26:55–09:31:34 Bogotá), 279 seconds. This receipt is a later documentation-only change; it does not change the tested application or rebuild the image.

Identity Frozen value
Image tag motoxdashboard-api:a47e94caab8c
Portable image configuration digest (image.id) sha256:927c0cb13965405529e33c3420b886b4f13eb7c3877576463ac861b2c4a13201
Docker Desktop daemon/index ID sha256:e2c3a9119521916eb8a6547a9e6076e67c4c42e26eab12db4d01f4582fbf4d46
Saved image.tar SHA-256 34054c92fb6d0c2a8fa8ecc667f1a9ecc6dd24b0911ea67168d8d6d2cf627a25
Saved source.tar SHA-256 9fae816e67f89abd5808b9a999e5eea8fcf0a209633c79d3f37e6543436a7148

Artifacts are retained outside Git in WSL:

  • API archive/source/checksums: /home/justin/.local/state/motoxdashboard/releases/rc-20261001-a47e94c;
  • phase samples, manifests, fetched images, provider events and receipt: /home/justin/.local/state/motoxdashboard/rehearsals/rc-20261001-a47e94c;
  • tested debug and instrumented APKs plus checksums: /home/justin/.local/state/motoxdashboard/releases/android-rc-20261001-a47e94c.

The fresh gates passed: 846 backend xUnit tests, 831 Android JUnit tests, and 31 XT1058/API-22 instrumented tests, with no failures or skips. Backend build, analyzers and format verification passed with zero warnings/errors; Spotless, Android lint (zero errors/seven existing warnings), both APK assemblies, all three shell regression helpers, ShellCheck, shfmt, Compose validation, text/diff checks and the strict MkDocs export/build passed. The Android source remained unchanged through the container/helper fixes; the preserved debug APK hash is 8804e70c111891e46baf2079d7ef1faaa986481bf3189a191a5570737283ea2a.

The full image produced all four real cards, seven observed manifest revisions, and 28 verified asset fetches. Each provider completed three successful fetches before the outage and another after recovery; Guardian conditional upstream 304 responses were handled successfully. Anonymous 401, authenticated 200 and manifest 304 passed. Weather and headline PNGs were also visually inspected for glyphs and layout. The one-minute refresh override accelerated normal polling; the live-game interval remained two minutes. No synthetic feed was enabled.

Phase Cgroup current memory Kernel peak so far
Startup before full feed 31.80 MiB 32.02 MiB
First four real cards 50.84 MiB 51.51 MiB
After repeated refresh/publication 63.34 MiB 64.30 MiB
All four providers failing 64.57 MiB 64.82 MiB
Recovered feed 65.43 MiB 67.13 MiB

The final kernel high-water mark was 70,389,760 bytes under the 268,435,456-byte (256 MiB) memory cap. Every phase sample recorded zero swap; cumulative memory limit-hit/OOM/OOM-kill counters were zero. Compose-equivalent settings were verified: 0.5 CPU (50000 100000 quota), 128 PIDs, non-root app, read-only root, /tmp tmpfs, all capabilities dropped, no-new-privileges, and 5 MB × 3 Docker log rotation. The highest sampled PID count was 20, including measurement processes. Final CPU accounting showed 4.53 CPU-seconds and 31 throttled periods (1.51 seconds), consistent with enforcing the quota. These are phase samples plus the kernel's memory peak, not continuous PID/swap peaks.

Disconnecting the isolated network produced one logged timeout for every provider. The application PID stayed the same. The host API endpoint was also unreachable during this network fault, so the run does not establish request availability during an upstream-only outage. After reconnection, the retained manifest and its asset returned 200 with the original revision/hash; all four providers then recovered and published a new revision. Repeated publications exercise the current-plus-two-snapshot store under load; the direct outage probe checks the last confirmed current snapshot. Prior-revision eviction semantics remain covered by deterministic store tests. The helper removed its container, network and private throwaway token files after the run.

A read-only VPS check on 1 October confirmed /opt/motoxdashboard/current and the recorded deployed commit still identify b2221b742993db040467c92064877e83bbd83450. No code push, docs-site publication, or production mutation was performed during Steps 4–5. Step 6 later deployed this exact image and passed force-stop/relaunch, physical Wi-Fi recovery and three-source production HTTPS acceptance; see the deployment receipt. MLB production acceptance, modern-device coverage beyond the Moto X Play, Bluetooth audibility and a multi-hour soak remain open. The Play's Android 10 inset, client suite and production HTTPS were accepted on 2 October 2026; see testing evidence. This bounded rehearsal does not establish capacity for future source/profile counts.

The clean preparation caught two operational departures before freezing: private umask 077 made root-owned published files unreadable by app (fixed with ownership on the final image copy), and a legitimate provider publication between GET/If-None-Match could make the old one-shot smoke fail with 200 (fixed with bounded changed-ETag retries). Neither failed preparation is a release candidate. An intermediate rehearsal was also restarted after correcting how nested Docker settings were retained in its report. The complete evidence above comes from the final replacement commit and archive.

Provider references

See also Dashboard overview, Backend, Android compatibility, and Architecture.