Session 1 of 2 of the i18n rollout.
- Split i18n module into per-language files under frontend/js/i18n/ so a
translator can edit one language without touching the others.
- Add Portuguese (pt) and seed Hindi (hi). Hindi is intentionally a skeleton
-- 0 keys, full English fallback -- because we have an active Indian user
and would rather ship "no Hindi" than ship machine-quality Hindi that
could read as unprofessional or get formality/gender register wrong.
- 183 keys, 100% parity across en/es/fr/de/pt; native review still
recommended before publicizing as "fully supported".
- Add t(key, vars) variable substitution and tn(keyBase, n, vars) plural
helper for _one/_other key pairs.
- setLanguage() now triggers a CustomEvent + HashChangeEvent so the
existing hash router naturally re-renders the current view, plus a
subscriber pattern for nav labels rendered once outside the router.
- Wire t() into 3 high-traffic views end-to-end: dashboard, login,
content-library. Sidebar nav labels in app.js update on language change.
- The remaining 16 views still ship with hardcoded English; they will be
wired in session 2. The t() lookup is robust against unwired views, so
the dashboard works in 5 languages while clicking into e.g. Schedule
still shows English. No regressions.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Device cards are now draggable. Group sections accept drops to add
membership (mirroring the Manage modal — same confirmation if the
device is already in another group). The Ungrouped section also
accepts drops to remove the device from every group it's in.
The existing Manage modal still works for bulk add/remove and for
finding devices not currently visible. Click-to-open on a card still
works; drag is only triggered on actual mouse movement.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Server-side: when a device reconnects on a fresh socket while the old
TCP zombie is still around, the old socket's eventual disconnect handler
flipped the device offline and removed the new heartbeat entry. Now we
proactively evict any prior socket on register and ignore disconnects
from sockets that are no longer the registered one for that device_id.
Frontend: dedupe devices by id from the API response and only render
each device in the first group it belongs to (multi-group membership
is still tracked for the Manage modal).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 4 group scheduling: schema migration adds group_id to schedules with
CHECK constraint, scheduler evaluates group+device schedules with priority,
group deletion converts schedules to per-device copies. Dashboard gets
playlist assignment dropdown and current playlist label on group headers.
Player persists audio unlock state in localStorage so version reloads
don't lose audio on unattended displays.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add requireGroupOwnership middleware to all group endpoints
- Whitelist allowed command types (screen_on/off, launch, update, reboot, shutdown)
- Validate color format as #RRGGBB
- Escape all user-controlled strings (device/group names, emails) in dashboard HTML
- Restrict trust proxy to first hop only (prevents IP spoofing + rate limit bypass)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Dashboard now organizes devices by group with colored section headers
- Group command endpoint (POST /groups/:id/command) sends to all members
- Manage modal with multi-group confirmation prompt
- Destructive commands (reboot/shutdown) require confirmation
- Ungrouped devices shown separately at bottom
- trust proxy + X-Forwarded-For for real client IPs behind Nginx
- Hide Android-only telemetry (battery/storage/RAM/CPU/WiFi) for web players
Co-Authored-By: Claude Opus 4.6 <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>