- re-query keeps current rows visible instead of clearing + showing the full 'Loading' spinner
- loading affordance is delayed ~180ms: instant responses show nothing; slow ones get a subtle dim (aria-busy)
- full spinner reserved for the genuine first load; append (load-more) shows no indicator
- export button is now a click-toggle menu (aria-haspopup/expanded, role=menu, caret, Escape + click-outside close)
- filter placeholders moved to i18n (actor/entity_type .placeholder keys, en/ru/zh)
- _fetchPage clears _loading before render so a zero-result page doesn't spin forever
- toolbar/entry use elevated card surface (--lux-bg-1); light-theme device badge contrast; mobile message grid
- dashboard full re-render now reconciles sections (only replaces changed ones) instead of wholesale .dashboard-dynamic innerHTML swap -> editing an entity no longer jumps the whole dashboard
- Recent Activity widget live DOM + perf strip preserved across re-renders; widget skips re-fetch when already populated (no flash)
- sweep stray non-section nodes so empty->populated doesn't leave an orphan 'no targets' banner (review-caught regression)
- Activity list rows use a CSS grid (fixed badge/actor columns) so message column aligns consistently across rows
- localize entry descriptions client-side via localizeMessage (activity_log.msg.* + entity_type.* templates x3 locales); server message kept as fallback/export/search
- remove redundant Activity header banner from tab
- Recent Activity widget is now a first-class dashboard section (Customize Dashboard show/hide/reorder; pre-existing layouts preserved)
- live activity event updates the widget surgically (no full dashboard rebuild); single listener with teardown
- relative-time labels tick via shared ensureRelativeTimeTicker (single 30s interval, visibility-aware)
- Setup wizard discovery now scans every device provider, not just WLED:
drop the hardcoded device_type=wled filter so the backend's parallel
all-provider scan runs (Adalight, DDP, OpenRGB, BLE, DMX, etc). The list
UI was already multi-type aware (per-type icon + badge).
- Auto-calibration corner picker: finish the unfinished step. The empty
.autocal-corner-glyph + dead --top-left/etc modifier classes now render a
mini-screen frame with the matching corner lit, so users map the physical
lit LED to a button at a glance. Hover/focus lights the corner dot.
- Remove dead empty CSS rule for #wizard-rerun-btn.
Final-review blocker: the setup scaffold created the LED output target in the
store but never registered it with the ProcessorManager, so the wizard's
"Start" step 404'd on a fresh setup (target not found) — the lights never
started despite a success screen. Now the scaffold calls
target.register_with_manager(manager) right after create (mirroring the
canonical POST /output-targets route, same ValueError guard), so
start_processing finds the target. Rollback unregisters via
manager.remove_target before deleting the store entity, so a post-registration
failure leaves no half-registered target.
Also from the final review:
- solve corner_indices elements now bounded ge=0 (clear 422 instead of silent
modulo-wrap).
- setup-wizard.ts: reuse tutorials' suppressGettingStartedTour()/TOUR_KEY
instead of a duplicated 'tour_completed' literal; drop a duplicate manual-form
submit listener.
Tests: + adversarial pass over the whole feature (solver/session/scaffold edge
cases) and a scaffold->register->startable regression test. Full suite
2149 passed / 2 skipped; tsc clean; build passes; ruff clean.
A multi-step first-run wizard that takes a brand-new user from install to a
running, calibrated ambient light in ~2 minutes, orchestrating the existing
primitives (no node graph required).
- features/setup-wizard.ts + modals/setup-wizard.html: welcome -> find device
(discovery list + manual add via the canonical, URL-validated POST /devices)
-> pick screen (GET /config/displays) -> scaffold (POST /setup/scaffold) ->
calibrate (embeds the phase-3 auto-calibration flow via mountAutoCalibration
on the scaffolded CSS + device) -> start -> done, with a progress indicator.
- First-run trigger in app.ts (checkAndOpenWizardIfNeeded): on load, if the
onboarding flag is unset AND no output targets exist, the wizard takes over
and the tooltip tour is suppressed; on finish/skip it PUTs the onboarding
flag and sets localStorage tour_completed so neither re-fires. Re-runnable.
- tutorials.ts exposes TOUR_KEY + a takeover hook so the getting-started tour
and the wizard never double-fire.
- Calibrate step always calls unmountAutoCalibration() on exit so the device
is restored. i18n in en/ru/zh (wizard.* keys + common.back).
Final phase of the edge-calibration + first-run-wizard feature. Big Bang final
gate green: tsc --noEmit clean, npm run build passes, full pytest suite
2064 passed / 2 skipped, ruff clean.
The aggregated /api/v1/snapshot poll now emits a `scene_playlists` section
(each playlist with its `is_running` flag) plus a companion `playlist_state`
key carrying the single global cycling state (running playlist, current index/
preset, dwell) — so the HA-coordinator and other low-overhead pollers get
playlist state in the same round trip as scenes/targets, matching the other
entity sections. Gated by the `scene_playlists` include-section like the rest.
Reuses the existing list_scene_playlists handler; snapshot route tests updated.
Reusable, chase-driven calibration flow that solves + saves the linear
CalibrationConfig with a few taps — no LED counting — and works in the
browser on desktop and Android (no Tkinter dependency).
- features/auto-calibration.ts: 5-step flow (start corner -> direction ->
tap-to-mark-corners -> solved preview -> save). Drives the phase-1 session
endpoints (session/position/solve) and persists via PUT /color-strip-
sources/{id}. cornerIndices[0] is anchored to strip index 0 per the solver
contract. unmountAutoCalibration() is the single cleanup gate — the
calibration session is always stopped (device restored) on cancel, modal
close, after save, AND on a mid-flow error, so the strip is never left dark
or stuck.
- Public API mountAutoCalibration({container, cssId, deviceId, onComplete,
onCancel}) for the phase-4 wizard to embed; showAutoCalibration() standalone.
- "Auto-calibrate" entry added to the existing calibration modal; standalone
modal template; app.ts/global.d.ts exports; .autocal-* CSS matching the
ds-section vocabulary; 43 autocal.* i18n keys in en/ru/zh; docs/CALIBRATION.md.
Part of the edge-calibration + first-run-wizard feature (Big Bang; intermediate
phase — full build/suite + test-writer gated at the final phase).
Backend for the first-run wizard (phase 4).
- POST /api/v1/setup/scaffold: given an existing device_id + display_index
(+ optional calibration), wires a working chain via the real validated
store create paths — create-or-reuse capture template -> raw picture
source -> picture color-strip source (calibration or default) -> LED
output target -> returns the ids. Does NOT auto-start. Rolls back every
entity it created (reverse order) on any partial failure, leaving no
orphans; "created" events are deferred until the whole chain succeeds so
a rolled-back scaffold never leaves ghost cards in the UI.
- Requires an existing device_id (no inline device creation) — the wizard
creates the device first via the canonical, URL-validated POST /devices,
so the scaffold can't bypass device validation. display_index is bounded.
- GET/PUT /api/v1/preferences/onboarding: persistent first-run flag
({onboarded, completed_at}) via db.set_setting; server stamps completed_at.
- Both routes AuthRequired. Tests: 25 (scaffold happy/reuse/rollback/
validation + onboarding + calibration round-trip integration). docs/API.md.
Part of the edge-calibration + first-run-wizard feature (Big Bang; intermediate
phase — full build/suite gated at the final phase).
Backend engine for guided LED-chase calibration, driven by the upcoming
auto-calibration UI (phase 3) and first-run wizard (phase 4).
- solve_calibration(): pure function mapping start corner + direction + 4
corner-tap indices to per-edge LED counts, consistent with EDGE_ORDER/
EDGE_REVERSE so it round-trips through build_segments().
- CalibrationChaseMixin.set_calibration_pixel(): light a specific LED index
(+ optional window) on a device, reusing the device_test_mode idle-client
send path.
- CalibrationSession: single-active session with start/position/stop/cancel,
a 60s idle-timeout watchdog, and a concurrency lock so interleaved calls
can't corrupt the stop/restore bookkeeping — start() stops + remembers any
running target on the device and stop/cancel/timeout always restore it
(never leaves the device dark or stuck in chase).
- Routes /api/v1/calibration/{session,session/position,session/stop,
session/cancel,session/state,solve} (all AuthRequired, bounds-validated);
calibration is persisted by reusing the existing PUT /color-strip-sources/
{id} (hot-reloads running streams) rather than a duplicate endpoint.
- Tests: 19 solver pure-logic + 19 route/bounds. docs/API.md updated.
Part of the edge-calibration + first-run-wizard feature (Big Bang; intermediate
phase — full build/suite gated at the final phase).
Snapshot of uncommitted dashboard-customization work (dashboard, customize,
layout, component styles, config defaults, and en/ru/zh locales) committed
as-is to clear the working tree before branching the edge-calibration +
first-run-wizard feature. Not independently verified to build.
Add ordered, timed sequences of scene presets that auto-cycle — activating
each preset and holding it for its dwell duration before advancing.
Backend:
- ScenePlaylist / PlaylistItem models + SQLite store (new scene_playlists table)
- PlaylistEngine: cycles ONE playlist at a time (starting one stops any other),
loop/shuffle, re-reads the playlist each cycle so edits/deletes apply at the
boundary, skips missing presets, guards against busy-loops; reuses the shared
apply_scene_state path used by scene presets and automations
- REST API: CRUD + /start, /stop, /state with scene-preset reference validation
- Constructed in the app lifespan with a bounded stop on shutdown
Frontend:
- New "Playlists" sub-tab in the Automations tab with start/stop controls and a
running indicator; editor modal with ordered scene rows (reorder + per-item
duration), loop/shuffle toggles, and tags
- Live refresh via the playlist_state_changed WebSocket event
- i18n in en/ru/zh
Tests: new unit + API coverage for the store/model, engine (cycling,
single-active exclusivity, missing-preset skip, shuffle, and the
playlist_state_changed event contract), and routes. Full suite green;
ruff and tsc clean.
Sample only a sub-rectangle of the captured frame instead of the whole display,
so a taskbar, game HUD, or letterbox bars don't pollute the border colours — the
first functional gap a reviewer hits (capture was full-display only).
- New pure crop_screen_capture() returns a numpy view (no copy), fast-paths the
full-frame case, and clamps degenerate/out-of-range ROIs to >=1px.
- ROI lives on CalibrationConfig (simple mode) as fractions 0..1 with a has_roi
helper; applied in the picture color-strip stream just before border
extraction, clamping border_width to the cropped size. Additive + backward
compatible (full-frame default, omitted from serialization when unset -> no
migration).
- Round-trips through the calibration schema automatically; frontend adds an
X/Y/Width/Height (%) 'Capture region' group to the calibration editor with
i18n (en/ru/zh).
10 unit tests (crop geometry, view-not-copy, clamping, ROI round-trip, legacy
default); full suite green (1946 passed).
Extend the time-of-day condition from a bare server-local HH:MM window to a real
schedule: pick which weekdays it is active (0=Mon..6=Sun, empty = every day) and
an optional IANA timezone (empty = server local). Closes the parity gap where
even a $5 WLED chip has weekday timers.
- Overnight windows (start > end) count toward the day they START on, so the
after-midnight tail is matched against the previous weekday.
- Timezones are resolved via zoneinfo, cached, and fall back to server-local
with a one-time warning on an invalid name (the ~1Hz tick never log-spams).
- Backward compatible: new fields default to all-days / server-local, so
existing automations are unchanged (no migration).
- Frontend: weekday chips + timezone input on the rule editor, day/timezone in
the rule summary, styles + i18n (en/ru/zh).
10 unit tests (weekday filter, overnight start-day semantics, tz fallback,
round-trip, invalid-day filtering); full suite green (1936 passed).
(Geographic sunrise/sunset triggers are a natural follow-up — the daylight
value source already has the solar math to reuse.)
Seed five curated, read-only post-processing templates so a non-expert gets
instant good-looking output before discovering the filter pipeline. Each is an
opinionated chain of existing filters (auto-crop/saturation/contrast/colour-
temperature/temporal-blur) tuned for a use case (films, games, evening ambience,
low-flicker, crisp cool-white).
Mirrors the built-in-gradient pattern: adds is_builtin to PostprocessingTemplate,
seeds missing looks on store init (idempotent, additive — no migration), and
makes built-ins read-only (update/delete raise -> 400; clone to customise).
Surfaced via the existing template picker + is_builtin in the response/type.
7 unit tests (seeding, idempotency, read-only protection, round-trip); full
suite green (1926 passed). (A runtime intensity slider is a follow-up — it needs
a filter-chain parameterisation layer.)
Add WLED's native realtime UDP protocol (port 21324) as a third output mode for
LED targets, alongside DDP and HTTP. For the device LedGrab drives most, this
brings three user-visible wins DDP lacks:
- Auto-revert: every packet carries a timeout byte, so if the stream stops
(host hiccup/sleep/crash) WLED returns to its preset instead of freezing on
the last frame.
- Correct RGBW whites: the DRGBW variant carries an explicit white channel.
- Lighter on weak Wi-Fi: raw RGB with a 2-byte header.
New WledRealtimeClient auto-selects DRGB (<=490), DRGBW (<=367), or chunked
DNRGB (>490). WLED applies its own per-bus colour order in realtime mode, so we
send plain RGB and the user's colour-order config just works. Protocol 'udp' is
threaded through WLEDConfig/provider/processor and the schema pattern; the
target editor gains a protocol option + badge + i18n (en/ru/zh).
8 unit tests for the packet builder; full suite green (1919 passed).
Cap an addressable strip's estimated current draw to a PSU budget so bright/
white scenes can't brown out an under-spec'd supply (voltage sag -> red/orange
shift, flicker, controller resets) — a classic 'it's broken' first impression.
- New core/processing/power_limit.py: pure current estimate (full white over N
LEDs draws N * mA_per_led) + a (0,1] scale to land a frame on budget.
- Applied in WledTargetProcessor._send_to_device (single choke point, every send
path; scales into a reusable scratch buffer, never mutates shared frames).
- Two per-target fields on LED targets: max_milliamps (0 = unlimited) and
milliamps_per_led (default 55), threaded through model/store/manager/processor/
schema/route with hot-update via update_target_settings. Additive with safe
defaults (no data migration needed; legacy targets read as unlimited).
- Frontend: editor fields + i18n (en/ru/zh) + LedOutputTarget type.
- Tests: 10 unit tests for the estimator/scale; full suite green (1911 passed).
apply_scene_state computed brightness_changed = "brightness" in changed, but
the change dict only ever uses the key "brightness_value_source_id", so the
branch was dead and a running target's brightness source was never live-synced
on scene activation (it only took effect after a restart). Check the correct
key.
The GET/PUT /api/v1/system/mqtt/settings handlers read cfg.mqtt.*, but the
single-broker MQTTConfig block was removed in the multi-broker refactor, so any
call raised AttributeError. Brokers are now first-class MQTTSource entities
managed via the mqtt.py router, and the frontend no longer calls this endpoint.
Remove the dead handlers, the _load_mqtt_settings helper, the now-unused
get_config import, and the orphaned MQTTSettings{Request,Response} schemas.
default_config.yaml shipped api_keys.dev: "development-key-change-in-production"
uncommitted/active, while the surrounding comment claimed it had been removed.
On a non-loopback bind this is a publicly-known credential granting full LAN
access. Restore the documented secure default (empty api_keys -> loopback-only
anonymous, LAN rejected) and leave a commented example instead.
The Android feature-gap assessment and per-feature design docs have served
their purpose — notification, audio, webcam, and the foreground-app automation
condition are all implemented and merged, so no gaps remain to track. The
implementation is documented in the code, commit messages, and git history; the
review docs are now obsolete. No committed files referenced them (only the
local-only plans/ archives, left as point-in-time records).
Make the existing Application automation rule (foreground app -> activate
scene) work on the Android-TV build. A Kotlin ForegroundAppBridge reads the
foreground app via UsageStatsManager and lists launchable apps via LauncherApps;
PlatformDetector bridges it in (ahead of the Windows-only ctypes guard) so the
existing AutomationEngine / ApplicationRule / storage / deactivation modes are
unchanged. New /system/installed-apps + /system/info endpoints feed an app picker
that stores package names (vs process names on desktop); on Android the editor
hides the match-type selector since the foreground app is the only obtainable
signal. PACKAGE_USAGE_STATS is granted via an on-device button + a web-UI banner
(no blanket prompt at capture start); detection degrades gracefully until granted.
Zero new Python/Gradle deps (UsageStatsManager + LauncherApps are in-platform;
matching only string-compares the package name, so no QUERY_ALL_PACKAGES).
assembleDebug + 1897 pytest + ruff + tsc + npm build all green; independent final
review (0 blockers) + security review (no critical issues).
Camera2 + ImageReader (CameraBridge) -> push RGB frames -> AndroidCameraEngine,
reusing the MediaProjection capture-engine selection path so webcams surface as
selectable displays on Android TV. On-demand camera lifecycle, conditional camera
FGS type (granted-only), single-camera ownership lock. Per-phase + final + security
reviews pass; 14 files; new isolated tests; zero new Python/Gradle deps.
assembleDebug + 1883 pytest + ruff all green.
Add on-device webcam capture to the experimental Android-TV build. Desktop
captures webcams via OpenCV (no Chaquopy/Android wheel); this adds a push-based
AndroidCameraEngine that plugs into the same selection path desktop uses
(capture template engine_type="android_camera" + display_index, HAS_OWN_DISPLAYS).
A Kotlin CameraBridge (Camera2) enumerates cameras and opens them on demand —
only while a capture source is active, driven Python->Kotlin via a guarded jclass
singleton (BleBridge pattern) — converts each frame YUV_420_888->RGB, and pushes
RGB bytes into a module-level queue mirroring mediaprojection_engine.py. Cameras
surface as selectable displays like the desktop OpenCV engine; the data-driven
capture-template UI is unchanged. No new Python deps; no new Gradle deps
(Camera2 is in-platform).
Engine: ENGINE_PRIORITY=0 (never auto-selected over MediaProjection=100; explicit
engine_type only). Single-camera ownership is serialized with a lock + ref-count
(same-camera streams attach, different-camera refused, last release stops),
mirroring the desktop CameraEngine guard.
Permission: CAMERA requested at capture-start, gated on FEATURE_CAMERA_ANY so
camera-less TV boxes never prompt; graceful degradation when denied. The service
is promoted with the camera FGS type (+ FOREGROUND_SERVICE_CAMERA) only when
CAMERA is already granted, so backgrounded capture keeps working without risking
a failed startForeground on camera-less boxes (camera can't ride the
MediaProjection token the way audio playback capture does).
Reviewed via multi-agent adversarial pass (13 findings -> 4 fixed: device leak on
session-failure, multi-stream collision, camera FGS type, i18n key; 9 refuted).
Tests: 18 new desktop-CI tests (no device needed); full suite 1883 passed.
Verified: assembleDebug BUILD SUCCESSFUL, ruff clean.
Docs: ANDROID-REVIEW/android-webcam-capture-plan.md (design), updated
android-missing-functionality.md + README feature table + en/ru/zh locales.
NotificationListenerService -> push_notification() -> _AndroidBackend path so OS
notifications drive the existing color-strip LED effects on Android TV at app-name
parity. Per-phase + final + security reviews all pass; 11 new isolated tests; zero
new Python deps. Android assembleDebug pending device/CI verification.
Add an Android backend to os_notification_listener.py so notifications on the
experimental Android-TV build drive the existing NotificationColorStripSource
LED effects (flash/pulse/sweep, per-app colors + sounds) at app-name parity
with the Windows/Linux backends.
A Kotlin NotificationListenerService forwards the posting app's display label
across the Chaquopy JNI boundary into a new push-based _AndroidBackend +
module-level push_notification() receiver; the existing color-strip pipeline,
per-app colors/filters, and history endpoint are reused unchanged.
- Python: _AndroidBackend (probed first), push_notification() receiver,
_LinuxBackend.probe() hardened with is_linux() to exclude Android (which
also reports platform.system() == "Linux").
- Android: LedGrabNotificationListener NLS — serial single-thread executor,
full crash isolation around Python.getInstance(), label-only forwarding
(never notification title/body), ongoing/group-summary/self-package noise
filtering. Manifest service exported + gated by
BIND_NOTIFICATION_LISTENER_SERVICE (no new uses-permission).
- UX: prompt-once notification-access + manual "Grant notification access"
button wired into the D-pad focus chain (computed from visible controls);
en/ru/zh strings.
- Tests: 11 isolated unit tests — module-global + tmp_path history isolation,
push routing contract, callback-exception swallowing, None app-name, and a
desktop-regression lock on backend selection order.
- Docs: README OS-support Android column (notification + audio cells),
ANDROID-REVIEW status flipped to Implemented.
Zero new Python deps; no build.gradle.kts / Chaquopy pip changes.
Enable audio-reactive lighting on the Android-TV build. A push-based
AndroidAudioEngine captures system playback audio via AudioPlaybackCapture
(API 29+), reusing the existing MediaProjection token, and feeds PCM into
the unchanged AudioAnalyzer pipeline. No new Python deps; no Chaquopy/pip
changes (numpy already bundled).
- Python: android_audio_engine.py — module-level queue + configure/
push_samples/shutdown mirroring mediaprojection_engine; AndroidAudioEngine
(priority 100) registered behind a guarded import. push_samples copies and
defensively trims/clamps each block so the analyzer can't crash on
variable-length or non-frame-divisible PCM.
- Kotlin: AudioCapture.kt — AudioRecord + AudioPlaybackCaptureConfiguration,
fixed chunk-size block framing, little-endian float32, mic fallback;
reads back the actual negotiated channel/sample rate. PythonBridge gains
configureAudio/pushAudio/shutdownAudio with a cached module handle.
- Wiring: CaptureService starts/stops AudioCapture in the MediaProjection
path (gated on API>=29 + RECORD_AUDIO + live projection); MainActivity
requests RECORD_AUDIO; manifest declares it. Degrades gracefully when
denied; root path stays audio-less by design.
- Tests: 13 desktop-CI tests incl. an over-length/non-divisible regression
guard that exercises the full read_chunk -> AudioAnalyzer.analyze path.
Add an opt-out `normalize` flag to the four magnitude value sources
(ha_entity, http, system_metrics, game_event). get_value() stays in
[0,1] for every source (the normalized scalar-bus invariant), so all
existing consumers — brightness sinks, gradient_map, template `name`
bindings, and color-strip bindable floats — are unaffected.
- normalize=False is a clamp-passthrough: skip the min/max rescale and
clamp the raw reading into [0,1] (for sources already reporting a
0..1 fraction). The un-normalized magnitude stays available via
get_raw_value() / template raw[name] / automations.
- Add get_raw_value() + a raw channel to GameEventValueStream (it had
none). game_event's flag is model/stream-level only (no CRUD schema).
- Finite-safe clamp01() util; harden the composite-layer brightness
multiply (latent negative-wrap / >=1 skip) with it.
- Preview WebSocket: tolerate non-numeric raw, generalize raw-range.
- Frontend: settings-toggle slider per HA/system_metrics/http editor
with min/max grey-out; toggle hidden for fixed-mapping percent
metrics. en/ru/zh locale keys.
- Additive optional field (default True) — JSON round-trip, no migration.
Tests: store create/update round-trip, clamp-passthrough, live
normalize flip, game_event raw channel + build_stream forwarding,
and finite-safe clamp01.
A new `template` value source evaluates a hardened, sandboxed Jinja
expression over the live values of other value sources — the system's
first float combinator.
Backend:
- Shared engine (utils/template_expr.py): ImmutableSandboxedEnvironment with
filters/tests and auto-injected globals stripped; only min/max/abs/round/
clamp exposed; rejects **, string/collection-literal repetition, attribute
access and non-global calls; NaN/inf-safe result coercion.
- TemplateValueSource model + TemplateValueStream runtime: compile-once,
primitives-only eval context, raw[name] exposure, eval_interval throttle,
ref-counted input acquire/release, rename-safe hot-update.
- Validation: unbound-variable + reserved-name rejection, reference
cycle/depth guards (depth-only at create, full cycle at update), runtime
acquire() depth backstop, and delete referential-integrity.
- API: Create/Update/Response schemas + discriminated unions, _RESPONSE_MAP,
and an advisory POST /value-sources/validate-template endpoint.
- Demo seed: a static source plus a template combinator example.
Frontend:
- Editor modal section: repeatable inputs list (EntitySelect rows), a
zero-dependency Jinja syntax highlighter, a hints/reference panel, and a
debounced live validator that gates Save (stale-response-safe).
- Graph editor: read-only template node with one edge per input.
- i18n (en/ru/zh), icon, and card rendering.
Tests: engine, stream, factory/cycle, validate endpoint, and demo seed.
README: add free & open-source (MIT) framing; add a Platforms table (Windows/Linux/macOS/Docker supported, Android-TV build marked experimental, scrcpy phone-capture retained); expand the LED device list to ~20 protocols; correct storage to SQLite, the auth model, and resource names (Output Targets, Automations); add a prebuilt-download section; remove the Architecture and Acknowledgments sections; embed dashboard, channels, live-preview, and device-picker screenshots.
docs/API.md: full rewrite from the live route modules — all 253 endpoints across 34 modules, grouped by resource with REST + WebSocket tables, accurate auth model and WS handshake, worked examples, and sensitive-endpoint markers. Replaces the stale v0.1.0 stub.
server/CLAUDE.md: storage is now SQLite (BaseSqliteStore / ledgrab.db / LEDGRAB_DATA_DIR / data_migrations.py); fix the auth description (loopback anonymous, LAN rejected with 401 — not all endpoints open); router registration happens in api/__init__.py.