bump-version.sh matched `<?xml version="1.0" ...?>` - the XML format version,
which has a leading space before version= just like the widget attribute - and
rewrote it to the app version, producing invalid XML that breaks the Tizen .wgt
build: 'XML version "X.Y.Z" is not supported, only XML 1.0 is supported'. (CI did
not catch it because the no-Tizen-CLI build path just zips the files without
validating the XML.)
- bump-version.sh: skip the `<?xml` declaration line in the tizen version sed.
- tizen/config.xml: restore the declaration to version="1.0" (prior bumps had
corrupted it to 1.8.2).
The widget version and tizen:application required_version are still updated /
left alone correctly (verified with a dummy bump + an XML parse).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- scripts/upgrade.sh: upgrade a self-hosted instance to a tagged release
(default latest). Backs up the db (.backup), checks out the tag, npm ci
--omit=dev, restarts the service (SERVICE_NAME override), reports the version.
- README: replace the git-pull update flow with scripts/upgrade.sh (latest or a
pinned tag); keep main as the bleeding-edge option. Add a Samsung Tizen entry
to device setup (URL Launcher -> /player).
- tizen/README: point path A at the server's built-in /player, and explain why
the released .wgt is unsigned (Samsung distributor certs are DUID-locked).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- release.yml: build the Tizen .wgt before the source tarball and bundle it in
(ScreenTinker.wgt at the tarball root). The signed Android APK is added by the
local finalize step (the keystore stays off CI).
- scripts/finalize-release.sh: after the release workflow publishes a tag, build
the signed APK locally, pull the CI-built unsigned .wgt from the release,
assemble a complete tarball (source + apk + wgt at the root, where /download/apk
resolves the apk after extraction), and upload the apk + complete tarball.
- .gitignore: ignore *.wgt and *.tar.gz so finalize temp files cannot be committed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- server/version.js: shared version helper that reads the root VERSION file once
(fallback 0.0.0). Replaces the stale hardcoded 1.2.0 / 1.5.1 / 1.0.0 fallbacks
in /api/version, /api/update/check, and /api/status.
- config.js: DATA_DIR / DB_PATH / UPLOADS_DIR / CERTS_DIR env overrides for the
db, uploads, and certs/jwt-secret locations. Unset resolves to exactly the
legacy in-repo paths, so existing installs (including production) are
byte-for-byte unchanged. Guarded by test/config-paths.test.js.
- package.json: rename remote-display-server -> screentinker (+ lockfile name).
- scripts/bump-version.sh: one-shot bump across VERSION, package.json (+lock),
android (versionName and versionCode + 1), and the tizen widget version; makes
one commit plus an annotated tag; prints the push command, never pushes.
- .gitignore: global *.db / *.db-wal / *.db-shm / *.db.* so no database file
(including .db.devbak backups, at any path) can be committed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds scripts/backup.sh — atomic SQLite .backup + hard-linked point-in-time
content snapshots, daily (7) + monthly (12) retention, and an error log.
Env-configurable (SCREENTINKER_DIR/BACKUP_DIR/DB/UPLOADS/*_KEEP*) so any
self-hoster can use it; defaults target a /opt/screentinker install.
Hardens two real failure modes found in production:
- Content snapshots EXCLUDE uploads/screenshots/ and use rsync --link-dest
instead of cp -al. The per-device *_latest.jpg screenshots are rewritten
24/7; cp -al aborts when a file mutates mid-copy and the prior script
swallowed the error with 2>/dev/null, silently breaking content snapshots
for ~8 weeks. rsync --link-dest hard-links unchanged files but tolerates
in-flight changes; errors now go to backup.log.
- Retention sorts by NAME, not mtime: rsync -a / cp -al preserve the source
dir's (frozen) mtime, so ls -dt treated fresh snapshots as oldest and pruned
them. The timestamp is in the dir name, so name-sort is chronological.
README Backups section documents the cron setup + env knobs. Verified on prod.
Curl-pipe URLs, --help output, clone-and-run path, and the root-check
error message all referenced pi-setup.sh / setup.sh / screentinker/pi,
none of which exist. Point them all at the actual filename and path:
scripts/raspberry-pi-setup.sh.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Turns the Raspberry Pi script from a basic Chromium kiosk launcher
into a full installer with two modes:
- All-in-One: installs Node.js, clones the repo, runs the server
on port 3001, and launches the kiosk pointing at localhost. One
Pi does everything.
- Player-Only: connects to an existing server; same kiosk behavior
as before but with better Chromium flags and crash-flag cleanup.
Other changes:
- Detects Pi OS Lite vs Desktop and adjusts strategy (startx + vt1
for Lite, plain kiosk launcher for Desktop)
- Auto-login on tty1 for Lite installs
- GPU memory, overscan, console-blanking, and watchdog tweaks
- screentinker-{status,update,logs} management commands
- MOTD with command hints
- Cleans up the legacy remotedisplay.service / kiosk script on
upgrade so old installs migrate cleanly
- set -euo pipefail, root check, architecture check, tee'd log at
/var/log/screentinker-setup.log
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
scripts/reset-admin.js signed a JWT with a synthetic id ("recovery-XXX")
and instructed the operator to paste it into localStorage. But the
requireAuth middleware always SELECTs the user row by id, so every
authed API call under the recovery token returned 401 "User not found"
and the recovery flow was effectively dead.
Fix:
- reset-admin.js now sets a `recovery: true` claim on the JWT.
- requireAuth / optionalAuth short-circuit the DB lookup when
decoded.recovery === true and synthesize a req.user record in
memory (role: admin, plan_id: enterprise). The synthetic user is
never persisted, so FK-constrained writes that expect a real
user (creating devices, etc.) will still fail — which is fine,
recovery is only meant to let the operator reset a password or
create a fresh admin via the Settings UI.
Security: a recovery token still requires the jwtSecret to sign,
so only someone with filesystem access to the server can mint one.
Token TTL remains 1h.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
ScreenTinker - open source digital signage management software.
MIT License, all features included, no license gates.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>