Commit graph

582 commits

Author SHA1 Message Date
xadyz1 ad4b7d618e fix: upgrade uuid to v11 for Node 22 CJS compat 2026-07-26 19:10:32 +01:00
xadyz1 0ae0984126 fixversionuuuid 2026-07-26 18:55:17 +01:00
xadyz1 0bf4eec56f remiote 2026-07-26 18:27:22 +01:00
xadyz1 f011dd2764 da 2026-07-26 15:26:37 +01:00
xadyz1 edc98f4073 da 2026-07-26 15:26:30 +01:00
xadyz1 bf7fa80a25 ee 2026-07-26 15:26:09 +01:00
xadyz1 5987db1ae4 d 2026-07-26 15:22:36 +01:00
xadyz1 299cf7c44c crashlog 2026-07-26 15:22:31 +01:00
xadyz1 bf65f91865 fix 2026-07-26 14:58:12 +01:00
xadyz1 28c2615a15 .. 2026-07-26 14:56:46 +01:00
xadyz1 f83f82f962 . 2026-07-26 14:51:31 +01:00
xadyz1 df111e528b fixdb 2026-07-26 14:50:02 +01:00
xadyz1 be15e5b1dc Update remote_display.db-client_wal_index 2026-07-26 14:39:56 +01:00
xadyz1 5c39ae13e6 , 2026-07-26 14:33:28 +01:00
xadyz1 09eb887cf7 . 2026-07-26 14:31:15 +01:00
xadyz1 7aa5497360 fixhtaccess 2026-07-26 14:29:32 +01:00
xadyz1 6bec5f63a8 .. 2026-07-26 14:19:59 +01:00
xadyz1 abb9294e4e ,. 2026-07-26 14:19:48 +01:00
xadyz1 67c6e0a31a v1 2026-07-26 14:06:26 +01:00
xadyz1 e1b444f252 1 2026-07-26 13:12:02 +01:00
xadyz1 93fc5021a1 . 2026-07-26 13:08:57 +01:00
xadyz1 90e48581e2 Update database.js 2026-07-26 13:04:56 +01:00
xadyz1 6cf86533e4 c 2026-07-26 13:04:18 +01:00
xadyz1 c9adb31882 . 2026-07-26 13:03:11 +01:00
xadyz1 6923385398 buildfix 2026-07-22 14:01:30 +01:00
xadyz1 bb83bd885a . 2026-07-22 13:53:49 +01:00
xadyz1 c8cc12810a . 2026-07-22 13:39:12 +01:00
xadyz1 38fd41324d v1 2026-07-22 13:32:59 +01:00
ScreenTinker 2d4af97f67 docs: fix double-escaped < in API reference, add README hero image
Some checks are pending
CI / Unit tests (node --test) (push) Waiting to run
CI / OpenAPI spec lint (push) Waiting to run
CI / Android unit tests (Kotlin schedule evaluator vectors) (push) Waiting to run
CI / Boot smoke + version check (push) Waiting to run
- openapi.yaml: use the numeric entity &#60; so Redoc renders
  'read < write < full' in the scope-ladder nav + section header,
  instead of the double-escaped 'read &lt; write &lt; full'
- README: add a centered dashboard hero image + quick-links row
  (Live demo / API reference / Self-hosting guide / Discord) at the top,
  and refresh the Support section

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 22:51:55 -05:00
screentinker 8c0bf77428
fix(web-player): reconcile advanceTimer on group/wall mode transitions (#200) (#208)
Some checks are pending
CI / Unit tests (node --test) (push) Waiting to run
CI / OpenAPI spec lint (push) Waiting to run
CI / Android unit tests (Kotlin schedule evaluator vectors) (push) Waiting to run
CI / Boot smoke + version check (push) Waiting to run
Reconcile advanceTimer on mode enter/exit via reconcileAdvanceTimerForMode in applyWallMode/applyGroupSync — fixes the group-entry zombie timer and the solo-exit frozen image. Closes #200.
2026-07-21 09:00:42 -05:00
screentinker 3efae1d2d6
feat(designer): edit designer-made widgets in the designer (+ legacy reconstruction) (#207)
Designs round-trip for visual editing: store the design source in the widget config, reroute Edit to the designer, reconstruct legacy HTML-only designs, PUT the original in place.
2026-07-21 09:00:36 -05:00
screentinker 397c4e1aec
fix(designer): let weather elements switch units to metric (#206)
Some checks failed
CI / Unit tests (node --test) (push) Waiting to run
CI / OpenAPI spec lint (push) Waiting to run
CI / Android unit tests (Kotlin schedule evaluator vectors) (push) Waiting to run
CI / Boot smoke + version check (push) Waiting to run
Shaders / Compile transition shaders (real WebGL) (push) Has been cancelled
Adds the missing Imperial/Metric units selector to the designer's weather element properties.
2026-07-20 18:22:42 -05:00
ScreenTinker 6b5b401f7c chore(release): v1.9.11 2026-07-20 16:59:56 -05:00
screentinker ba00dd2811
fix(transition-engine): Android supersede wedge/leak + web/Tizen stale-video guard (#205)
Pre-release review follow-up to #204: fixes the Android superseded-wipe playlist wedge + GL leak, and adds the stale-item guard to web/Tizen renderVideoBuffered.
2026-07-20 16:58:28 -05:00
screentinker 96b71a0d56
feat: transition engine — GL wipes across web, Tizen & Android (+ image↔video) (#204)
Client-side GL Transitions v1 across all three players with never-blank degradation: shader lib + generated manifest + real-WebGL CI, transition-as-widget normalization, persistent WebGL/GLES2 compositors (web/Tizen/native Android), image↔video wipes, SSRF-hardened media proxy, decode-gated image preload, dashboard picker. Fixes the playlist change-fingerprint that dropped transition edits. Alpha + on-device soak validated.
2026-07-20 16:45:32 -05:00
ScreenTinker af89eaa75b chore(release): v1.9.10
Some checks failed
CI / Unit tests (node --test) (push) Has been cancelled
CI / OpenAPI spec lint (push) Has been cancelled
CI / Android unit tests (Kotlin schedule evaluator vectors) (push) Has been cancelled
CI / Boot smoke + version check (push) Has been cancelled
2026-07-17 20:24:39 -05:00
screentinker 335681b907
feat(directory-board): panel-ring scroll + in-place refresh + per-device frame diagnostic (#203)
Compositor panel-ring board scroll (smooth on Blink+Gecko, no blank-on-refresh), a per-device frame-rate diagnostic widget + dashboard card, and web/Android/Tizen device-id passthrough to widget render URLs.
2026-07-17 20:17:21 -05:00
screentinker bb6c7597da
fix(web-player): buffered widget swap + schedule-aware solo-board hold (directory-board black flicker) (#202)
Some checks are pending
CI / Unit tests (node --test) (push) Waiting to run
CI / OpenAPI spec lint (push) Waiting to run
CI / Android unit tests (Kotlin schedule evaluator vectors) (push) Waiting to run
CI / Boot smoke + version check (push) Waiting to run
* fix(web-player): buffered widget swap + solo-board hold to end directory-board black flicker

A fullscreen widget (e.g. a solo directory board) re-rendered on the advance timer:
renderContent tore the container down to black (innerHTML='') BEFORE the replacement
iframe finished loading, and a single/only-active widget re-advanced to itself every
duration_sec — so the board cycled black every few seconds. That reload was ALSO the
only thing refreshing the board's static, server-rendered data, so simply holding it
in place would freeze the data.

- Buffered swap: build the new widget iframe hidden OVER the current content and reveal
  it on 'load', then tear down the outgoing content — no black frame on any widget
  transition. On a load timeout, keep the last-good board and discard the dead hidden
  frame via a shared cleanup path (don't reveal a blank frame); a transient server blip
  self-heals on the next refresh.
- Solo/held widget (nextActiveIndex === currentIndex): hold in place and refresh its
  DATA on a decoupled interval (WIDGET_SOLO_REFRESH_MS = 60s) via the buffered swap,
  instead of re-querying the DB + re-rendering full HTML every duration_sec, fleet-wide.

Scoped to non-wall fullscreen widgets; wall+widget keeps the legacy path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(web-player): route held directory-board refresh through nextItem (schedule-aware)

Follow-up to the buffered widget swap: the solo/held board refreshed via a bespoke
self-rescheduling loop that never re-evaluated the schedule — so a board could outlive
its daypart, and a newly-active sibling item was never picked up (the player stuck on
the board). Delete the duplicate loop entirely and advance via nextItem in both the
held (WIDGET_SOLO_REFRESH_MS cadence) and rotating (duration) cases: nextItem
re-evaluates the schedule every cycle and re-renders the held board through the buffered
swap (still no flash), and drops the duplicate code path that caused the bug.

Verified: the timer-lifecycle harness (6 scenarios / 68 assertions) still passes,
including widget->video transition and the leak/timer-count checks.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 13:42:12 -05:00
screentinker 5c1cb4b992
fix(server): floor duration_sec to prevent widget zero-duration player loop (#199)
A duration_sec=0 assignment (especially a widget) made the player schedule a 0ms
auto-advance, self-looping and black-screening the TV. #198 fixed the Android
client; this hardens the source so a 0 can't be stored or served in the first
place. assignments.js accepted an explicit 0 on the POST/PUT/copy write paths —
the `= 10` destructure default only covers an ABSENT field, not an explicit 0.

- Add normalizeDuration() and apply it on all assignment write paths so any
  missing/invalid/<1 duration is floored to the 10s default.
- Add an idempotent migration repairing existing playlist_items rows with
  duration_sec IS NULL OR < 1 (fixes the live widget on existing DBs).

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 13:41:52 -05:00
screentinker af286826dc
fix(android): stop zero-duration widget self-loop pegging the main thread (#198)
Some checks are pending
CI / Unit tests (node --test) (push) Waiting to run
CI / OpenAPI spec lint (push) Waiting to run
CI / Android unit tests (Kotlin schedule evaluator vectors) (push) Waiting to run
CI / Boot smoke + version check (push) Waiting to run
A solo fullscreen widget (or image) with duration_sec=0 hit an unclamped
scheduleAdvance(item.durationSec * 1000L) in PlaylistController.playCurrentItem,
scheduling a 0ms auto-advance. For a single-item playlist next() re-selects the
same item, so it re-played every looper tick (~20x/sec) — black-screening the TV
and locking the UI (couldn't even reach home). Triggered when a schedule collapses
the playlist to a single always-on duration-0 widget.

Use slotMs() (the max(1, duration||10) contract shared with the web/Tizen players)
so a zero/negative duration floors to 10s. Also floor scheduleAdvance() itself to
MIN_ADVANCE_MS (500ms) as a backstop so no future path can busy-loop the main thread.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 00:01:19 -05:00
screentinker 059ee1744e
fix(widgets): directory board scroll stutter — seamless loop gap mismatch (#197)
Some checks are pending
CI / Unit tests (node --test) (push) Waiting to run
CI / OpenAPI spec lint (push) Waiting to run
CI / Android unit tests (Kotlin schedule evaluator vectors) (push) Waiting to run
CI / Boot smoke + version check (push) Waiting to run
The vertical auto-scroll is a CSS keyframe that translates the track by
cycleH = baseH + GAP_PX and loops linear infinite. GAP_PX was 100 but the actual
.gap element between the content and its seamless clone is 120px, so every cycle
the reset landed 20px off — a visible jump/stutter once per loop.

Set GAP_PX = 120 to match the .gap CSS, and drive each gap element's height from
GAP_PX inline so the scroll math and the rendered gap can never drift again.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 14:11:21 -05:00
screentinker 5f5ec88eb0
fix(widgets): put the CORP: cross-origin header on the route that actually serves content (#196)
Follow-up to #195. The CORP fix there landed on routes/content.js `/:id/file`, but that
handler is SHADOWED: server.js registers a public `app.get('/api/content/:id/file')`
(and `/thumbnail`) BEFORE the auth-gated content router, and that public route (gated by
playlist/widget reference) is what actually serves widget logo/background images. So the
header never changed on the wire — origin still returned CORP: same-origin and the player's
sandboxed (opaque-origin) widget iframe kept getting NS_ERROR_DOM_CORP_FAILED / 0 bytes.

Set Access-Control-Allow-Origin: * + Cross-Origin-Resource-Policy: cross-origin on the real
public routes in server.js: /file, /thumbnail (local), and the remote-thumbnail proxy.
Revert the now-dead content.js edit so the fix lives only where the bytes are served.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 14:02:31 -05:00
screentinker 178af029a4
Directory board: JSON/CSV import + logo-replaces-title + fix images on player (#195)
* feat(widgets): bulk import for the directory board (JSON / CSV / TSV / text)

Adds an "Import from JSON / CSV" button to the directory-board editor. Paste JSON
(the { company, tenantsByFloor, advertisements, backgroundImages } shape plus
categories[]/floors[]/flat-array/bare-floor-map variants), a CSV/TSV/pipe/semicolon
table (with or without a header — vacant/yes/1 => available, quoted fields), or a
sectioned "room name" text list, and it auto-fills title, footer, floors->categories,
rooms/names/details/availability, and background-image URLs. "Replace / append" toggle.

Tolerant key matching (room/suite/unit/id, name/tenant/company, details/subtitle, …);
warns on things it can't use (bare-filename background images, headerless columns).
parseDirectoryImport is pure and was unit-tested in node across every format.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(widgets): directory board — logo replaces title, and images load on the player

Two on-screen bugs on the directory board:

1. A logo did not remove the title text — both rendered, stacking the wordmark over
   the name. renderDirectoryBoard (and the directory-search header) now gate the title
   h1 behind !logoSrc, so a logo replaces the title. New render test guards it.

2. Logo + background images did not show on the player (NS_ERROR_DOM_CORP_FAILED,
   0 bytes). The player embeds widgets in a sandbox="allow-scripts" (opaque-origin)
   iframe, so /api/content image requests are cross-origin, and the helmet default
   Cross-Origin-Resource-Policy: same-origin blocks them. Set CORP: cross-origin (+
   ACAO:*) on the content file + thumbnail routes, matching the existing /uploads/content
   static route. Content already serves publicly, so no new exposure. Verified in a real
   sandboxed iframe: same-origin blocks, cross-origin loads.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 13:53:34 -05:00
screentinker 61c1246b5b
fix(ui): make modals scroll instead of overflowing the viewport (#194)
The base .modal had no max-height/overflow on desktop — only the mobile media
query capped it — so a tall modal (e.g. the directory-board widget editor with
many tenant entries) grew past the screen with no scroll, stranding the lower
entries and the Save button off-screen ("unusable").

Cap .modal to 90vh and lay it out as a flex column so .modal-body becomes the
scroll region (flex + min-height:0 + overflow-y:auto) while the header and footer
(Cancel / Save) stay pinned and always reachable. Moved the cap onto the base
rule and dropped the now-redundant overflow from the mobile override.

Shared across all modals (all use the header/body/footer structure); short modals
are unchanged since max-height is a ceiling, not a fixed height. Verified in real
Chrome at a 700px viewport: modal capped to 630px, body scrollable (overflow=auto),
Save footer on-screen. CSS-only, no JS.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 12:12:52 -05:00
screentinker 76f072bf4b
fix(tizen): decode-gated image double-buffer to kill the black flash between stills [#187] (#193)
Some checks are pending
CI / Unit tests (node --test) (push) Waiting to run
CI / OpenAPI spec lint (push) Waiting to run
CI / Android unit tests (Kotlin schedule evaluator vectors) (push) Waiting to run
CI / Boot smoke + version check (push) Waiting to run
The Tizen .wgt player black-flashed between IMAGE items on slow decode HW
(Samsung OM55B / SSSP). Root cause: playCurrent() called clearStage() before
renderImage() set img.src, so the stage was empty (black) until the new image
decoded. Images had no decode-gated double-buffer — video gained one in #167
(938a43a), which made the always-present image flash conspicuous by contrast.
renderImage has been byte-identical since the first Tizen commit, so there was
nothing to revert; the buffer had to be ADDED, mirroring the video path.

- preloadImage()/_takePreloadImage()/_releasePreloadImage(): one-ahead,
  image-only decode-gated buffer modeled on preloadVideo()/_takePreload().
  Detached <img>, src set, warmed via HTMLImageElement.decode() (feature-
  detected — onload/complete fallback for Tizen 5.0 / SSSP6). Warmed when the
  current image begins its dwell and from the group-sync boundary tick.
- renderImage now SWAPS: take the pre-decoded <img> (or decode a fresh one) and
  only THEN clearStage()+append, in one synchronous block — the compositor
  never sees an empty stage. Never clear-then-load on the image path.
- Scoped to images only: playCurrent() skips the up-front clearStage() solely
  for image targets (same branch order as the dispatch); video/youtube/widget
  keep their pre-dispatch clear untouched. onerror and decode() rejection route
  to skipSoon(); stale-index guard blocks mounting a stale decode over the
  current item after next()/gotoIndex/load(); one-ahead with stale release on
  index move, load(), stop(), and group-sync exit. #A1 single-item heal intact.

Tests:
- server/test/tizen-image-blackflash.test.js (new, 4/4): loads the real
  player.js in a vm context with a test-controllable decode() Promise and proves
  the invariant — across image->image the #stage is NEVER without a mounted
  <img> (old element held until the new image's decode()/onload resolves, then
  swap). Covers decode() supported, decode() absent (onload fallback), and
  broken-image (decode-reject / onerror) -> skipSoon. Proven to VIOLATE on the
  old clear-then-load ordering and HOLD on the fix.
- server/test/pip-overlay.test.js: the decode-gate makes image mount async,
  which broke its older shim (no decode()/onload/complete). Teach the shim
  element complete/naturalWidth so renderImage takes its synchronous
  complete-fallback branch and mounts. Test-only.

Full server suite 486/486. node -c clean. Headless proves the DOM ordering
invariant (the flash's precondition); final black-frame sign-off needs a real
OM55B panel (manual steps in the test-file header).

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 18:29:39 -05:00
ScreenTinker 79cf4cac2b chore(release): v1.9.9
Some checks are pending
CI / Unit tests (node --test) (push) Waiting to run
CI / OpenAPI spec lint (push) Waiting to run
CI / Android unit tests (Kotlin schedule evaluator vectors) (push) Waiting to run
CI / Boot smoke + version check (push) Waiting to run
2026-07-15 16:06:18 -05:00
screentinker ba0663edc1
fix(pairing): close deferred-offline reclaim race + idempotent same-code adopt (#192)
A fresh unclaimed player that reconnects (same fingerprint) INSIDE the server's
~5s deferred-offline grace hit a false 'active on another connection' reclaim
reject, then collided on UNIQUE(devices.pairing_code) on the fall-through INSERT
and wedged unclaimed with no content. Real trial customer (web player) hit it.

server/ws/deviceSocket.js:
- Fix A (guard): gate the liveConn reclaim reject on !inDeferredOffline
  (pendingOfflines.has(id)). A device mid-deferred-offline is a zombie, not live,
  so a same-fingerprint reconnect is a legit reconnect, not a hijack. A genuinely
  live socket (never disconnected -> no pending-offline) still rejects a cloned
  fingerprint -> anti-hijack boundary preserved (documented).
- Fix B (idempotency): when the unclaimed old row holds the SAME pairing_code the
  reconnecting player presents, ADOPT/refresh it (mirror the claimed-reclaim path,
  but no device:paired) instead of INSERT-colliding. Differing-code case unchanged.
- deferOffline is NOT shrunk (it exists to prevent transient-blip flapping).

server/player/index.html:
- The cold-boot flap source: an unfiltered pageshow handler ran verifyLivenessSoon()
  on every load, opening+registering a socket early, which the boot connect() then
  tore down and rebuilt (connect->register->disconnect->reconnect). Guard it with
  ev.persisted (mirror the pagehide guard) so only real bfcache restores trigger it.

server/test/pairing-race.test.js:
- Forces the race against the real socket server (log-gated reconnect inside the
  deferred-offline window), asserts no false reject / no UNIQUE collision / single
  claimable row; + a hijack case asserting a cloned fingerprint on a genuinely-live
  display is still rejected. Web- and android-shaped fingerprints. Fails 2/4 on
  pre-fix code, 4/4 with the fix.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 16:05:50 -05:00
ScreenTinker 87d5c5e1d7 chore(release): v1.9.8 2026-07-15 10:06:11 -05:00
screentinker 77322c631a
fix(android): show soft keyboard for PIN/URL dialogs over immersive fullscreen (#191)
The hidden-settings PIN box (2x back) and the change-server URL dialog couldn't be
typed into on kiosk devices: a plain AlertDialog shown over the player's
SYSTEM_UI_FLAG_IMMERSIVE_STICKY activity never gains window focus, so the soft
keyboard doesn't attach and the EditText gets no cursor. On a panel/Fire TV with
no hardware keyboard that means the PIN can't be entered at all. Pre-existing
(showPinDialog unchanged since it was added; immersive flags since the initial
release) — surfaces on tier-0 kiosk devices where immersive is active.

Fix: shared showImeDialog() applies the standard immersive-dialog IME workaround —
mark the dialog NOT_FOCUSABLE before show() (so it doesn't steal focus and reset
the activity's immersive flags), mirror the immersive systemUiVisibility onto the
dialog, then clear NOT_FOCUSABLE after show() so the IME can attach, force
SOFT_INPUT_STATE_ALWAYS_VISIBLE, and requestFocus the field. Routed both the PIN
and change-server dialogs through it.

Compiles (:app:compileDebugKotlin). IME behavior needs a real tier-0 device to
confirm (can't be exercised headless).

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 10:00:26 -05:00
screentinker fa31eb9cc3
fix(android): reset stuck download backoff on content change + network reconnect (#170) (#190)
Symptom 1's 'stuck on first load, fixed by toggling the playlist' is stale
download backoff. On a fresh device the first downloads fail while the link is
settling; DownloadCoordinator escalates an exponential backoff (15s..5min cap),
and ensure() then SKIPS those items. The 60s playlist refresh re-fires
onPlaylistUpdate but ensure() still skips them, and backoff was only ever cleared
by forget() (content-delete) — never by a re-assignment. So the item stays stuck
until the 5-min window happens to lapse; toggling the playlist is just a manual
way to wait it out.

Fix (storm-safe — neither reset fires on the routine same-playlist 60s refresh):
1. DownloadCoordinator.resetBackoff(id) / resetAllBackoff() — clear attempts +
   nextAttemptAt but KEEP inFlight (single-flight preserved, no duplicate .part).
2. onPlaylistUpdate resets backoff for each item ONLY when the content-id
   signature changed (first load, reassignment, toggle-back), then ensures — so a
   genuine (re)assignment retries immediately. Same-signature 60s refresh -> no
   reset -> the retry-storm guard stays intact.
3. Network onAvailable (was onLost-only) -> resetAllBackoff() + requestPlaylistRefresh,
   so content that failed while the link was settling retries the moment real
   connectivity arrives.

Tests: DownloadCoordinatorTest — resetBackoff/resetAllBackoff re-attempt a
backed-off item before the clock advances; existing backoff/single-flight tests
unchanged. :app:testDebugUnitTest green.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 09:07:43 -05:00