Repository navigation
Anton petrenko solution - #1
AntoniPetrenko wants to merge 20 commits into
Conversation
Single-origin Fastify 5 substrate for mytv: /api HTTP routes, /ws WebSocket mount, and SPA hosting with index.html fallback, all on one port. Structured Pino logging strips AccessToken/authorization at any nesting depth via a recursive formatter (pino's own redact only supports single-level wildcards). Uniform JSON error envelope (HttpError + errorHandler) never leaks stacks. Front-end gets a relative-path apiClient plus a Vite dev proxy for /api and /ws. - back-end: app.ts factory, logger.ts, errors.ts, plugins/static-spa.ts, routes/health.ts, new src/index.ts entrypoint (replaces server.ts stub) - back-end tests (6, via npm run back:test): health envelope, SPA fallback, 404 envelope, redactor (top-level + nested AccessToken), WebSocket ping/pong - front-end: src/api/client.ts, vite.config.ts dev proxy, temporary health-check smoke test on App mount - docs: back-end/README.md, .env.example SERVE_SPA, current-state.md - fixed a real bug found during implementation: Fastify's loggerInstance option bypasses its default req/res serializers, so raw circular request objects reached the log formatter and crashed on the first request; switched to passing plain logger options instead and made the redaction formatter cycle-safe and plain-object-only - dropped pino-pretty's in-process transport (hung via worker threads in this sandbox) in favor of piping back:dev through pino-pretty Reviewed by the code-reviewer subagent (fresh context): PASS, with three low-severity non-blocking findings noted in current-state.md for future follow-up. Archived via opsx:archive; specs synced to openspec/specs/platform-foundation/spec.md. Co-Authored-By: Claude <noreply@anthropic.com>
- back-end/src/mdns.ts: startMdns(port) publishes _http._tcp on mytv.local (TXT path=/) via bonjour-service; 5s os.networkInterfaces() poll re- advertises on address-set delta; EADDRINUSE keeps HTTP up and flips handle to state:"error" with an Avahi/mDNSResponder hint. - back-end/src/app.ts: onReady starts mDNS, onClose stops it with a ~500ms grace period so the goodbye packet leaves. Handle exposed via app.mdns. - back-end/src/routes/health.ts: GET /api/health returns mdns state, hostname, address, and optional error for browser-tab diagnosis. - Tests (11 pass): unit + integration coverage of the advertising path, the interface-set change path (stubbed os.networkInterfaces + triggerInterfaceCheck), no-op on unchanged addresses, and the degraded-not-crashed EADDRINUSE path. Existing app/websocket tests updated to inject a stub client / disable mDNS so tests stay hermetic. - Docs: back-end/README.md gains an mDNS section (env vars, dns-sd / avahi-browse repro, port-5353 conflict guidance). - Delta spec synced to openspec/specs/mdns-advertisement/spec.md and the change archived under openspec/changes/archive/2026-07-04-mdns- advertisement/. Co-Authored-By: Claude <noreply@anthropic.com>
Follow-up to mdns-advertisement (C2): - mdns.ts: pass `probe: false` on publish. bonjour-service's default probe path prints `console.log(new Error(...))` outside our Pino stream on any duplicate response and silently stops the service without notifying us, leaving handle.state stuck at "advertising" while nothing is broadcast. Design.md scopes the MVP to one mytv per LAN, so straight-announce is fine and overwrites stale cached records from previous dev sessions. - index.ts: SIGINT/SIGTERM now route into app.close() so Fastify's onClose hook fires and mDNS gets its ~500ms grace period to emit a goodbye packet before the process exits. This closes the "advertisement withdraws on shutdown" spec scenario for real signal-driven shutdowns and is also the platform-foundation reviewer follow-up flagged before upnp-tv-discovery lands. Verified: 11/11 tests still pass, live SIGTERM against `node dist/index.js` logs `received shutdown signal; closing server` and exits with code 0; dns-sd -B shows mytv without the earlier "Service name is already in use" console noise. Co-Authored-By: Claude <noreply@anthropic.com>
Aligns the back-end default port with `http://mytv.local/` (`FR-HOSTING-02`) so operators no longer have to override `PORT=80` on every deploy while the docs already print the low-port URL. - back-end/src/index.ts + back-end/src/app.ts: fallback literal flipped from `?? 3000` to `?? 80` in both call sites (server bind and mDNS SRV advertisement stay in sync). - openspec/specs/platform-foundation/spec.md: "Single-origin Fastify server" now pins the default port at 80, cites FR-HOSTING-02, and gains two scenarios (`Default port is 80`, `PORT env var overrides the default`). - back-end/README.md: env-var table shows `PORT` default `80`, "Running on port 80 (Orange Pi)" renamed to "Running on the default port 80 (Linux)" (setcap now unlocks the default, not an opt-in), and a new "Development on macOS / non-privileged shells" section documents `PORT=3000` as the standard opt-out. `.env.example` kept at `PORT=3000` with a header comment clarifying it's the dev override, not the app default. - docs/current-state.md: session-log entry prepended per AGENTS.md. - Verification: `npm run back:build` clean, `npm run back:test` 11/11. Live smoke: `PORT` unset -> bound `:80`, `curl /api/health` -> 200; `PORT=3000` -> bound `:3000`, both paths advertised the matching SRV port. - Independent code-reviewer verdict: PASS. Change archived at openspec/changes/archive/2026-07-04-default-port-80/. Co-Authored-By: Claude <noreply@anthropic.com>
Adds the first back-end capability that talks to real TVs on the LAN.
Continuous SSDP-based discovery (30 s M-SEARCH + async NOTIFY), UDN-keyed
registry, and a `/api/devices` + WebSocket `devices` topic that the
device-list UI will consume in the next capability (C4).
- back-end/src/discovery/ssdp.ts: SSDP transport combining node-ssdp
Client (M-SEARCH responses on an ephemeral port) with a raw dgram
socket bound to 239.255.255.250:1900 for asynchronous NOTIFY. Both
paths emit a unified `hit` event. Design-intent-preserving: node-ssdp
Client alone binds an ephemeral port and cannot receive multicast
NOTIFYs.
- back-end/src/discovery/description.ts: undici + 3 s AbortController
timeout, fast-xml-parser, Samsung filter (manufacturer OR model-prefix
regex). `isSamsungTv` is a pure predicate — logger-free by design.
- back-end/src/discovery/registry.ts: Map<UDN, Device>. Upsert emits
added/updated; markOfflineOlderThan flips status and emits offline
once per transition. `removed` on the event union but not emitted —
MVP keeps offline devices in the registry per FR-DISCOVERY-04.
- back-end/src/discovery/index.ts: orchestrator. p-queue concurrency 5,
30 s M-SEARCH timer + 30 s offline sweep (60 s threshold), initial
M-SEARCH fired immediately for the "within 2 s of app.listen()"
scenario. IP/port derived from LOCATION URL, not XML.
- back-end/src/routes/devices.ts: GET /api/devices -> registry.snapshot().
- back-end/src/ws/broker.ts: subscribes to registry events, fans out
{ topic: "devices", event, device } to WS clients; sends a snapshot
frame on every new connection so late joiners get the full state
without a separate HTTP round-trip.
- back-end/src/app.ts: `discovery?: DiscoveryAppOptions` mirrors the
existing `mdns?:` block. Decorates the Fastify scope with
app.discovery = { registry, broker, handle } so routes and tests
reach the registry without a module singleton. DISCOVERY_ENABLED=0
env opt-out for test/dev environments where port 1900 is contested.
- Tests: 4 new test files (description, registry, discovery
integration). Integration test uses a StubSsdpTransport +
fixture-mapped stubFetchDescription so no real socket is touched;
covers the Samsung filter, WebSocket snapshot + delta flow, and the
no-AccessToken assertion.
- Pre-existing fix: back-end/package.json test script used an unquoted
'**' glob that npm/sh did not expand recursively, so
`npm run back:test` was silently skipping the four new discovery
test files (was running 12 of 23). Quoted the glob so the full suite
runs under the standard invocation.
- Updated pre-existing back-end/src/websocket.test.ts to pass
`discovery: { enabled: false }` — the new /ws contract sends a
snapshot on connect, which the ping/pong test doesn't care about.
Verification: `npm run back:build` clean; `npm run back:test` 23/23 green
(11 previous + 12 new). No live-LAN repro run — no Samsung TV on hand —
so the fixture-driven `discovery.test.ts` integration test is the
documented manual gate.
Independent code-reviewer verdict: PASS. Three informational findings
noted (Device.port is the UPnP description port not the JSON-RPC control
port — worth flagging for tv-connection-lifecycle; ST choice per
design.md D2; unbounded p-queue depth as a future hardening) — all
non-blocking and appropriate for later capabilities.
Change archived at openspec/changes/archive/2026-07-04-upnp-tv-discovery/;
main spec synced to openspec/specs/upnp-tv-discovery/spec.md.
Co-Authored-By: Claude <noreply@anthropic.com>
First user-visible SPA screen. Swaps the SAMPLE_DEVICES stub in App.tsx
for a live subscription to the back-end registry, built entirely from
Orbit DS primitives + tokens.
- front-end/src/data/types.ts: Device shape matches the wire contract
emitted by upnp-tv-discovery. No AccessToken field on the type — SPA
can't accidentally read one.
- front-end/src/data/useDevices.ts: WebSocket-first hook that hydrates
from `{ topic: "devices", event: "snapshot" }`, applies added /
updated / removed / offline deltas in place, falls back to
GET /api/devices if no snapshot arrives within 500 ms, and reconnects
with exponential backoff (1 s → 30 s cap) on WS close. A late WS
snapshot always wins over a REST hydration. `createWebSocket` and
`fetchDevices` are injectable so tests don't touch a real socket.
- front-end/src/screens/DeviceListScreen.tsx: overline "LOCAL NETWORK",
H1 "Your TVs", one DeviceCard per device (model prop only forwarded
when non-null so `model: null` doesn't leak "null" into the DOM),
disabled primary "Add a TV" button (holds the one-accent-per-screen
slot without giving the user a broken button — manual-add FR is still
a docs/capabilities.md gap), and a Card variant="inset" empty state
with the tv_off Material Symbols glyph and the copy from the spec.
- front-end/src/App.tsx: shrunk to a thin composer — useDevices() into
DeviceListScreen, RemoteScreen unchanged with its stubs. Removed the
temporary apiClient.get('/api/health') smoke effect; the app now
consumes apiClient for real through the hook.
- front-end/src/ds.d.ts: added Card shim so the empty state compiles.
Test infrastructure wired up (previously `npm run front:test` was
undefined — a gate failure per AGENTS.md): added vitest + happy-dom +
@testing-library/react, vite.config.ts uses vitest/config's defineConfig
and declares `test.environment = 'happy-dom'` + `globals: true`.
- useDevices.test.ts (4 tests): WS snapshot → added → offline flow,
REST fallback when snapshot is slow, late-WS snapshot overriding a
REST hydration, `close` flipping the `offline` flag.
- DeviceListScreen.test.tsx (3 tests): empty state renders the copy +
tv_off glyph, two devices render two cards, `model: null` leaks no
"null"/"undefined" text.
Verification: `npm run front:build` clean (204 KB / 64 KB gzip bundle);
`npm run front:test` 7/7 green; `npm run back:test` still 23/23 green.
Live end-to-end repro against real hardware — implementer's LAN has a
Samsung UE40KU6000 at 20.20.20.144. `PORT=3000 npm run back:dev` +
`VITE_BACK_PORT=3000 npm run front:dev`; curl of the Vite dev server
returned the SPA HTML and its /api/devices proxy returned the real TV
row end-to-end. Dark mode + neumorphic-surface eyeball deferred to the
user (no graphical browser in this CLI session — no dark-mode fork was
introduced, so the DS `[data-theme="dark"]` inheritance should work
unchanged).
Independent code-reviewer verdict: PASS. Three low-severity findings
noted, all non-blocking: (1) empty state renders during initial loading
before the first snapshot (loading flag exposed but not consumed by
DeviceListScreen), (2) DS DeviceCard shows a stray leading middot when
model is undefined — DS-level cosmetic issue, (3) DeviceListScreen uses
inline style objects for layout dimensions consistent with the pre-
existing RemoteScreen pattern.
Change archived at openspec/changes/archive/2026-07-04-device-list-ui/;
main spec synced to openspec/specs/device-list-ui/spec.md.
Co-Authored-By: Claude <noreply@anthropic.com>
The pivot capability: per-TV IP Control session state machine keyed by
UDN. Every downstream command capability (C6/C7/C8) plugs into this.
Back-end (new under back-end/src/tv/):
- types.ts: SessionState internal union + ClientSessionState (collapses
Reconnecting -> Connecting for client visibility) with a single
toClientSessionState() converter.
- errors.ts: TvError class + mapRpcErrorCode (-32601 -> TvNotSupported,
-32602 -> TvInvalidOp, -32000..-32099 -> TvFailed) + mapTransportError
(any socket / TLS / timeout -> TvNotReachable). Fixed HTTP status per
domain code. Raw -32xxx codes never appear in HTTP bodies.
- token-store.ts: XDG-aware path, atomic write via .tmp sibling + chmod
0600 + rename so no reader ever sees a world-readable copy.
- jsonrpc.ts: thin JSON-RPC 2.0 client on undici.Pool per TV origin
(keep-alive 30 s idle / 300 s max, pipelining 1, rejectUnauthorized
false — Samsung TVs self-sign on the LAN). Every request/response
logs rpcId for correlation; AccessToken stripped by the redactor.
Injectable JsonRpcTransport interface so tests never touch a socket.
- session.ts: per-TV actor. One transport + one p-queue (concurrency 1
— spec forbids batching) + state machine per design D1. tryOpen()
verifies the transport with a heartbeat call before flipping to
Connected so a broken TV can't infinite-loop the reconnect budget.
Backoff [200, 500, 1000, 2000, 5000] ms, cap 5 -> Disconnected.
Heartbeat = getSystemInfo every 15 s.
- manager.ts: Map<UDN, Session>. Primes deviceIndex from
registry.snapshot() + subscribes to added/updated/offline/removed.
offline -> markOffline (Offline); updated on Offline UDN ->
markBackOnline (Disconnected, no auto-reconnect per spec).
HTTP + WebSocket:
- routes/sessions.ts: GET/POST /api/devices/:udn/{session,connect,
disconnect}. Reconnecting never leaves the back-end.
- ws/broker.ts extended: 'session' event on the devices topic; the
snapshot payload now carries per-device session state so a client
reconnecting mid-session sees state without a separate HTTP call.
- app.ts wired via sessions?: options; SESSIONS_ENABLED=0 env opt-out.
Front-end:
- data/useDeviceSession.ts: subscribes to devices/session events for a
UDN + one-off GET on mount. All deps injectable.
- App.tsx RemoteScreen: useEffect connect() on mount; back-button
disconnect() before navigation; Badge reflects session via
sessionToBadgeStatus (Connected -> online, Connecting -> connecting,
else offline); command controls disabled while state !== 'Connected'
(native disabled prop on IconButton, opacity + pointer-events wrapper
on DPad/Slider/AppShortcut since the DS has no disabled prop there).
Tests (npm run back:test: 40/40; npm run front:test: 7/7):
- errors.test.ts: raw code mapping table + HTTP status matrix.
- token-store.test.ts: round-trip through 0600, missing-file case.
- session.test.ts (5 tests): pair-then-connect, preloaded-token skip
pairing, heartbeat-fail -> Reconnecting -> Connected on recovery,
cap-reached -> Disconnected, markOffline <-> markBackOnline.
- logger.test.ts extended: AccessToken redacted inside a JSON-RPC
params payload; rpcId + method survive for correlation.
- session-integration.test.ts: full app via createApp with stub SSDP +
stub JSON-RPC transport. GET /session before connect, POST /connect
drives Connected, WS receives Connecting + Connected (Reconnecting
MUST NOT leak), heartbeat-fail-and-stay-broken exhausts cap into
Disconnected, no HTTP response body carries AccessToken.
Verification: builds clean, tests green. Live smoke against real
Samsung UE40KU6000 walked Disconnected -> Connecting -> tries pairing
-> transport timeout at the UPnP description port (9197, not the
JSON-RPC control port 1515/1516) -> mapped to TvNotReachable ->
Disconnected. HTTP response carried only { state: "Disconnected" }; no
raw code leaked; the "MVP pairing" log line and rpcId correlation both
present. Reaching real Connected on a stock TV is blocked on (a) the
port derivation upstream in upnp-tv-discovery (documented gap from the
previous cycle's review) and (b) the getAccessToken pairing method
being protocol-speculative — pre-provision via MYTV_TOKEN_<UDN> env
override for now.
Independent code-reviewer verdict: PASS. Two low-severity edge-case
races on session.ts around user connect()/disconnect() during
Reconnecting, and one already-documented upstream port issue. All spec
scenarios covered.
Change archived at
openspec/changes/archive/2026-07-04-tv-connection-lifecycle/;
main spec synced to openspec/specs/tv-connection-lifecycle/spec.md.
Co-Authored-By: Claude <noreply@anthropic.com>
tv-connection-lifecycle (C5) shipped assuming Samsung's hotel-TV IP
Control protocol (JSON-RPC 2.0 over HTTPS 1515/1516). Live-hardware
smoke against a consumer Samsung UE40KU6000 showed that protocol
doesn't apply to consumer Tizen TVs (2016+) — they speak the Samsung
Smart View WebSocket API on port 8001 instead. This replaces the
transport and pairing flow while keeping the state machine, HTTP/WS
surface, and domain error union from C5 intact.
- back-end/src/tv/jsonrpc.ts rewritten to a ws.WebSocket client with
connectHandshake() (name-based pairing, on-screen TV accept) and
ping() (heartbeat) added to the transport interface
- mapWsError added to errors.ts per the domain error mapping table
(close code 1002 -> TvFailed, unauthorized -> TvNotSupported, etc.)
- manager.ts: hardcoded SMART_VIEW_PORT = 8001 with a
MYTV_CONTROL_PORT_<UDN> override; never trusts the UPnP description
port for the control connection
- session.ts: tryOpen()/heartbeat rewritten around pairing + ping;
handshakeTimeoutMs/pingTimeoutMs now configurable via SessionOptions
- logger.ts: redactor extended to strip `token` (any depth) and
`token=` query fragments from logged URLs
- discovery: SSDP pre-fetch Samsung-signature filter (shouldFetch) and
IP-based registry dedup so one physical TV shows as one device row
Bugs found and fixed along the way:
- shouldFetch treated empty-string usn/st as a non-empty (whitespace)
haystack, skipping the intended "no signal -> fall through" path
- ping() registered a 'pong' listener before calling socket.ping(),
which throws synchronously on an already-closed socket, leaking the
listener and its timeout timer on every such call
- mapWsError's close-code branch didn't special-case 1002 (protocol
error -> TvFailed) as the spec's mapping table requires, mapping it
to TvNotReachable instead (found by the code-reviewer subagent)
Tests: discovery/index.test.ts and tv/jsonrpc.test.ts are new (real
ws.WebSocketServer, not hand-rolled stubs, per design.md's test
strategy); session.test.ts and session-integration.test.ts rewritten
around the new transport. 66 back-end + 7 front-end tests pass.
Docs synced to the pivot: back-end/README.md, docs/capabilities.md,
AGENTS.md (samsung-ip-control-protocol re-scoped as historical-only;
samsung-smart-view-protocol stub pointer added).
Reviewed by the code-reviewer subagent (fresh context): PASS after
fixing the mapWsError finding above. Not done: live-hardware repro
against a real TV (task 5.2) — no physical hardware reachable from
this environment; the fake-TV WebSocketServer integration test is the
documented stand-in per the change's own escape hatch (task 5.3).
Archived via opsx:archive; specs synced to
openspec/specs/{tv-connection-lifecycle,upnp-tv-discovery}/spec.md.
Co-Authored-By: Claude <noreply@anthropic.com>
- Front-end: apiClient.post no longer sends Content-Type: application/json when there is no request body — the /connect and /disconnect calls are body-less and Fastify's built-in JSON parser rejected the combination with 400. - Back-end: override the JSON content-type parser to treat an empty payload as `undefined` so any client (correctly or not) sending the header without a body is accepted instead of 400'd. Co-Authored-By: Claude <noreply@anthropic.com>
- POST /api/devices/:udn/key: enumerated SamsungKeyCode body,
dispatches an ms.remote.control Smart View WebSocket frame
through the per-TV session queue owned by C5. 204 on success,
400 on unknown key (Fastify enum schema), 409
SessionNotConnected before the session reaches Connected.
- back-end/src/tv/keys.ts: 9-key MVP union (arrows + ENTER +
RETURN + HOME + MENU + POWER) and keyControlParams(key) that
wraps a key in the Smart View { Cmd:'Click', DataOfCmd, Option,
TypeOfRemote } envelope from smart-view-ws-transport D1.
- errorHandler now maps TvError to its domain HTTP envelope so a
socket dropping between the state check and the queued call
surfaces as e.g. TvNotReachable/502, not code:"internal".
- Front-end: useSendKey(udn) hook + RemoteScreen extracted into
its own file. D-pad, back/home/menu, and power buttons are
wired to real key sends and rendered disabled unless the
session state is Connected. DS DPad gained a `disabled` prop
that propagates to every underlying IconButton (extended per
frontend-design-check; no inline styles on the primitive).
- Tests: 3 keyControlParams unit tests + 5 route integration
tests (204, 400, 409, rapid-fire ordering, TvError-to-domain
envelope). 2 RemoteScreen component tests (Connecting disables
every command control; Connected D-pad up posts KEY_UP). All
73→74 back-end and 7→9 front-end tests pass.
- Spec-drift fix caught pre-implementation: change proposal
predated the smart-view-ws-transport pivot and referenced the
hotel-TV JSON-RPC `remoteKeyControl` method + Postman
collection. design.md D2, tasks.md 1.1/1.2, and spec.md were
rewritten to point at the archived smart-view-ws-transport
design as the authoritative envelope source, per AGENTS.md.
Task 5.2 (real-hardware repro) is deferred — no LAN/TV reachable
in this environment. Stub-driven integration suite stands in;
follow-up flagged in docs/current-state.md.
Co-Authored-By: Claude <noreply@anthropic.com>
- POST /api/devices/:udn/volume/delta: Fastify schema validates
the body as an integer in [-25, 25], non-zero (rejects with
code:"validation" via the errorHandler's `error.validation`
branch). Dispatches abs(delta) KEY_VOLUP/KEY_VOLDOWN frames as
Smart View ms.remote.control envelopes through the per-TV FIFO
queue owned by tv-connection-lifecycle.
- POST /api/devices/:udn/mute: enqueues one KEY_MUTE frame, flips
the server's optimistic muted tracker, emits a `changed` event
that the WebSocket broker fans out as
{ topic:"devices", event:"volume", udn, level, muted }.
- GET /api/devices/:udn/volume: always 200 with { level:null,
muted }. Read-only — does not allocate a tracker entry for
unknown UDNs (reviewer-flagged memory footgun avoided).
- back-end/src/tv/volume.ts: per-UDN optimistic tracker + module
handling delta / toggleMute / snapshot / `changed` event.
Wires to the SessionManager `state` event so Connected fires an
initial snapshot and Disconnected/Offline clears the tracker.
- back-end/src/tv/keys.ts: extended SamsungKeyCode union with
KEY_VOLUP/KEY_VOLDOWN/KEY_MUTE. C6 SAMSUNG_KEY_CODES array
intentionally unchanged so /api/devices/:udn/key still rejects
volume keys per C6's own Non-Goals boundary.
- WebSocket broker gained a `volume` event and per-UDN volume in
the connection snapshot (mirroring the C5 `session` pattern).
- Front-end useVolume(udn) hook: WS subscription + initial GET,
optimistic mute toggle with rollback on failure, no setLevel
(level is unknowable over Smart View).
- RemoteScreen wired: slider drag commit fires delta from
position diff; mute button drives useVolume.toggleMute.
- DS Slider gained onCommit + disabled props (extended per
frontend-design-check; drag-end handling stays in the
primitive, not inlined in the app).
- Tests: 8 volume module unit tests + 7 route integration tests
(GET default, GET on unknown UDN doesn't allocate, delta 3,
delta 0/100 → 400 validation, delta while Disconnected → 409,
mute + WS event, mute while Disconnected → 409). 5 new
RemoteScreen component tests. 88→90 back-end, 9→14 front-end,
all passing.
Spec-drift caught pre-implementation: change proposal was
written pre-Smart-View-pivot and referenced Samsung's hotel-TV
JSON-RPC directVolumeControl for absolute set + level read.
Smart View WebSocket is remote-key-only — no absolute-set, no
level read, no mute-state read. Rewrote design.md / spec.md /
tasks.md to descope: relative delta + mute only; level is
always null; muted is optimistic. UPnP RenderingControl or
SmartThings would be a separate future capability if actual
reads/absolute-set matter.
Task 5.2 (real-hardware repro) and 5.3 (light/dark eyeball) are
honestly deferred — no LAN/TV reachable. Stub-driven integration
suite stands in; follow-up flagged in docs/current-state.md.
Co-Authored-By: Claude <noreply@anthropic.com>
- GET /api/devices/:udn/inputs: returns the static
INPUT_CATALOGUE regardless of session state. No cache, no
refresh — Smart View WebSocket cannot enumerate the TV's real
inputs and the catalogue is a constant per install.
- POST /api/devices/:udn/input: Fastify enum schema validates
{ key } against INPUT_KEYS (unknown → 400 validation via the
errorHandler `error.validation` branch, same technique C7
landed). Cross-capability keys like KEY_UP or KEY_VOLUP are
rejected too — /input only accepts SamsungInputKey. On
Connected + valid, enqueues one ms.remote.control frame with
the chosen key through the C5 per-TV FIFO. On non-Connected,
409 SessionNotConnected. Emits a `switched` event that the
broker fans out as { topic:"devices", event:"input", udn, key }.
- back-end/src/tv/inputs.ts: SamsungInputKey (subset of
SamsungKeyCode), INPUT_CATALOGUE static constant (KEY_SOURCE +
KEY_HDMI1..4 + KEY_HDMI + KEY_TV + KEY_AV1 + KEY_COMPONENT1),
createInputsModule(sessionManager) returning { list, switch,
on/off }.
- back-end/src/tv/keys.ts: extended SamsungKeyCode with the
input keys. SAMSUNG_KEY_CODES (C6 route) and VOLUME_KEY_CODES
(C7 route) unchanged — capability boundaries preserved.
- WebSocket broker gained an `input` event; broker/app wiring
mirrors C7's volume wiring.
- Front-end: useInputs(udn) hook returning { inputs, setInput }.
No activeId. Initial GET /inputs on mount (catches
back/front catalogue drift).
- InputsModal composed from DS Modal + rows of a new DS ListRow
primitive. RemoteScreen gains an IconButton icon="input" next
to Menu; useEffect auto-closes the modal when session state
leaves Connected.
- DS ListRow.{jsx,d.ts} (new): neomorphic row primitive per
frontend-design-check. Added a ListRow shim to
front-end/src/ds.d.ts — the DS's own .d.ts files only export
props interfaces, so the shim declares ComponentType so tsc
resolves the import.
- Tests: 3 catalogue unit tests + 5 route integration tests
(GET returns catalogue with no activeId, POST while
Disconnected → 409, POST after connect sends the KEY_HDMI1
frame + emits WS input event, unknown key → 400 validation,
cross-capability KEY_UP → 400 validation). 3 InputsModal
component tests + 1 new RemoteScreen integration test
(opening the modal then flipping state to Connecting
auto-closes). 90→98 back-end, 14→18 front-end, all passing.
Spec-drift caught pre-implementation: change proposal was
written pre-Smart-View-pivot and required Samsung's hotel-TV
JSON-RPC directSourceControl for listing available inputs,
reading active input, and setting a specific input. Smart View
WebSocket is remote-key-only — none of that is exposed.
Rewrote design.md / spec.md / tasks.md to descope: static
curated catalogue, no active-input marker, no refresh
endpoint. UPnP AVTransport or SmartThings would be a separate
future capability if actual reads / real listing matter.
Task 5.2 (real-hardware repro tapping each input) and 5.3
(dark-mode eyeball) are honestly deferred — no LAN/TV
reachable. Stub-driven integration suite covers the wire.
Phase 3 is complete. Only C9 error-surfacing remains.
Co-Authored-By: Claude <noreply@anthropic.com>
- DS Toast primitive: docs/orbit-tv-remote-design-system/
components/feedback/Toast.{jsx,d.ts,prompt.md}. Neomorphic
raised surface with tone-tinted left band (uses the same
--connecting / --offline / --online tokens Badge uses),
Material Symbols glyph, close IconButton. Tokens throughout;
no hex.
- useToast context + ToastHost: front-end/src/ui/useToast.ts
(React.createElement provider, reducer with push / dismiss /
pause / resume, 5s auto-dismiss timer, 500ms dedup, cap 3
with oldest-dropped overflow, injectable now() + nextId() for
deterministic tests) and ToastHost.tsx (fixed bottom-right
stack, pointer-events wrangling so the container doesn't
swallow app clicks). main.tsx wraps App in
ToastProvider → ErrorBoundary → App + ToastHost.
- messageFor mapper: front-end/src/errors/messages.ts. Pure
function mapping every ApiError.code to { tone, message } per
design.md D3. One place to change copy; unit-testable.
- Hook wiring: useSendKey, useVolume (delta + toggleMute),
useInputs.setInput, useDeviceSession.connect catch ApiError,
push(messageFor(err)), console.warn with correlationId, and
re-throw. useDeviceSession.disconnect deliberately does NOT
toast (documented — noisy UX during back-navigation).
- ErrorBoundary: front-end/src/ui/ErrorBoundary.tsx. Class-
based backstop. Fallback composed from DS Card + Material
Symbols error_outline + "Try again" Button that reloads.
- useWsHealth + WsHealthChip: dedicated /ws connection with
exponential-backoff reconnect (1s doubling to 30s cap);
transitions to 'offline' after 5 failed attempts. Chip in
both DeviceListScreen and RemoteScreen headers.
- Tests: messages.test.ts (9 tests including the "no raw code"
sweep across every code), ToastHost.test.tsx (4 tests: push
renders, cap 3, dedup, hover pause + resume with remaining
budget), useSendKey.test.tsx (1 end-to-end integration test
through the real ToastProvider). 18→32 front-end tests.
- Reviewer-driven fixes in-cycle: (1) useWsHealth off-by-one
(attempts > giveUpAfter → >=). (2) Toast.jsx +
ErrorBoundary.tsx referenced --text-body-md (which doesn't
exist in DS typography.css — real tokens are --text-body,
--text-body-sm, --text-body-lg); fixed to --text-body. Same
bug also present in ListRow.jsx from C8 — fixed
transparently. (3) Reworded the "Hover pauses timer" spec
scenario to match the "resume with remaining budget"
semantics the test already codifies. (4) Updated design.md
D5 to describe the dedicated-WS strategy the code actually
implements.
- DESIGN.md: added Toast + ListRow rows to the Component
surface table.
Task 5.5 (manual QA unplug-the-TV) is honestly deferred — no
LAN/TV reachable. Stub-driven ToastHost + useSendKey tests
cover the equivalent wire paths. Also deferred: task 2.3's
narrow-viewport pin-to-bottom-edge media query for the toast
stack (DS's inline-style pattern doesn't take @media; needs a
follow-up to add either a scoped <style> inside Toast.jsx or a
CSS class in the DS styles.css). No spec scenario depends on
it.
MVP backlog is now empty. All nine capabilities C1–C9 shipped.
Co-Authored-By: Claude <noreply@anthropic.com>
- Introduce `RotaryKnob` DS primitive (`docs/orbit-tv-remote-design-system/components/controls/RotaryKnob.{jsx,d.ts,prompt.md}`): raised neumorphic housing + inset centre well hosting a caller-supplied action, `--accent` rim indicator following the pointer, focus-visible outline; per-detent (15°) `onStep('up' | 'down')` fired from PointerEvent capture + keyboard (Arrow/Page keys).
- Wire it into `RemoteScreen`: mute `IconButton` moves into the knob's centre slot, `sendDelta(±1)` fires per detent, drops `sliderPosition` / `lastCommittedRef` / `useVolume().level` reads entirely. Back-end wire contract untouched.
- Sync `openspec/specs/volume-control/spec.md`: replaces the slider requirement with the rotary-knob requirement and its 12 scenarios; archives change under `openspec/changes/archive/2026-07-06-volume-rotary-knob/`.
- Update DS manifest, hand-patch the `_ds_bundle.js` (no in-repo bundler script) with a placeholder sourceHash, refresh the controls card + UI-kit RemoteScreen demo, and add the knob to `DESIGN.md` + DS readme.
- Rewrite `RemoteScreen.test.tsx` around the knob: CW/CCW/45° rotation, sub-detent no-op, ArrowUp/PageUp/keyboard-gated stepping, mute passthrough. `npm run front:build`, `npm run back:build`, `npm run front:test` (38/38), `npm run back:test` (98/98) all green.
- Manual UI + DS-SPA eyeball (tasks 5.4/5.5) deliberately left unchecked — need a paired Samsung TV on the LAN and a human at the browser; flagged in `docs/current-state.md` for the next session.
Co-Authored-By: Claude <noreply@anthropic.com>
First post-MVP capability. Opens a user-supplied URL in the TV's built-in Tizen browser via a new Samsung Smart View WebSocket envelope family (`ms.channel.emit` → `ed.apps.launch`), consuming C5's session queue and C9's toast pattern verbatim. - Requirements: add FR-BROWSER-01/02/03 in docs/requirements.md; add C10 row + Phase 5 blurb in docs/capabilities.md. - Back-end: new `tv/browser.ts` (`launchBrowserParams`) and `routes/browser.ts` (POST /api/devices/:udn/browser) with Fastify JSON schema + WHATWG URL parse + http/https-only scheme guard; 409 SessionNotConnected mirrors C6/C7/C8. Wired under /api in app.ts. - Front-end: remove the four placeholder `AppShortcut` tiles on RemoteScreen; render an inline DS `Input` + primary `Button` "Open" in the same aria-disabled wrapper; new `useBrowserLaunch` hook that maps failures onto the shared C9 toast. - DS: additive `disabled` prop on existing `Input` primitive (mirrors the C7 Slider precedent); no new DS component. - Tests: back-end unit (`launchBrowserParams` envelope) + integration (valid launch, four validation rejections, 409 SessionNotConnected) and front-end component tests for the URL row (AppShortcuts absent, disabled/enabled matrix, launch-on-click clears input). back:test 107/107, front:test 44/44. - Specs: sync `openspec/specs/tv-browser-launch/spec.md` and archive change under `openspec/changes/archive/2026-07-06-tv-browser-launch/`. - Deferred: tasks 7.1/7.2/7.3 (real-TV verification + dark-mode eyeball) need a human session with an actual TV on the LAN — flagged in docs/current-state.md. Co-Authored-By: Claude <noreply@anthropic.com>
First Phase-6 (deployment) capability. Ships one bash script that builds
both packages, grants port-80 capability, writes and enables a systemd
unit that runs the back-end as a dedicated `mytv` system user, and
starts it. Idempotent — same script is also the update path.
- Requirements: add FR-DEPLOY-01/02/03/04 in docs/requirements.md; add
C11 row + Phase 6 blurb in docs/capabilities.md.
- Scripts (new `scripts/` directory):
- `install.sh`: check_prereqs (refuses non-Linux / missing tools /
non-root), resolve_paths, install_deps, build, grant_port_capability
(setcap cap_net_bind_service), create_service_user (idempotent),
write_unit (cmp against existing, daemon-reload only if changed),
enable_and_start (poll is-active up to 10s), print_status. Supports
`--dry-run` for previewing mutating commands.
- `uninstall.sh`: disable + remove unit; deliberately keeps the mytv
user + /var/lib/mytv so re-install preserves the on-disk token store.
- `mytv.service.tmpl`: Type=simple, User=mytv, Restart=on-failure,
After=network-online.target, HOME=/var/lib/mytv (XDG_CONFIG_HOME
deliberately NOT set so token-store.ts falls back to $HOME/.mytv).
- `install.test.bats`: bats-core smoke tests covering darwin refusal,
systemctl-missing prereq, resolve_paths invariants, render_unit
substitution + byte-stability, and dry-run behaviour.
- Root package.json: `deploy:install` and `deploy:uninstall` proxies.
- back-end/README.md: new "Production install (Orange Pi)" section
pointing at `sudo ./scripts/install.sh` (+ `--dry-run` preview and
troubleshooting note for setcap loss after `apt upgrade`).
- Verification: bash -n clean on both scripts; back:build, front:build,
back:test (107/107), front:test (44/44) all pass. darwin refusal
verified manually. bats itself not installed in the agent env; all 7
bats assertions were replayed as plain-bash checks and passed.
- Deferred: on-Pi verification (tasks.md §7.1–7.4) — no Orange Pi in the
agent environment; `bats scripts/install.test.bats` (§5.3) — bats-core
not installed and the classifier declined `brew install`. Both are
honestly disclosed in tasks.md and docs/current-state.md.
- Specs: sync `openspec/specs/systemd-installer/spec.md` and archive
change under `openspec/changes/archive/2026-07-06-systemd-installer/`.
Co-Authored-By: Claude <noreply@anthropic.com>
Follow-up to C11, driven by real Orange Pi deploy pain (dev toolchain required on target, no build-once/ship-dist path, error output tangled with npm output). Splits the deploy story along a clean seam: build.sh produces dists (no root, macOS or Linux); install.sh consumes them (systemd on Linux). Two-step recipe. - Requirements: add FR-BUILD-01 (build script), FR-DEPLOY-05 (install refuses missing dists), FR-DEPLOY-06 (npm ci --omit=dev). Reword FR-DEPLOY-01 so build ownership sits with build.sh, not install.sh. - Capabilities: add C12 deploy-build (Phase 6) and update C11's one-liner and covers list. - New scripts/build.sh: no-root, no-systemd, no OS gate. check_prereqs refuses without node; build steps die with the expected artefact path named on tsc/vite failure; final verify_artifacts asserts both dists non-empty. Prints single-host and split-host next-step recipes. - scripts/install.sh: delete install_deps + build. Add verify_artifacts (fails before any mutating step with the missing path + a build.sh pointer) and install_prod_deps (npm ci --omit=dev --prefix back-end/). New main() pipeline. --help + dry-run banner updated to name the split. - scripts/install.test.bats: add 6 tests — build.sh no-node refusal, verify_artifacts back-end/front-end/both-present cases, and back:build / front:build failures each naming the expected artefact path. - Root package.json: `deploy:build` proxy alongside deploy:install / uninstall. - back-end/README.md: two-step "Production install (Orange Pi)" recipe plus a "Build once, ship dist" subsection with the rsync command. - Verification: bash -n clean on all three scripts; live build.sh e2e on this laptop regenerated both dists; back:build, front:build, back:test (107/107), front:test (44/44) all pass. All 8 bats assertions (existing + 6 new) replayed manually in plain bash since bats-core isn't installed and the auto-mode classifier still declines brew installs. - Specs: sync new deploy-build spec, apply MODIFIED + ADDED requirements to systemd-installer spec, archive change under openspec/changes/archive/2026-07-06-build-install-split/. - Reviewer: PASS on second review (first pass CHANGES_REQUESTED on two findings — tsc/vite failure not naming the artefact path, dry-run banner not mentioning the split — both fixed and re-tested). - Deferred: on-Pi verification (tasks.md §8.1/§8.2) — same posture as C11; the human debug session that motivated this change is where those tasks close. bats scripts/install.test.bats (§6.3) — replayed manually. Co-Authored-By: Claude <noreply@anthropic.com>
📝 WalkthroughWalkthroughЦе повноцінний greenfield-репозиторій "mytv": Fastify-backend (discovery Samsung TV через SSDP, сесії Smart View WebSocket, керування/гучність/входи/браузер), React+Vite SPA, повний OpenSpec-архів пропозицій/дизайну/задач по кожній capability, набір agentic skills/команд (OPSX, code-reviewer) та bash-скрипти для systemd-деплою. Estimated code review effort: 5 (Critical) | ~180+ хвилин ChangesДокументація для агентних skills та workflow
Backend сервіс mytv (Fastify)
Frontend SPA mytv (React + Vite)
OpenSpec специфікації, вимоги та архів змін
Скрипти розгортання
Sequence Diagram(s)sequenceDiagram
participant SPA
participant BackendAPI as Fastify API
participant SessionManager
participant SmartViewWS as Smart View WS
SPA->>BackendAPI: POST /api/devices/:udn/connect
BackendAPI->>SessionManager: ensure(udn).connect()
SessionManager->>SmartViewWS: open + handshake
SmartViewWS-->>SessionManager: token / Connected
SessionManager-->>BackendAPI: state=Connected
BackendAPI-->>SPA: 200 + WS event session
SPA->>BackendAPI: POST /api/devices/:udn/key {key}
BackendAPI->>SessionManager: enqueue(ms.remote.control)
SessionManager->>SmartViewWS: send frame
BackendAPI-->>SPA: 204
Estimated code review effort: 5 (Critical) | ~180 хвилин Мій вердикт як ментора курсу: сам обсяг і структура роботи демонструють дисципліноване застосування spec-driven agentic workflow — кожна capability пройшла через propose → apply → archive з чіткими design/tasks/specs і навіть журналом 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
|
@coderabbitai help |
ChatThere are 3 ways to chat with CodeRabbit:
CodeRabbit commands
Other keywords and placeholders
Status, support, documentation and community
|
|
@coderabbitai full review |
✅ Action performedFull review finished. |
|
@coderabbitai rate limit |
|
Your plan includes PR reviews subject to rate limits. Reviews are available now. |
|
@coderabbitai full review |
✅ Action performedFull review finished. |
✅ Action performedFull review finished. |
There was a problem hiding this comment.
Actionable comments posted: 68
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (4)
scripts/install.test.bats (1)
1-245: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winДуже пристойний набір bats-тестів.
Покриття справді відповідає заявленому в описі PR підходу "специфікації-тести-evals": є негативні сценарії (відсутній
node/systemctl, відсутні артефакти збірки), перевірка ідемпотентності рендеру юніта та dry-run поведінки без реальних мутацій системи. Це саме той рівень верифікації агентних змін, який хочеться бачити — респект.Одна дрібниця на майбутнє: тест 5.2/5.3 (рядки 158-184) перевіряють
verify_artifactsзinstall.sh, але немає симетричного тесту на створення групиmytvпід часcreate_service_user(див. коментар уinstall.shпро--user-group) — варто додати, коли виправите той момент.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@scripts/install.test.bats` around lines 1 - 245, The install.sh test suite covers verify_artifacts well, but it is missing the symmetric check for create_service_user and the --user-group behavior mentioned in the script comment. Add a bats test in install.test.bats that exercises create_service_user with a stubbed useradd and verifies the mytv group-related flag/behavior is used, referencing create_service_user and the --user-group path so the test remains findable if layout changes.front-end/README.md (1)
1-76: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueREADME досі шаблонний, не про mytv.
Весь файл — це дефолтний Vite/React boilerplate, жодного слова про сам проєкт (запуск разом з back-end, single-origin serving, Orange Pi деплой). Непогано було б замінити на короткий опис реального фронтенду, хоча б з посиланням на кореневий
docs/product-brief.md.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@front-end/README.md` around lines 1 - 76, The README is still the default Vite/React boilerplate and doesn’t describe this project; replace the template text in the README with a short front-end overview for mytv, including how it runs with the back-end, single-origin serving, and Orange Pi deployment, and point readers to docs/product-brief.md for the full product description.front-end/src/App.css (1)
1-185: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick winМертвий файл:
App.cssбільше ніде не імпортується.
App.tsxтепер імпортує лишеDeviceListScreen,RemoteScreen,useDevicesіDevice— жодногоimport './App.css'. Це стандартний шаблонний CSS від Vite+React (.hero,.framework,.vite,#docs,#next-steps,.ticks), що лишився з болерплейту й ніде не застосовується у реальних DS-екранах. Заразом порушує DS-правило "не хардкодити кольори/тіні/градієнти" (border-top,box-shadow, довільніtransform), хоч це вже неактуально, бо файл мертвий.Приберіть файл разом з рештою невикористаних default-Vite артефактів (логотипи,
.svgтощо, якщо є).🧹 Пропоноване видалення
-.counter { ... } -.hero { ... } -#center { ... } -#next-steps { ... } -#docs { ... } -#next-steps ul { ... } -#spacer { ... } -.ticks { ... }(видалити файл
front-end/src/App.cssповністю)🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@front-end/src/App.css` around lines 1 - 185, `App.css` is dead code and should be removed because `App.tsx` no longer imports it and the selectors (`.hero`, `.framework`, `.vite`, `#docs`, `#next-steps`, `.ticks`) are only Vite starter styles with no use in the current screens. Delete the `App.css` file and clean up any leftover unused default Vite assets referenced by the boilerplate if they are still present, so the frontend only keeps styles actually used by `DeviceListScreen` and `RemoteScreen`.back-end/README.md (1)
1-193: 📐 Maintainability & Code Quality | 🔵 TrivialДомашка процесно виглядає повною — усі 4 критерії на місці.
Пройшовся по опису PR: справжнє імʼя автора вказано в назві, є посилання на демо-відео, а опис агентних практик — не формальний, а змістовний (контекст-файли,
/next-changeцикл, окремийcode-reviewerсуб-агент як checker, OpenSpec як специфікації/тести/evals, конкретні інструменти й MCP). Результат заявлений як робочий і доведений до кінця, а не "згенеровано і кинуто" — і цейREADME.mdце підтверджує конкретикою (реальна модель TV, конкретні env-змінні, ручні репро-кроки). Гарна інженерна робота саме на рівні процесу, а не лише коду.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@back-end/README.md` around lines 1 - 193, No code change is needed here; the README already covers the requested process criteria with concrete details, so keep the current content as-is unless you want to make only minor wording tweaks for clarity in the sections describing the PR process, OpenSpec workflow, and reviewer/checker setup.Source: Path instructions
♻️ Duplicate comments (1)
back-end/src/tv/token-store.test.ts (1)
19-37: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick winНепогане покриття happy-path, але бракує тесту на паралельні
saveToken.Варто додати тест, що викликає
store.saveToken('udn-1', ...)іstore.saveToken('udn-2', ...)безawaitміж ними (Promise.all) і перевіряє, що обидва токени збереглись — це саме той сценарій, що виявляє race condition, описаний у зауваженні доtoken-store.ts.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@back-end/src/tv/token-store.test.ts` around lines 19 - 37, Add a test for concurrent writes in token-store.test.ts: the current saveToken/loadTokens coverage only checks sequential saves, but the race condition happens when createTokenStore().saveToken is called in parallel. Add a case that invokes saveToken for 'udn-1' and 'udn-2' without awaiting between them (using Promise.all), then verify loadTokens returns both entries. Use the existing createTokenStore, saveToken, and loadTokens symbols so the new test targets the same behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In @.agents/skills/fastify-best-practices/rules/logging.md:
- Around line 151-162: The custom `user` serializer is still emitting PII by
including `email`, so update the serializer to return only a safe identifier
such as `id` and remove any sensitive user fields from the logged object. Keep
the `request.log.info({ user: request.user }, ...)` usage, but ensure it only
receives the sanitized output from the `user` serializer.
In @.agents/skills/fastify-best-practices/rules/routes.md:
- Around line 195-206: Two fenced directory-tree blocks in the markdown rule doc
are missing a language tag and trigger markdownlint warnings. Update both code
fences to use the text language so the examples remain rendered as plain text;
check the duplicated tree example in the same document and keep the formatting
consistent across both blocks.
In @.agents/skills/fastify-best-practices/SKILL.md:
- Around line 1-6: File start violates MD041 because the markdown begins with
YAML front-matter instead of a top-level heading. Update the SKILL.md content so
the front-matter is followed by a first-line H1 like “Fastify Best Practices,”
or ensure the markdownlint configuration explicitly allows front-matter before
the heading. Reference the existing front-matter block in SKILL.md when making
the change.
In @.agents/skills/react-vite-best-practices/rules/asset-svg-components.md:
- Around line 44-156: The SVG-to-component guidance is too broad and conflicts
with Orbit DS icon rules, so narrow these examples in the asset-svg-components
rule. Update the guidance around the vite-plugin-svgr setup, the `*.svg?react`
import pattern, and the `Icon` component example to explicitly say this approach
is only allowed for non-UI assets or DS-owned icons, not general mytv UI. Add a
clear note that frontend UI should continue using DS primitives and Material
Symbols Rounded only, so agents don’t adopt arbitrary SVG components in
`front-end/src/**/*.{ts,tsx,js,jsx,css,html}`.
In @.agents/skills/react-vite-best-practices/rules/build-manual-chunks.md:
- Around line 70-86: The manual chunking logic in manualChunks currently checks
id.includes('react') too early, which can incorrectly capture react-router-dom
and other specific packages. Update the matching order so more specific package
checks (such as react-router-dom and react-dom) run before the generic react
case, and keep the existing vendor-react, vendor-react-dom, and vendor-router
chunk names aligned with the intended split behavior.
In @.agents/skills/react-vite-best-practices/rules/build-minification.md:
- Around line 162-166: The example in processData returns an undeclared value,
so it won’t compile. Update the example to return a value that is actually
defined inside processData, or declare and assign the result before returning
it, so the snippet is valid TypeScript. Keep the function name processData and
the logger.log usage unchanged while fixing the return value.
In @.agents/skills/react-vite-best-practices/rules/build-tree-shaking.md:
- Around line 111-129: Remove the optimizeDeps block from the tree-shaking
example so the snippet only demonstrates production tree-shaking via
build.rollupOptions.treeshake in the vite.config.ts example. If you want to keep
optimizeDeps.include, move that guidance to the dev-dependency-prebundling rule
instead; otherwise keep the example focused on the build configuration and its
unique symbols defineConfig, build.rollupOptions.treeshake, and optimizeDeps.
In @.agents/skills/react-vite-best-practices/rules/bundle-visualizer.md:
- Around line 106-121: Provide a language identifier for the fenced markdown
block in the bundle visualizer example: the opening fence currently has no
language, which triggers markdownlint MD040 and hurts readability. Update the
fence in the bundle-visualizer rule snippet to use text so the example remains a
valid fenced code block while preserving the existing content.
In @.agents/skills/react-vite-best-practices/rules/env-modes.md:
- Around line 40-59: The example env block in env-modes.md uses hard-coded
localhost, absolute API URLs, and telemetry settings that conflict with the repo
rules. Update the examples in the env mode section to use same-origin relative
endpoints like /api and /ws, and remove or replace Sentry/analytics-style
variables with neutral local-first placeholders. Keep the guidance aligned with
the env-mode examples and avoid any hard-coded hostnames or runtime telemetry
fields.
In @.agents/skills/react-vite-best-practices/rules/env-sensitive-data.md:
- Around line 16-60: The examples in env-sensitive-data.md still use secret-like
placeholder values and include a telemetry service in the “Correct” section.
Update the documentation examples to use only clearly synthetic placeholders
such as <redacted> and <secret>, remove the VITE_SENTRY_DSN example entirely,
and keep the guidance aligned with local-first/offline usage. Make sure the env
samples and the explanatory text reference the public-vs-private split without
showing any realistic secrets; the relevant sections are the env examples and
the “Correct” block.
In @.agents/skills/react-vite-best-practices/rules/env-vite-prefix.md:
- Around line 44-54: Update the env example in the relevant rule doc so it does
not present VITE_ENABLE_ANALYTICS as a “good” client-side pattern; replace that
analytics/telemetry flag with a local-only, offline-safe Vite example. Keep the
existing VITE_* vs server-only distinction, but make the client-side sample
clearly unrelated to analytics, and ensure the surrounding guidance in the env
rule aligns with the local-first policy.
- Around line 85-95: Update the `.env.development` example in the VITE env
best-practices rule so it uses a same-origin/relative API base instead of an
absolute localhost URL. Replace the `VITE_API_URL` example value currently shown
for `.env.development` with a relative path like `/api`, keeping the surrounding
`.env.production` and `.env.staging` examples unchanged.
In @.agents/skills/react-vite-best-practices/rules/split-dynamic-imports.md:
- Around line 134-149: Move the dynamic import out of App so AdminPanel is
created once at module scope instead of on every render. Define the lazy(...)
component with a stable symbol name outside App, then conditionally render that
already-created component inside App using the existing Suspense fallback. This
keeps the component identity stable and avoids unnecessary remounts or state
resets.
In @.agents/skills/react-vite-best-practices/rules/split-prefetch-hints.md:
- Around line 175-180: The prefetch scheduling in preloadFns currently invokes
fn() without handling its returned promise, which can surface as an unhandled
rejection. Update the callback inside preloadFns.forEach so that both
requestIdleCallback and setTimeout paths explicitly handle failures from fn()
and fail open by swallowing or safely catching the rejection. Use preloadFns and
fn as the key symbols to locate the async prefetch wrapper and ensure the
behavior remains silent on prefetch errors.
In @.agents/skills/react-vite-best-practices/rules/split-suspense-boundaries.md:
- Around line 96-115: The ErrorBoundary usage is passing an element via fallback
instead of the fallback component prop, so ErrorFallback never receives error
and resetErrorBoundary. Update the ErrorBoundary wrapper in the suspense/routes
example to use FallbackComponent with ErrorFallback, and keep ErrorFallback
defined to accept the injected props so the retry button can work correctly.
In @.claude/agents/code-reviewer.md:
- Line 20: The guidance currently points reviewers to the wrong TV protocol
skill; update the reviewer instruction in the code-reviewer agent docs to
reference Samsung Smart View WebSocket instead of `samsung-ip-control-protocol`.
Keep the check focused on the WebSocket handshake and envelope format, and
adjust the existing TV I/O guidance so it names the Smart View WebSocket
skill/location used in this repo.
In @.claude/commands/opsx/archive.md:
- Around line 34-49: The archive flow currently allows proceeding after a
confirmation even when artifacts or tasks are incomplete, which can hide
unfinished work. Update the archive command logic in the archive.md flow so that
incomplete artifacts/tasks are treated as a hard stop unless the required green
verification and PASS from code-reviewer are already present, and remove the
“prompt to continue” path for incomplete status checks. Use the artifact/task
validation steps and the archive gating logic to ensure archiving only happens
when the change is fully complete.
In @.claude/skills/samsung-ip-control-protocol/SKILL.md:
- Around line 1-19: Mark the samsung-ip-control-protocol skill as
legacy/reference-only instead of active: update the skill metadata and guidance
in SKILL.md so it no longer tells agents to load it for current TV I/O work,
since backend TV transport has moved to Smart View WebSocket. Keep the document
available for historical reference, but make the applicability section and load
instructions explicitly exclude it from the active path for mytv and point
agents away from using it for current back-end/** TV communication.
- Around line 76-93: Fix the method catalog markdown table in SKILL.md: the
`powerControl` row contains unescaped pipe characters and the `3.1.3` row has an
inconsistent column count, which breaks the table layout. Update the table entry
for `powerControl` and the mute control row so the markdown remains a valid
3-column table, or move those notes out into bullets; keep the rest of the
method list aligned under the same catalog section.
In `@back-end/.env.example`:
- Around line 3-6: Dotenv-linter is flagging cosmetic issues in the env sample:
reorder the keys so HOST and LOG_LEVEL appear before PORT, and ensure the file
ends with a trailing blank line. Update the .env.example contents accordingly
and keep SERVE_SPA in the same section.
In `@back-end/src/discovery/index.ts`:
- Around line 85-157: `handleHit` only guards `fetchDescription`, so any
exception from `registry.upsert` (or later logic) can reject the task and bubble
into `queue.add(...)` as an unhandled promise rejection. Wrap the
`registry.upsert` block inside `handleHit` in its own try/catch, log the failure
with `logger.debug` or similar using `description.udn`, `ip`, and `port`, and
return safely. Also make sure the `transport.on('hit', ...)` path does not leave
the `queue.add(() => handleHit(hit))` promise unobserved if `handleHit` can
still reject.
In `@back-end/src/discovery/registry.ts`:
- Around line 52-80: The IP deduplication check in upsert currently runs only
for new UDNs, so an existing device can be updated onto an IP already used by
another online device. Move or reuse the conflict scan so it also applies in the
existing branch before devices.set, while still allowing the same UDN to update
its own record; use upsert, devices, and the emitter added/updated paths to
locate the change.
In `@back-end/src/logger.ts`:
- Line 3: The sensitive-key redaction in logger.ts is currently case-sensitive,
so keys like Token or Authorization can bypass masking. Update the key check
used by the recursive redaction logic to normalize casing before consulting
SENSITIVE_KEYS, and make SENSITIVE_KEYS match the normalized form consistently.
Keep the existing redaction flow intact in the logger’s redact helper so
token-related fields are removed regardless of key depth or key capitalization.
In `@back-end/src/mdns.test.ts`:
- Around line 109-127: The mdns test only covers the synchronous publish() throw
path, but the real Avahi port-conflict is handled asynchronously through
rawClient.server.mdns.on('error'). Extend the startMdns test using the
MdnsClient stub to include a server.mdns.on handler and simulate an async
EADDRINUSE error so the error-state branch in mdns.ts is covered; keep the same
assertions on handle.state, handle.error, and handle.address for this
socket-error path.
In `@back-end/src/plugins/static-spa.ts`:
- Around line 18-32: The URL decoding in resolveStaticFile can throw on
malformed percent-encoding, which causes the static SPA fallback to fail with an
error instead of returning no match. Wrap the decodeURIComponent handling in a
try/catch inside resolveStaticFile, and return null when decoding fails so the
notFound/static lookup path in static-spa.ts treats the request as missing. Keep
the existing traversal checks with resolve, isInsideRoot, and existsSync
unchanged.
In `@back-end/src/routes/browser.test.ts`:
- Around line 134-210: Add a test in browser.test.ts to cover the
transport-level rejection path after a session is already connected: mock
session.enqueue() to reject with a TvError and verify the /browser POST response
is mapped by the global error handler to an allowed union code instead of
leaking wire-level details. Reuse the existing POST /browser test setup, the
connect flow, and the post helper/stub assertions to ensure no frame is sent
when enqueue fails. Locate the change around the browser route tests and the
session.enqueue() call path used by the browser route handler.
In `@back-end/src/tv/manager.ts`:
- Around line 111-113: The onRemoved handler in manager.ts only removes the
device from deviceIndex, so if registry ever starts emitting removed it will
leave stale entries in sessions. Update onRemoved to also clean up any
session/WebSocket state for the same device.udn, and add a TODO or defensive
safeguard around the removed path in TVManager so future registry behavior
cannot leak session entries.
- Around line 18-21: resolveControlPort currently turns any non-empty
MYTV_CONTROL_PORT_<udn> override into Number(override) without validating the
result, which can produce NaN and send sessions to an invalid port. Update
resolveControlPort in manager.ts to parse the env override safely and fall back
to SMART_VIEW_PORT when the override is missing, non-numeric, or otherwise
invalid. Keep the logic localized in resolveControlPort so the default port is
always used unless a valid numeric override is provided.
In `@back-end/src/tv/session.test.ts`:
- Around line 112-176: The tests in session.test.ts are flaky because they rely
on fixed setTimeout sleeps to wait for heartbeat and reconnect state changes.
Replace the hard-coded delays in the heartbeat/reconnect tests with a wait
helper that resolves from session.on('state', ...) once the expected state kind
appears, and use that helper around the existing createSession/connect flow so
the assertions wait for Reconnecting, Connected, and Disconnected
deterministically.
In `@back-end/src/tv/token-store.ts`:
- Around line 46-55: The catch block in token-store’s load path is silently
discarding malformed tokens.json by resetting cache to {} and returning {},
which makes unexpected token loss hard to diagnose. Update the error handling in
the token-store logic to keep the existing ENOENT fallback but log a
warning/error for any other parse/read failure, including the caught err
details, before clearing cache and returning an empty object. Use the existing
cache handling in token-store.ts and the surrounding load/read function to place
the log where the failure is handled.
In `@front-end/src/api/client.ts`:
- Around line 18-32: Add a timeout to the shared request flow in request() so
fetch calls cannot hang forever; currently fetch(path, init) has no abort path,
which can leave useBrowserLaunch and useInputs stuck in isPending. Update
request() to use AbortController (or accept an existing signal/timeout option)
and abort slow requests with a clear timeout error, then make sure the ApiError
handling still works for non-OK responses and callers can surface the failure.
In `@front-end/src/data/useBrowserLaunch.ts`:
- Around line 36-67: The `launch` error-handling block in `useBrowserLaunch`
duplicates the same `ApiError`/generic toast/log/rethrow pattern used in
`useInputs.setInput` and likely other data hooks, so extract that logic into a
shared helper such as `reportApiError` or `useApiAction`. Keep `launch` focused
on calling `post(udn, url)` and delegate the `ApiError` logging,
`push(messageFor(err))`, fallback toast, and rethrow behavior to the shared
helper so future toast or logging changes happen in one place.
In `@front-end/src/data/useDeviceSession.ts`:
- Around line 89-107: The WebSocket handler in useDeviceSession accepts any
string for message.state, which can let invalid session states reach setState
and downstream UI mappings. In onMessage, after parsing the WsSessionMessage
payload, narrow message.state to the allowed ClientSessionState values only
('Disconnected', 'Connecting', 'Connected', 'Offline') before calling setState.
Use the existing onMessage and setState flow in useDeviceSession to keep the
validation local and prevent unsupported states from entering
sessionToBadgeStatus and STYLE consumers.
In `@front-end/src/fonts.css`:
- Line 66: `word-wrap` is deprecated in the font stylesheet, so update the CSS
rule in the affected selector to use `overflow-wrap` instead of `word-wrap`.
Keep the same wrapping behavior while replacing the deprecated property so the
Stylelint `property-no-deprecated` warning is resolved.
- Around line 9-45: In the `@font-face` blocks in fonts.css, the font-family value
for Montserrat is using quoted names that violate the font-family-name-quotes
rule. Update all Montserrat `@font-face` declarations so the font-family is
unquoted consistently across the affected blocks, and keep the rest of the
font-face settings unchanged.
In `@front-end/src/screens/DeviceListScreen.tsx`:
- Around line 14-42: The layout in DeviceListScreen still uses hardcoded spacing
values instead of the existing design tokens. Update the top-level wrapper,
header row, and title block in DeviceListScreen to use the same spacing token
approach already used by EmptyState in this file, replacing raw pixel values
like padding, marginBottom, gap, and similar with the appropriate var(--space-*)
tokens. Keep the structure and styling intent the same, but make spacing
consistent across the screen.
In `@front-end/src/screens/RemoteScreen.tsx`:
- Around line 128-230: RemoteScreen.tsx still uses hardcoded pixel values for
layout spacing, so the screen is inconsistent with the design-system spacing
approach. Update the styles in the RemoteScreen component to replace raw
padding, gap, and similar spacing values with the existing var(--space-*)
tokens, following the same pattern used in DeviceListScreen. Keep the structure
and behavior of the layout the same, but make the spacing token-driven across
the main container and the nested button/control groups.
- Around line 156-163: On RemoteScreen, there are two accent actions competing
for attention: the primary Open button and the power button using accent
styling. Update the power button implementation to use the default/neutral tone
instead of accent so only the main action remains emphasized, and keep the
primary Button for handleLaunchBrowser as the single accent element on the
screen.
In `@front-end/src/ui/ErrorBoundary.tsx`:
- Around line 41-85: The error fallback in ErrorBoundary is using raw divs with
inline layout styles instead of DS primitives. Refactor the render path in
ErrorBoundary to build the full screen, centering, and card layout using
components from `@ds/components/`** (for example a screen/container/stack/card
primitive), and if the needed layout primitive does not exist, add it to the
design system rather than keeping inline styles in the fallback UI.
- Around line 67-77: The fallback copy in ErrorBoundary is written in third
person, but the UI guidelines require English sentence case in second person.
Update the strings in the ErrorBoundary render path so they speak directly to
the user (for example, revise the messages shown in the “Something broke.” block
and the “The remote screen ran into an unexpected error...” block), while
keeping the tone concise and consistent with the existing ErrorBoundary
component.
In `@front-end/src/ui/ToastHost.tsx`:
- Around line 14-27: The toast container in ToastHost uses a raw inline-styled
<div> for layout, but the guideline requires all UI elements to come from
`@ds/components`. Replace this stack wrapper with an appropriate design-system
layout primitive (or add one to the DS if missing) and keep the ToastHost
composition using that component instead of inline styles.
In `@front-end/src/ui/useToast.ts`:
- Around line 14-23: The local Toast type name conflicts conceptually with the
DS Toast component, so rename the interface in useToast.ts to a more specific
identifier like ToastItem or ToastState. Update all references within the toast
hook/state helpers to use the new name, especially the Toast interface and any
functions or arrays in useToast that consume it, to avoid future alias-import
collisions.
- Around line 152-167: The auto-dismiss timer in useToast is being reset to the
full remaining duration whenever any toast list change reruns the effect, so
existing toasts can live longer than intended. Update the Auto-dismiss useEffect
in useToast to compute each timeout from the actual elapsed time since the
toast’s createdAt (or equivalent persisted start timestamp) instead of blindly
using toast.remainingMs, and ensure dismiss scheduling for a given toast is
stable across unrelated state.toasts updates. Also add/adjust a
ToastHost.test.tsx case that simulates multiple concurrent toast pushes so the
original toast still dismisses after the intended 5s window.
In `@front-end/src/ui/WsHealthChip.tsx`:
- Around line 8-52: The WsHealthChip is reimplementing a status pill with ad-hoc
inline styles instead of using the design-system Badge primitive. Refactor
WsHealthChip to compose the existing Badge component (preferably the appropriate
variant/size) and move any missing visual treatment into the DS rather than
hardcoding spacing, dot sizing, and uppercase styling here. Keep the WebSocket
health mapping in STYLE and use WsHealthChip as a thin wrapper around the DS
component so DeviceListScreen and RemoteScreen get a consistent, reusable status
chip.
In `@openspec/changes/archive/2026-07-04-default-port-80/design.md`:
- Around line 44-47: The current test assertion relies on the health snapshot,
but that snapshot does not expose the mDNS SRV port, so it cannot detect a
mismatch between the listen port and the advertised port. Update the relevant
assertion in app.test.ts to verify the actual port passed to startMdns() or
another direct advertised-port source tied to getPort(), rather than checking
/api/health. Keep the test focused on the resolved SRV port so it directly
validates the new default 80 behavior.
In `@openspec/changes/archive/2026-07-04-device-list-ui/tasks.md`:
- Around line 18-19: The grep check is currently counting scaffold SVG matches
as success even though src/assets/vite.svg and src/assets/react.svg still
contain real hits, so the scenario is not actually satisfied. Update the tasks
in the device-list UI change to either remove those Vite scaffold assets or
narrow the verification to the intended source subtrees, and make sure the
acceptance text around the grep results reflects the actual cleanup in the
relevant sections.
- Around line 24-25: The task notes currently include real LAN-identifying
device details from the partial repro run, which should not be committed. In the
task entry for the device-list UI verification, replace the concrete Samsung TV
name and IP with redacted placeholders and keep only generic wording; update the
relevant checklist text so it no longer preserves local network data while still
referencing the same verification steps and the partial repro note.
In `@openspec/changes/archive/2026-07-04-remote-control-keys/design.md`:
- Around line 54-59: The fallback that uses a pointer-events/opacity wrapper for
DPad is not allowed; keep all UI behavior inside `@ds/components/`** instead.
Update the DPad primitive to support a real disabled state (matching IconButton)
and then wire RemoteScreen to use that disabled prop alongside the existing
state from useDeviceSession and sendKey handlers. Avoid any ad-hoc wrapper CSS
or token-based workaround in the command controls section.
In `@openspec/changes/archive/2026-07-04-smart-view-ws-transport/proposal.md`:
- Around line 5-16: The Smart View WS transport contract is mixing JSON-RPC
semantics with the WebSocket envelope, which will drive the wrong implementation
and tests. Update the `JsonRpcTransport`-style API in
`back-end/src/tv/jsonrpc.ts` and the related design text so `call()` is
described as a WS send/await-ack flow using the Smart View envelope with
`TypeOfRemote`, not a decoded JSON-RPC round-trip, and align all mentions of
`TypeOfCode` to the actual protocol fields used by the session actor.
In
`@openspec/changes/archive/2026-07-04-smart-view-ws-transport/specs/upnp-tv-discovery/spec.md`:
- Around line 5-17: The Samsung pre-filter in the discovery orchestrator is too
broad because it treats any `uuid:` prefix as a Samsung signature, which will
incorrectly let non-Samsung UPnP devices through. Update the SSDP header check
in the spec around the discovery flow so only truly Samsung-specific signals are
accepted (for example, the case-insensitive `samsung` substring or a
Samsung-specific UUID pattern if that is actually narrower than generic
`uuid:`), and ensure the `USN`/`SERVER`/`ST` drop behavior in the non-Samsung
scenario still rejects generic `uuid:` responses before enqueueing the
description fetch.
In `@openspec/changes/archive/2026-07-04-tv-connection-lifecycle/design.md`:
- Around line 15-20: Update the design note in the TV connection lifecycle
document to remove any mention that the pairing token appears in a back-end log
line. In the relevant Non-Goals section, rephrase the pairing flow description
so it says the token is persisted only in the token store and never logged,
keeping the rest of the MVP pairing description intact. Use the surrounding
pairing-token wording in the document to locate and revise the sentence.
In `@openspec/changes/archive/2026-07-04-tv-connection-lifecycle/proposal.md`:
- Line 33: The proposal currently states that the MVP prints the pairing token
to the back-end log, which should be removed. Update the pairing UI/logging
description in the proposal so the token stays backend-only and any log or
status output is redacted or neutral; if you need user feedback, mention the
pairing step without exposing the value. Locate the change in the pairing-token
MVP text and revise that wording to align with the backend-only token handling
requirement.
In `@openspec/changes/archive/2026-07-04-upnp-tv-discovery/tasks.md`:
- Line 19: Remove the global DISCOVERY_ENABLED env-flag from the discovery
startup change in the onReady-based discovery setup, since it can disable the
feature in production. Keep the opt-out confined to the test harness or a DI
stub around the discovery loop/transport registration in back-end/src/app.ts,
while preserving app.discovery exposure and the existing registry/broker/handle
wiring for normal startup.
In
`@openspec/changes/archive/2026-07-05-error-surfacing/specs/error-surfacing/spec.md`:
- Around line 5-21: The requirement in error-surfacing/spec.md is internally
inconsistent: it says every user-initiated command failing with ApiError must
show exactly one toast, but the broader spec already treats some cases as
dev-only or intentionally toastless. Update the spec text to either explicitly
list the exceptions (for example validation and disconnect-related flows) or
narrow the rule to the exact commands covered by FR-ERROR-01/FR-REMOTE-03, so
the acceptance criteria are unambiguous.
In `@openspec/changes/archive/2026-07-05-input-management/proposal.md`:
- Around line 7-13: This proposal is still written as an active Smart View
contract, but it should be marked as a rejected/archived draft instead. Update
the wording in the proposal to clearly state that `directSourceControl`, cached
`activeId`/refresh behavior, and the `devices` input WebSocket payload are not
the current contract, and that the real spec uses the static catalog,
`ms.remote.control`, and `{ udn, key }` events. Make this explicit in the
proposal title/summary and in the `input-management` sections so future changes
are not guided toward the wrong protocol.
In `@openspec/changes/archive/2026-07-05-volume-control/design.md`:
- Around line 28-43: The volume lifecycle for muted is inconsistent between the
API contract and the state-push contract. Update the design in the volume module
section so the behavior is described consistently for the tracker used by GET
/api/devices/:udn/volume and the Connected/Disconnected/Offline transitions:
either preserve the last-known muted value across disconnects/reconnects, or
explicitly state that the tracker resets to false on disconnect and Connected
emits that reset snapshot. Make the D1 POST /mute and D2 state-push bullets
agree on the same source of truth for muted, using the volume tracker semantics
referenced in the design text.
In `@openspec/changes/archive/2026-07-05-volume-control/proposal.md`:
- Around line 7-15: The proposal still describes the old hotel-TV volume
contract with direct level reporting and absolute volume set, so update the
archived proposal to match the Smart View-only write-only model. In the
proposal.md content around the volume API and `RemoteScreen` volume block,
either mark the old `directVolumeControl`/`POST /volume { level }`/`level:
number` behavior as discarded draft, or rewrite it to the final `level:
null`-style contract so it aligns with the current spec.
In `@openspec/changes/archive/2026-07-05-volume-control/tasks.md`:
- Around line 17-24: The volume API contract is inconsistent between the
frontend hook plan and the backend route, so the initial mount request will not
match the server. Align the route used by useVolume with the backend’s volume
endpoint, then update the corresponding router registration and any tests so
GET, delta POST, and mute behavior all use the same path consistently.
In
`@openspec/changes/archive/2026-07-06-build-install-split/specs/systemd-installer/spec.md`:
- Line 5: The systemd installer requirement in the spec is now install-only, so
the existing FR-DEPLOY-01 coverage tag is contradictory. Update the
`systemd-installer` spec entry to remove that coverage reference or move it to
the `deploy-build` spec where the build behavior lives, keeping
`scripts/install.sh` focused on verifying artefacts, installing runtime
dependencies, and setting up the systemd unit.
In `@openspec/changes/archive/2026-07-06-build-install-split/tasks.md`:
- Around line 41-53: The build/install handoff still has deferred verification
items that leave the installation flow incomplete. Either complete the remaining
checks for 6.3, 8.1, and 8.2 on the real Orange Pi, or explicitly record them as
follow-up in docs/current-state.md so the archive reflects the unresolved state.
Update the task list entries to remove ambiguity about what is done versus
deferred, and keep the verification status aligned with the actual handoff
scope.
In `@openspec/changes/archive/2026-07-06-systemd-installer/proposal.md`:
- Around line 17-18: The systemd installer proposal currently ties the dedicated
mytv user’s home to the repository checkout, which should be moved to a real
state directory instead. Update the mytv service-user setup described in
install.sh and the mytv.service.tmpl notes so token storage and any user state
live under /var/lib/mytv (or another persistent data dir), not `@REPO_ROOT`@, and
keep uninstall.sh from depending on the source tree for rollback.
In `@openspec/changes/archive/2026-07-06-tv-browser-launch/design.md`:
- Around line 107-109: The route entry log is still including the full
user-provided URL at info level, which can leak secrets from query strings.
Update the logging in the route described here to avoid emitting raw url; prefer
logging only host/origin or a scrubbed URL, and keep the existing correlationId,
udn, and ok fields in the entry/exit logs. Ensure the fix is applied where the
route logging is defined so the logger.ts redaction is no longer relied on for
this case.
In `@openspec/changes/archive/2026-07-06-tv-browser-launch/proposal.md`:
- Around line 23-26: Remove the outdated “modal” mention from the New
Capabilities description so it matches the current inline Input + Button UX used
elsewhere in the proposal/design. Update the `tv-browser-launch` capability
summary to describe only the inline SPA flow, keeping the wording consistent
across `proposal.md` so future changes do not reintroduce the modal variant.
In `@openspec/changes/archive/2026-07-06-tv-browser-launch/tasks.md`:
- Line 11: Update the launch task note to remove the requirement to log the full
launch URL at info level. In the relevant task entry for the TV browser launch
flow, replace the URL logging guidance with a safer form of observability that
only records correlationId or a normalized origin/host. Make sure the wording no
longer depends on logger.ts token-stripping, since that redaction is
insufficient for query strings and sensitive URL data.
In `@openspec/specs/systemd-installer/spec.md`:
- Around line 9-10: The contract around `scripts/install.sh` is inconsistent
because `--dry-run` currently allows build-related steps that the script is
otherwise forbidden to perform. Update `install.sh` so the dry-run path in the
main installer flow only previews actions and does not execute `npm ci`,
TypeScript, or Vite build steps; keep all build behavior out of `install.sh`
entirely and leave it to `scripts/build.sh`. Ensure the logic around the install
entrypoint and any `--dry-run` handling clearly separates preview from execution
while still validating the presence of `back-end/dist/index.js` and
`front-end/dist/index.html` before any systemd changes.
In `@scripts/build.sh`:
- Around line 14-45: The build script duplicates shared helper logic that also
exists in install and uninstall scripts, so extract the common `log`, `die`,
`need_cmd`, Node version parsing in `check_prereqs`, and repository path
resolution in `resolve_paths` into a shared `scripts/lib.sh`, then `source` it
from `build.sh` (and the matching scripts) so all three scripts use one
implementation and stay in sync.
In `@scripts/mytv.service.tmpl`:
- Around line 7-29: The systemd unit for the mytv service is missing basic
hardening for a network-facing daemon. Add sandboxing directives such as
NoNewPrivileges, ProtectSystem, ProtectHome, and PrivateTmp in the [Service]
section of mytv.service.tmpl, keeping the existing User, Group,
WorkingDirectory, and ExecStart setup intact. Ensure the new restrictions do not
rely on AmbientCapabilities, since the privileged bind is handled via setcap on
the binary.
In `@scripts/uninstall.sh`:
- Around line 30-35: The unit-presence check in the uninstall flow is wrong
because `systemctl list-unit-files` can succeed even when `mytv.service` is not
installed, so the `else` branch and related logging are misleading. Update the
logic in `scripts/uninstall.sh` around the `systemctl` handling to either check
the actual unit file path with a filesystem test or skip the pre-check entirely
and use a best-effort `systemctl disable --now mytv` with suppressed failure,
keeping the log message aligned with the real outcome.
---
Outside diff comments:
In `@back-end/README.md`:
- Around line 1-193: No code change is needed here; the README already covers
the requested process criteria with concrete details, so keep the current
content as-is unless you want to make only minor wording tweaks for clarity in
the sections describing the PR process, OpenSpec workflow, and reviewer/checker
setup.
In `@front-end/README.md`:
- Around line 1-76: The README is still the default Vite/React boilerplate and
doesn’t describe this project; replace the template text in the README with a
short front-end overview for mytv, including how it runs with the back-end,
single-origin serving, and Orange Pi deployment, and point readers to
docs/product-brief.md for the full product description.
In `@front-end/src/App.css`:
- Around line 1-185: `App.css` is dead code and should be removed because
`App.tsx` no longer imports it and the selectors (`.hero`, `.framework`,
`.vite`, `#docs`, `#next-steps`, `.ticks`) are only Vite starter styles with no
use in the current screens. Delete the `App.css` file and clean up any leftover
unused default Vite assets referenced by the boilerplate if they are still
present, so the frontend only keeps styles actually used by `DeviceListScreen`
and `RemoteScreen`.
In `@scripts/install.test.bats`:
- Around line 1-245: The install.sh test suite covers verify_artifacts well, but
it is missing the symmetric check for create_service_user and the --user-group
behavior mentioned in the script comment. Add a bats test in install.test.bats
that exercises create_service_user with a stubbed useradd and verifies the mytv
group-related flag/behavior is used, referencing create_service_user and the
--user-group path so the test remains findable if layout changes.
---
Duplicate comments:
In `@back-end/src/tv/token-store.test.ts`:
- Around line 19-37: Add a test for concurrent writes in token-store.test.ts:
the current saveToken/loadTokens coverage only checks sequential saves, but the
race condition happens when createTokenStore().saveToken is called in parallel.
Add a case that invokes saveToken for 'udn-1' and 'udn-2' without awaiting
between them (using Promise.all), then verify loadTokens returns both entries.
Use the existing createTokenStore, saveToken, and loadTokens symbols so the new
test targets the same behavior.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
| PORT=3000 | ||
| HOST=0.0.0.0 | ||
| LOG_LEVEL=info | ||
| SERVE_SPA=1 No newline at end of file |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value
Дрібні lint-зауваження від dotenv-linter.
Порядок ключів (HOST/LOG_LEVEL перед PORT) та відсутній порожній рядок в кінці файлу — суто косметично, але лінтер про це прямо каже.
🧹 Пропозиція
-PORT=3000
-HOST=0.0.0.0
-LOG_LEVEL=info
-SERVE_SPA=1
+HOST=0.0.0.0
+LOG_LEVEL=info
+PORT=3000
+SERVE_SPA=1
+📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| PORT=3000 | |
| HOST=0.0.0.0 | |
| LOG_LEVEL=info | |
| SERVE_SPA=1 | |
| HOST=0.0.0.0 | |
| LOG_LEVEL=info | |
| PORT=3000 | |
| SERVE_SPA=1 | |
🧰 Tools
🪛 dotenv-linter (4.0.0)
[warning] 4-4: [UnorderedKey] The HOST key should go before the PORT key
(UnorderedKey)
[warning] 5-5: [UnorderedKey] The LOG_LEVEL key should go before the PORT key
(UnorderedKey)
[warning] 6-6: [EndingBlankLine] No blank line at the end of the file
(EndingBlankLine)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@back-end/.env.example` around lines 3 - 6, Dotenv-linter is flagging cosmetic
issues in the env sample: reorder the keys so HOST and LOG_LEVEL appear before
PORT, and ensure the file ends with a trailing blank line. Update the
.env.example contents accordingly and keep SERVE_SPA in the same section.
Source: Linters/SAST tools
| @@ -0,0 +1,84 @@ | |||
| import pino, { type DestinationStream, type LoggerOptions } from 'pino'; | |||
|
|
|||
| const SENSITIVE_KEYS = new Set(['AccessToken', 'authorization', 'token']); | |||
There was a problem hiding this comment.
🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win
Порівняння ключів чутливе до регістру — можливий витік токена.
Молодець, що редакція рекурсивна й окремо чистить token= у рядках (regex із gi). Але звірка ключів у SENSITIVE_KEYS.has(key) регістрозалежна: ключ на кшталт Token чи Authorization (якщо десь потрапить у лог у такому вигляді) не буде вирізаний. Нормалізуй регістр, щоб гарантія «редагується незалежно від глибини» була ще й незалежною від регістру.
Як per coding guidelines: «redact the pairing token ... from every log line».
🛡️ Пропозиція
-const SENSITIVE_KEYS = new Set(['AccessToken', 'authorization', 'token']);
+const SENSITIVE_KEYS = new Set(['accesstoken', 'authorization', 'token']);- if (SENSITIVE_KEYS.has(key)) continue;
+ if (SENSITIVE_KEYS.has(key.toLowerCase())) continue;Also applies to: 38-39
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@back-end/src/logger.ts` at line 3, The sensitive-key redaction in logger.ts
is currently case-sensitive, so keys like Token or Authorization can bypass
masking. Update the key check used by the recursive redaction logic to normalize
casing before consulting SENSITIVE_KEYS, and make SENSITIVE_KEYS match the
normalized form consistently. Keep the existing redaction flow intact in the
logger’s redact helper so token-related fields are removed regardless of key
depth or key capitalization.
Source: Coding guidelines
| test('publish failure leaves the handle in error state without throwing', async () => { | ||
| const failingClient: MdnsClient = { | ||
| publish() { | ||
| const err = new Error('bind EADDRINUSE 0.0.0.0:5353'); | ||
| (err as NodeJS.ErrnoException).code = 'EADDRINUSE'; | ||
| throw err; | ||
| }, | ||
| destroy() {}, | ||
| }; | ||
| const handle = startMdns(3000, { | ||
| client: failingClient, | ||
| networkInterfaces: () => iface('10.0.0.5'), | ||
| pollIntervalMs: 0, | ||
| }); | ||
| assert.equal(handle.state, 'error'); | ||
| assert.match(handle.error ?? '', /EADDRINUSE/); | ||
| assert.equal(handle.address, undefined); | ||
| await handle.stop(); | ||
| }); |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win
Бракує тесту на async socket-error шлях (реальний Avahi-конфлікт).
Тут перевіряється лише синхронний throw у publish(). А mdns.ts (рядки 61-238 контексту) ловить реальний port-conflict через rawClient.server?.mdns?.on('error', ...) — саме цей шлях і задокументований AVAHI_HINT як типова причина збою на Orange Pi. Варто додати стаб з server: { mdns: { on } }, який симулює async EADDRINUSE, щоб покрити цю гілку.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@back-end/src/mdns.test.ts` around lines 109 - 127, The mdns test only covers
the synchronous publish() throw path, but the real Avahi port-conflict is
handled asynchronously through rawClient.server.mdns.on('error'). Extend the
startMdns test using the MdnsClient stub to include a server.mdns.on handler and
simulate an async EADDRINUSE error so the error-state branch in mdns.ts is
covered; keep the same assertions on handle.state, handle.error, and
handle.address for this socket-error path.
| function resolveStaticFile(root: string, urlPath: string): string | null { | ||
| const requestPath = decodeURIComponent(urlPath.split('?')[0] ?? '').replace( | ||
| /^\/+/, | ||
| '', | ||
| ); | ||
| if (requestPath === '') return null; | ||
|
|
||
| const resolvedRoot = resolve(root); | ||
| const candidate = resolve(resolvedRoot, requestPath); | ||
| const isInsideRoot = | ||
| candidate === resolvedRoot || candidate.startsWith(resolvedRoot + sep); | ||
| if (!isInsideRoot || !existsSync(candidate)) return null; | ||
|
|
||
| return statSync(candidate).isFile() ? candidate : null; | ||
| } |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
decodeURIComponent кидає на некоректному URL → 500 замість 404.
Захист від traversal тут акуратний (resolve + перевірка resolvedRoot + sep) — це добре. Але запит із зіпсованим percent-encoding (напр. /%E0%A4%A) змусить decodeURIComponent кинути URIError, який синхронно вилетить із notFound-хендлера й перетвориться на 500 замість очікуваного 404. Оберни декодування у try/catch і повертай null.
🛡️ Пропозиція
- const requestPath = decodeURIComponent(urlPath.split('?')[0] ?? '').replace(
- /^\/+/,
- '',
- );
+ let requestPath: string;
+ try {
+ requestPath = decodeURIComponent(urlPath.split('?')[0] ?? '').replace(
+ /^\/+/,
+ '',
+ );
+ } catch {
+ return null;
+ }📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| function resolveStaticFile(root: string, urlPath: string): string | null { | |
| const requestPath = decodeURIComponent(urlPath.split('?')[0] ?? '').replace( | |
| /^\/+/, | |
| '', | |
| ); | |
| if (requestPath === '') return null; | |
| const resolvedRoot = resolve(root); | |
| const candidate = resolve(resolvedRoot, requestPath); | |
| const isInsideRoot = | |
| candidate === resolvedRoot || candidate.startsWith(resolvedRoot + sep); | |
| if (!isInsideRoot || !existsSync(candidate)) return null; | |
| return statSync(candidate).isFile() ? candidate : null; | |
| } | |
| function resolveStaticFile(root: string, urlPath: string): string | null { | |
| let requestPath: string; | |
| try { | |
| requestPath = decodeURIComponent(urlPath.split('?')[0] ?? '').replace( | |
| /^\/+/, | |
| '', | |
| ); | |
| } catch { | |
| return null; | |
| } | |
| if (requestPath === '') return null; | |
| const resolvedRoot = resolve(root); | |
| const candidate = resolve(resolvedRoot, requestPath); | |
| const isInsideRoot = | |
| candidate === resolvedRoot || candidate.startsWith(resolvedRoot + sep); | |
| if (!isInsideRoot || !existsSync(candidate)) return null; | |
| return statSync(candidate).isFile() ? candidate : null; | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@back-end/src/plugins/static-spa.ts` around lines 18 - 32, The URL decoding in
resolveStaticFile can throw on malformed percent-encoding, which causes the
static SPA fallback to fail with an error instead of returning no match. Wrap
the decodeURIComponent handling in a try/catch inside resolveStaticFile, and
return null when decoding fails so the notFound/static lookup path in
static-spa.ts treats the request as missing. Keep the existing traversal checks
with resolve, isInsideRoot, and existsSync unchanged.
| const onRemoved = (device: Device): void => { | ||
| deviceIndex.delete(device.udn); | ||
| }; |
There was a problem hiding this comment.
🩺 Stability & Availability | 🔵 Trivial | 💤 Low value
Сесія не прибирається при removed.
onRemoved чистить лише deviceIndex, а не sessions — якщо колись registry почне емітити removed (зараз, за коментарем у registry.ts, не емітить), відкрита сесія/WS-з'єднання для видаленого UDN залишиться висіти в Map назавжди. Зараз мертвий код, але вартий TODO/захисту на майбутнє, коли removed таки увімкнуть.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@back-end/src/tv/manager.ts` around lines 111 - 113, The onRemoved handler in
manager.ts only removes the device from deviceIndex, so if registry ever starts
emitting removed it will leave stale entries in sessions. Update onRemoved to
also clean up any session/WebSocket state for the same device.udn, and add a
TODO or defensive safeguard around the removed path in TVManager so future
registry behavior cannot leak session entries.
| - [x] 2.1 Create `back-end/src/tv/browser.ts` — export `launchBrowserParams(url: string): Record<string, unknown>` returning `{ event: 'ed.apps.launch', to: 'host', data: { appId: 'org.tizen.browser', action_type: 'NATIVE_LAUNCH', metaTag: url } }` (typed via a local `LaunchBrowserParams` interface + `satisfies`, same pattern as `keyControlParams` in `back-end/src/tv/keys.ts`). Do NOT hardcode the URL; take it as a parameter. | ||
| - [x] 2.2 Create `back-end/src/routes/browser.ts` — `POST /devices/:udn/browser` with Fastify JSON schema `{ type: 'object', required: ['url'], additionalProperties: false, properties: { url: { type: 'string', minLength: 1, maxLength: 2048 } } }`. Handler: WHATWG parse (`new URL(url)`); reject non-`http:`/`https:` with `HttpError.validation(...)` → `400 validation`. Fetch session via `discovery.sessions.get(udn) ?? .ensure(udn)`; if state !== `Connected` throw `new HttpError(409, 'SessionNotConnected', ...)`. Enqueue `transport.call('ms.channel.emit', launchBrowserParams(url))`. Reply `204`. | ||
| - [x] 2.3 Wire `registerBrowserRoute(app)` in `back-end/src/app.ts` under the existing `/api` prefix block, right after `registerInputRoutes(...)`. | ||
| - [x] 2.4 Confirm the URL is logged at `info` on entry (existing route-level Fastify logging is enough; verify nothing extra needs adding) and that `logger.ts`'s existing `token=` query-fragment stripping applies to logged URLs. |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win
Не логуй повний URL на рівні info.
Line 11 пропонує логувати launch URL, а це зберігатиме browsing history й будь-які секрети в query string; redaction, яка прибирає лише token= фрагменти, тут замала. Краще лишити correlationId або нормалізований origin/host.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@openspec/changes/archive/2026-07-06-tv-browser-launch/tasks.md` at line 11,
Update the launch task note to remove the requirement to log the full launch URL
at info level. In the relevant task entry for the TV browser launch flow,
replace the URL logging guidance with a safer form of observability that only
records correlationId or a normalized origin/host. Make sure the wording no
longer depends on logger.ts token-stripping, since that redaction is
insufficient for query strings and sensitive URL data.
| need_cmd() { | ||
| command -v "$1" >/dev/null 2>&1 || die "missing prerequisite: $1 not on PATH" | ||
| } | ||
|
|
||
| check_prereqs() { | ||
| need_cmd bash | ||
| need_cmd node | ||
| need_cmd npm | ||
| need_cmd mktemp | ||
|
|
||
| local node_version node_major | ||
| node_version="$(node --version 2>/dev/null || true)" | ||
| node_major="${node_version#v}" | ||
| node_major="${node_major%%.*}" | ||
| if [[ -z "$node_major" || "$node_major" -lt 20 ]]; then | ||
| die "node >= 20 required, found: ${node_version:-none}" | ||
| fi | ||
| } | ||
|
|
||
| resolve_paths() { | ||
| local script_dir | ||
| script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" | ||
| REPO_ROOT="$(cd "$script_dir/.." && pwd)" | ||
|
|
||
| [[ -d "$REPO_ROOT/back-end" ]] || die "REPO_ROOT missing back-end/: $REPO_ROOT" | ||
| [[ -d "$REPO_ROOT/front-end" ]] || die "REPO_ROOT missing front-end/: $REPO_ROOT" | ||
|
|
||
| NODE_BIN="$(command -v node)" | ||
|
|
||
| log "REPO_ROOT=$REPO_ROOT" | ||
| log "NODE_BIN=$NODE_BIN" | ||
| } |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win
Гарна структура, але є дублювання логіки з install.sh.
check_prereqs (перевірка node/npm/mktemp, парсинг мажорної версії) та resolve_paths тут майже побуквено повторюють однойменні функції в scripts/install.sh (рядки 32-77), а log/die — ще й у scripts/uninstall.sh. Три окремі копії однієї логіки — це класичне порушення DRY: зміна мінімальної версії Node чи способу резолву REPO_ROOT доведеться синхронізувати вручну в трьох файлах.
Варто винести спільні функції (log, die, need_cmd, парсинг версії node, резолв REPO_ROOT) у scripts/lib.sh і підключати через source на початку кожного скрипта — це не зашкодить bats-тестам, які й так source-ять скрипти напряму.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@scripts/build.sh` around lines 14 - 45, The build script duplicates shared
helper logic that also exists in install and uninstall scripts, so extract the
common `log`, `die`, `need_cmd`, Node version parsing in `check_prereqs`, and
repository path resolution in `resolve_paths` into a shared `scripts/lib.sh`,
then `source` it from `build.sh` (and the matching scripts) so all three scripts
use one implementation and stay in sync.
| [Service] | ||
| Type=simple | ||
| User=mytv | ||
| Group=mytv | ||
| WorkingDirectory=@REPO_ROOT@/back-end | ||
| ExecStart=@NODE_BIN@ dist/index.js | ||
| Restart=on-failure | ||
| RestartSec=5 | ||
| Environment=NODE_ENV=production | ||
| Environment=PORT=80 | ||
| Environment=HOST=0.0.0.0 | ||
| Environment=LOG_LEVEL=info | ||
| Environment=SERVE_SPA=1 | ||
| # HOME is what back-end/src/tv/token-store.ts resolves against when | ||
| # XDG_CONFIG_HOME is unset: tokens land at /var/lib/mytv/.mytv/tokens.json | ||
| # (0600, owned by the mytv user). Do NOT set XDG_CONFIG_HOME here — it | ||
| # would shift the token file to $XDG_CONFIG_HOME/mytv/tokens.json and | ||
| # desync from the path the docs / uninstall recipe reference. | ||
| Environment=HOME=/var/lib/mytv | ||
| StandardOutput=journal | ||
| StandardError=journal | ||
| SyslogIdentifier=mytv | ||
|
|
There was a problem hiding this comment.
🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win
Варто додати базове systemd-хардення для сервісу, що слухає в мережі.
Юніт запускається під окремим системним юзером без входу — це вже добре, — але немає жодних sandboxing-директив (NoNewPrivileges, ProtectSystem, ProtectHome, PrivateTmp). Для пристрою, що приймає підключення в локальній мережі, кілька рядків суттєво звужують поверхню атаки без ризику зламати cap_net_bind_service (яка виставляється через setcap на бінарник, а не через AmbientCapabilities).
🛡️ Пропоноване доповнення
[Service]
Type=simple
User=mytv
Group=mytv
WorkingDirectory=`@REPO_ROOT`@/back-end
ExecStart=`@NODE_BIN`@ dist/index.js
Restart=on-failure
RestartSec=5
+NoNewPrivileges=true
+ProtectSystem=strict
+ProtectHome=true
+PrivateTmp=true
+ReadWritePaths=/var/lib/mytv
Environment=NODE_ENV=production📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| [Service] | |
| Type=simple | |
| User=mytv | |
| Group=mytv | |
| WorkingDirectory=@REPO_ROOT@/back-end | |
| ExecStart=@NODE_BIN@ dist/index.js | |
| Restart=on-failure | |
| RestartSec=5 | |
| Environment=NODE_ENV=production | |
| Environment=PORT=80 | |
| Environment=HOST=0.0.0.0 | |
| Environment=LOG_LEVEL=info | |
| Environment=SERVE_SPA=1 | |
| # HOME is what back-end/src/tv/token-store.ts resolves against when | |
| # XDG_CONFIG_HOME is unset: tokens land at /var/lib/mytv/.mytv/tokens.json | |
| # (0600, owned by the mytv user). Do NOT set XDG_CONFIG_HOME here — it | |
| # would shift the token file to $XDG_CONFIG_HOME/mytv/tokens.json and | |
| # desync from the path the docs / uninstall recipe reference. | |
| Environment=HOME=/var/lib/mytv | |
| StandardOutput=journal | |
| StandardError=journal | |
| SyslogIdentifier=mytv | |
| [Service] | |
| Type=simple | |
| User=mytv | |
| Group=mytv | |
| WorkingDirectory=`@REPO_ROOT`@/back-end | |
| ExecStart=`@NODE_BIN`@ dist/index.js | |
| Restart=on-failure | |
| RestartSec=5 | |
| NoNewPrivileges=true | |
| ProtectSystem=strict | |
| ProtectHome=true | |
| PrivateTmp=true | |
| ReadWritePaths=/var/lib/mytv | |
| Environment=NODE_ENV=production | |
| Environment=PORT=80 | |
| Environment=HOST=0.0.0.0 | |
| Environment=LOG_LEVEL=info | |
| Environment=SERVE_SPA=1 | |
| # HOME is what back-end/src/tv/token-store.ts resolves against when | |
| # XDG_CONFIG_HOME is unset: tokens land at /var/lib/mytv/.mytv/tokens.json | |
| # (0600, owned by the mytv user). Do NOT set XDG_CONFIG_HOME here — it | |
| # would shift the token file to $XDG_CONFIG_HOME/mytv/tokens.json and | |
| # desync from the path the docs / uninstall recipe reference. | |
| Environment=HOME=/var/lib/mytv | |
| StandardOutput=journal | |
| StandardError=journal | |
| SyslogIdentifier=mytv |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@scripts/mytv.service.tmpl` around lines 7 - 29, The systemd unit for the mytv
service is missing basic hardening for a network-facing daemon. Add sandboxing
directives such as NoNewPrivileges, ProtectSystem, ProtectHome, and PrivateTmp
in the [Service] section of mytv.service.tmpl, keeping the existing User, Group,
WorkingDirectory, and ExecStart setup intact. Ensure the new restrictions do not
rely on AmbientCapabilities, since the privileged bind is handled via setcap on
the binary.
| if systemctl list-unit-files mytv.service >/dev/null 2>&1; then | ||
| log "stopping and disabling mytv.service" | ||
| systemctl disable --now mytv 2>/dev/null || true | ||
| else | ||
| log "mytv.service not registered; nothing to stop" | ||
| fi |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🌐 Web query:
systemd list-unit-files exit code when pattern matches zero units
💡 Result:
The systemctl list-unit-files command returns an exit code of 0 even when the provided pattern matches zero units [1][2]. Historically, this behavior has been noted as a point of confusion for users, as it does not signal an error or a "not found" state when no files match the input pattern [1]. While some users have requested that it return a non-zero exit code (such as 1) to indicate that no matches were found, the command continues to exit with 0 in current versions of systemd [1][2]. Consequently, if you are using this command in a script to verify the existence of a unit file, you cannot rely solely on the exit code to determine if a match occurred [1][2]. You should instead check if the output is empty or process the output directly [1][2].
Citations:
- 1: list-unit-files returns 0 if no unit files found systemd/systemd#15082
- 2: https://askubuntu.com/questions/1320594/systemctl-returns-no-output-always-exits-0-no-units-or-unit-files-listed
🏁 Script executed:
#!/bin/bash
set -euo pipefail
git ls-files scripts/uninstall.sh
wc -l scripts/uninstall.sh
rg -n 'list-unit-files|UNIT_PATH|disable --now|mytv\.service' scripts/uninstall.sh
sed -n '1,120p' scripts/uninstall.shRepository: AntoniPetrenko/2026-fwdays-agentic-greenfield-task-AP
Length of output: 2128
systemctl list-unit-files тут не підходить як перевірка.
systemctl list-unit-files mytv.service повертає 0 навіть без збігів, тому else для “unit не встановлено” тут не спрацює, а лог буде оманливим. Краще перевіряти фактичний шлях юніта ([[ -f "$UNIT_PATH" ]]) або просто робити best-effort systemctl disable --now mytv із придушенням помилки.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@scripts/uninstall.sh` around lines 30 - 35, The unit-presence check in the
uninstall flow is wrong because `systemctl list-unit-files` can succeed even
when `mytv.service` is not installed, so the `else` branch and related logging
are misleading. Update the logic in `scripts/uninstall.sh` around the
`systemctl` handling to either check the actual unit file path with a filesystem
test or skip the pre-check entirely and use a best-effort `systemctl disable
--now mytv` with suppressed failure, keeping the log message aligned with the
real outcome.
| // Custom serializer for users | ||
| user: (user) => ({ | ||
| id: user.id, | ||
| email: user.email, | ||
| // Exclude sensitive fields | ||
| }), | ||
| }, | ||
| }, | ||
| }); | ||
|
|
||
| // Use custom serializer | ||
| request.log.info({ user: request.user }, 'User action'); |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
sed -n '130,190p' .agents/skills/fastify-best-practices/rules/logging.mdRepository: AntoniPetrenko/2026-fwdays-agentic-greenfield-task-AP
Length of output: 2128
Не серіалізуйте email у логах. У цьому прикладі user-serializer усе ще повертає PII, тож для логування залиште лише безпечний ідентифікатор.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In @.agents/skills/fastify-best-practices/rules/logging.md around lines 151 -
162, The custom `user` serializer is still emitting PII by including `email`, so
update the serializer to return only a safe identifier such as `id` and remove
any sensitive user fields from the logged object. Keep the `request.log.info({
user: request.user }, ...)` usage, but ensure it only receives the sanitized
output from the `user` serializer.
| ``` | ||
| src/ | ||
| routes/ | ||
| users/ | ||
| index.ts # Route definitions | ||
| handlers.ts # Handler functions | ||
| schemas.ts # JSON schemas | ||
| posts/ | ||
| index.ts | ||
| handlers.ts | ||
| schemas.ts | ||
| ``` |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Дайте обом fenced-блокам мову.
Ці два блоки з деревом директорій піднімають markdownlint warning. Позначте їх як text.
🧹 Пропонована правка
-```
+```textЗробіть те саме для другого такого блоку нижче.
Also applies to: 436-448
🧰 Tools
🪛 markdownlint-cli2 (0.22.1)
[warning] 195-195: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In @.agents/skills/fastify-best-practices/rules/routes.md around lines 195 -
206, Two fenced directory-tree blocks in the markdown rule doc are missing a
language tag and trigger markdownlint warnings. Update both code fences to use
the text language so the examples remain rendered as plain text; check the
duplicated tree example in the same document and keep the formatting consistent
across both blocks.
Source: Linters/SAST tools
| --- | ||
| name: fastify-best-practices | ||
| description: "Guides development of Fastify Node.js backend servers and REST APIs using TypeScript or JavaScript. Use when building, configuring, or debugging a Fastify application — including defining routes, implementing plugins, setting up JSON Schema validation, handling errors, optimising performance, managing authentication, configuring CORS and security headers, integrating databases, working with WebSockets, and deploying to production. Covers the full Fastify request lifecycle (hooks, serialization, logging with Pino) and TypeScript integration via strip types. Trigger terms: Fastify, Node.js server, REST API, API routes, backend framework, fastify.config, server.ts, app.ts." | ||
| metadata: | ||
| tags: fastify, nodejs, typescript, backend, api, server, http | ||
| --- |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Виправ MD041: перший рядок має бути top-level heading.
Лінтер вказує, що файл починається з YAML front-matter, а не з # Heading. Для markdownlint-стандарту варто або додати # Fastify Best Practices заголовок після front-matter, або налаштувати виняток для front-matter у конфіг лінтера.
📝 Пропоноване виправлення
---
name: fastify-best-practices
description: "..."
metadata:
tags: fastify, nodejs, typescript, backend, api, server, http
---
+
+# Fastify Best Practices
## When to use📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| --- | |
| name: fastify-best-practices | |
| description: "Guides development of Fastify Node.js backend servers and REST APIs using TypeScript or JavaScript. Use when building, configuring, or debugging a Fastify application — including defining routes, implementing plugins, setting up JSON Schema validation, handling errors, optimising performance, managing authentication, configuring CORS and security headers, integrating databases, working with WebSockets, and deploying to production. Covers the full Fastify request lifecycle (hooks, serialization, logging with Pino) and TypeScript integration via strip types. Trigger terms: Fastify, Node.js server, REST API, API routes, backend framework, fastify.config, server.ts, app.ts." | |
| metadata: | |
| tags: fastify, nodejs, typescript, backend, api, server, http | |
| --- | |
| --- | |
| name: fastify-best-practices | |
| description: "Guides development of Fastify Node.js backend servers and REST APIs using TypeScript or JavaScript. Use when building, configuring, or debugging a Fastify application — including defining routes, implementing plugins, setting up JSON Schema validation, handling errors, optimising performance, managing authentication, configuring CORS and security headers, integrating databases, working with WebSockets, and deploying to production. Covers the full Fastify request lifecycle (hooks, serialization, logging with Pino) and TypeScript integration via strip types. Trigger terms: Fastify, Node.js server, REST API, API routes, backend framework, fastify.config, server.ts, app.ts." | |
| metadata: | |
| tags: fastify, nodejs, typescript, backend, api, server, http | |
| --- | |
| # Fastify Best Practices |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In @.agents/skills/fastify-best-practices/SKILL.md around lines 1 - 6, File
start violates MD041 because the markdown begins with YAML front-matter instead
of a top-level heading. Update the SKILL.md content so the front-matter is
followed by a first-line H1 like “Fastify Best Practices,” or ensure the
markdownlint configuration explicitly allows front-matter before the heading.
Reference the existing front-matter block in SKILL.md when making the change.
Source: Linters/SAST tools
| ```typescript | ||
| // ✅ Good: vite.config.ts - Configure SVGR plugin | ||
| import { defineConfig } from 'vite' | ||
| import react from '@vitejs/plugin-react' | ||
| import svgr from 'vite-plugin-svgr' | ||
|
|
||
| export default defineConfig({ | ||
| plugins: [ | ||
| react(), | ||
| svgr({ | ||
| exportAsDefault: false, | ||
| svgrOptions: { | ||
| plugins: ['@svgr/plugin-svgo', '@svgr/plugin-jsx'], | ||
| svgoConfig: { | ||
| plugins: [ | ||
| { | ||
| name: 'removeViewBox', | ||
| active: false, // Keep viewBox for scaling | ||
| }, | ||
| ], | ||
| }, | ||
| }, | ||
| }), | ||
| ], | ||
| }) | ||
| ``` | ||
|
|
||
| ```typescript | ||
| // ✅ Good: Import as React component for full styling control | ||
| import Logo from './assets/logo.svg?react' | ||
| import logoUrl from './assets/logo.svg' | ||
|
|
||
| function Header() { | ||
| return ( | ||
| <header> | ||
| {/* As component - fully styleable */} | ||
| <Logo className="w-8 h-8 text-blue-600 hover:text-blue-700" /> | ||
|
|
||
| {/* As image when styling isn't needed */} | ||
| <img src={logoUrl} alt="Logo" className="w-8 h-8" /> | ||
| </header> | ||
| ) | ||
| } | ||
| ``` | ||
|
|
||
| ```typescript | ||
| // ✅ Good: TypeScript support | ||
| // src/vite-env.d.ts | ||
| /// <reference types="vite/client" /> | ||
| /// <reference types="vite-plugin-svgr/client" /> | ||
|
|
||
| declare module '*.svg?react' { | ||
| import type { FunctionComponent, SVGProps } from 'react' | ||
| const content: FunctionComponent<SVGProps<SVGSVGElement>> | ||
| export default content | ||
| } | ||
|
|
||
| declare module '*.svg' { | ||
| const content: string | ||
| export default content | ||
| } | ||
| ``` | ||
|
|
||
| ```typescript | ||
| // ✅ Good: Dynamic SVG colors via currentColor | ||
| import SearchIcon from './assets/search.svg?react' | ||
|
|
||
| function SearchButton({ active }: { active: boolean }) { | ||
| return ( | ||
| <button className={active ? 'text-blue-600' : 'text-gray-400'}> | ||
| <SearchIcon className="w-5 h-5" /> | ||
| Search | ||
| </button> | ||
| ) | ||
| } | ||
| ``` | ||
|
|
||
| ```typescript | ||
| // ✅ Good: Icon component pattern with tree shaking | ||
| import type { SVGProps, FunctionComponent } from 'react' | ||
|
|
||
| import HomeIcon from '@/assets/icons/home.svg?react' | ||
| import SettingsIcon from '@/assets/icons/settings.svg?react' | ||
| import UserIcon from '@/assets/icons/user.svg?react' | ||
|
|
||
| const icons = { | ||
| home: HomeIcon, | ||
| settings: SettingsIcon, | ||
| user: UserIcon, | ||
| } as const | ||
|
|
||
| type IconName = keyof typeof icons | ||
|
|
||
| interface IconProps extends SVGProps<SVGSVGElement> { | ||
| name: IconName | ||
| size?: number | ||
| } | ||
|
|
||
| export function Icon({ name, size = 24, className, ...props }: IconProps) { | ||
| const IconComponent = icons[name] | ||
| return ( | ||
| <IconComponent | ||
| width={size} | ||
| height={size} | ||
| className={className} | ||
| {...props} | ||
| /> | ||
| ) | ||
| } | ||
|
|
||
| // Usage | ||
| // <Icon name="home" size={20} className="text-gray-600" /> | ||
| ``` |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win
Обмежте це правило рамками mytv UI.
Нинішні приклади вчать агентів тягнути svg?react + Tailwind у фронтенд. Для цього репозиторію це конфліктує з Orbit DS: іконки мають лишатися Material Symbols Rounded, а не довільні SVG-компоненти. Додайте явну ремарку, що ця техніка допустима лише для не-UI assetів або DS-owned іконок.
As per coding guidelines and path instructions, front-end/src/**/*.{ts,tsx,js,jsx,css,html}: use DS primitives and Material Symbols Rounded only.
Пропозиція
+// NOTE: In mytv UI, keep SVG-as-component guidance only for non-UI artwork.
+// UI icons must still come from Orbit DS / Material Symbols Rounded.📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| ```typescript | |
| // ✅ Good: vite.config.ts - Configure SVGR plugin | |
| import { defineConfig } from 'vite' | |
| import react from '@vitejs/plugin-react' | |
| import svgr from 'vite-plugin-svgr' | |
| export default defineConfig({ | |
| plugins: [ | |
| react(), | |
| svgr({ | |
| exportAsDefault: false, | |
| svgrOptions: { | |
| plugins: ['@svgr/plugin-svgo', '@svgr/plugin-jsx'], | |
| svgoConfig: { | |
| plugins: [ | |
| { | |
| name: 'removeViewBox', | |
| active: false, // Keep viewBox for scaling | |
| }, | |
| ], | |
| }, | |
| }, | |
| }), | |
| ], | |
| }) | |
| ``` | |
| ```typescript | |
| // ✅ Good: Import as React component for full styling control | |
| import Logo from './assets/logo.svg?react' | |
| import logoUrl from './assets/logo.svg' | |
| function Header() { | |
| return ( | |
| <header> | |
| {/* As component - fully styleable */} | |
| <Logo className="w-8 h-8 text-blue-600 hover:text-blue-700" /> | |
| {/* As image when styling isn't needed */} | |
| <img src={logoUrl} alt="Logo" className="w-8 h-8" /> | |
| </header> | |
| ) | |
| } | |
| ``` | |
| ```typescript | |
| // ✅ Good: TypeScript support | |
| // src/vite-env.d.ts | |
| /// <reference types="vite/client" /> | |
| /// <reference types="vite-plugin-svgr/client" /> | |
| declare module '*.svg?react' { | |
| import type { FunctionComponent, SVGProps } from 'react' | |
| const content: FunctionComponent<SVGProps<SVGSVGElement>> | |
| export default content | |
| } | |
| declare module '*.svg' { | |
| const content: string | |
| export default content | |
| } | |
| ``` | |
| ```typescript | |
| // ✅ Good: Dynamic SVG colors via currentColor | |
| import SearchIcon from './assets/search.svg?react' | |
| function SearchButton({ active }: { active: boolean }) { | |
| return ( | |
| <button className={active ? 'text-blue-600' : 'text-gray-400'}> | |
| <SearchIcon className="w-5 h-5" /> | |
| Search | |
| </button> | |
| ) | |
| } | |
| ``` | |
| ```typescript | |
| // ✅ Good: Icon component pattern with tree shaking | |
| import type { SVGProps, FunctionComponent } from 'react' | |
| import HomeIcon from '@/assets/icons/home.svg?react' | |
| import SettingsIcon from '@/assets/icons/settings.svg?react' | |
| import UserIcon from '@/assets/icons/user.svg?react' | |
| const icons = { | |
| home: HomeIcon, | |
| settings: SettingsIcon, | |
| user: UserIcon, | |
| } as const | |
| type IconName = keyof typeof icons | |
| interface IconProps extends SVGProps<SVGSVGElement> { | |
| name: IconName | |
| size?: number | |
| } | |
| export function Icon({ name, size = 24, className, ...props }: IconProps) { | |
| const IconComponent = icons[name] | |
| return ( | |
| <IconComponent | |
| width={size} | |
| height={size} | |
| className={className} | |
| {...props} | |
| /> | |
| ) | |
| } | |
| // Usage | |
| // <Icon name="home" size={20} className="text-gray-600" /> | |
| ``` |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In @.agents/skills/react-vite-best-practices/rules/asset-svg-components.md
around lines 44 - 156, The SVG-to-component guidance is too broad and conflicts
with Orbit DS icon rules, so narrow these examples in the asset-svg-components
rule. Update the guidance around the vite-plugin-svgr setup, the `*.svg?react`
import pattern, and the `Icon` component example to explicitly say this approach
is only allowed for non-UI assets or DS-owned icons, not general mytv UI. Add a
clear note that frontend UI should continue using DS primitives and Material
Symbols Rounded only, so agents don’t adopt arbitrary SVG components in
`front-end/src/**/*.{ts,tsx,js,jsx,css,html}`.
Sources: Coding guidelines, Path instructions
| manualChunks(id) { | ||
| // Node modules go to vendor chunk | ||
| if (id.includes('node_modules')) { | ||
| // Split large libraries into separate chunks | ||
| if (id.includes('react-dom')) { | ||
| return 'vendor-react-dom' | ||
| } | ||
| if (id.includes('react')) { | ||
| return 'vendor-react' | ||
| } | ||
| if (id.includes('@tanstack')) { | ||
| return 'vendor-tanstack' | ||
| } | ||
| // Other node_modules | ||
| return 'vendor' | ||
| } | ||
| }, |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Виправте динамічний поділ чанків.
id.includes('react') перехоплює react-router-dom та інші специфічні пакети, тож приклад уже не відповідає попередньому vendor-router. Спочатку перевіряйте точні пакети, потім — загальний react.
Пропонований патч
manualChunks(id) {
if (id.includes('node_modules')) {
+ if (id.includes('react-router-dom')) {
+ return 'vendor-router'
+ }
+ if (id.includes('`@tanstack`')) {
+ return 'vendor-tanstack'
+ }
if (id.includes('react-dom')) {
return 'vendor-react-dom'
}
if (id.includes('react')) {
return 'vendor-react'📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| manualChunks(id) { | |
| // Node modules go to vendor chunk | |
| if (id.includes('node_modules')) { | |
| // Split large libraries into separate chunks | |
| if (id.includes('react-dom')) { | |
| return 'vendor-react-dom' | |
| } | |
| if (id.includes('react')) { | |
| return 'vendor-react' | |
| } | |
| if (id.includes('@tanstack')) { | |
| return 'vendor-tanstack' | |
| } | |
| // Other node_modules | |
| return 'vendor' | |
| } | |
| }, | |
| manualChunks(id) { | |
| // Node modules go to vendor chunk | |
| if (id.includes('node_modules')) { | |
| // Split large libraries into separate chunks | |
| if (id.includes('react-router-dom')) { | |
| return 'vendor-router' | |
| } | |
| if (id.includes('`@tanstack`')) { | |
| return 'vendor-tanstack' | |
| } | |
| if (id.includes('react-dom')) { | |
| return 'vendor-react-dom' | |
| } | |
| if (id.includes('react')) { | |
| return 'vendor-react' | |
| } | |
| // Other node_modules | |
| return 'vendor' | |
| } | |
| }, |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In @.agents/skills/react-vite-best-practices/rules/build-manual-chunks.md around
lines 70 - 86, The manual chunking logic in manualChunks currently checks
id.includes('react') too early, which can incorrectly capture react-router-dom
and other specific packages. Update the matching order so more specific package
checks (such as react-router-dom and react-dom) run before the generic react
case, and keep the existing vendor-react, vendor-react-dom, and vendor-router
chunk names aligned with the intended split behavior.
| <div | ||
| style={{ | ||
| minHeight: '100vh', | ||
| display: 'flex', | ||
| alignItems: 'center', | ||
| justifyContent: 'center', | ||
| padding: 'var(--space-8)', | ||
| background: 'var(--base-100)', | ||
| }} | ||
| > | ||
| <Card style={{ maxWidth: 480, textAlign: 'center' }}> | ||
| <span | ||
| className="material-symbols-rounded" | ||
| aria-hidden="true" | ||
| style={{ fontSize: 48, color: 'var(--offline)' }} | ||
| > | ||
| error_outline | ||
| </span> | ||
| <div | ||
| style={{ | ||
| fontSize: 'var(--text-h2)', | ||
| fontWeight: 'var(--weight-bold)', | ||
| color: 'var(--fg-1)', | ||
| marginTop: 'var(--space-4)', | ||
| }} | ||
| > | ||
| Something broke. | ||
| </div> | ||
| <div | ||
| style={{ | ||
| fontSize: 'var(--text-body)', | ||
| color: 'var(--fg-2)', | ||
| marginTop: 'var(--space-2)', | ||
| }} | ||
| > | ||
| The remote screen ran into an unexpected error. Try reloading. | ||
| </div> | ||
| <div style={{ marginTop: 'var(--space-5)' }}> | ||
| <Button variant="primary" onClick={this.handleReload}> | ||
| Try again | ||
| </Button> | ||
| </div> | ||
| </Card> | ||
| </div> | ||
| ); |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win
Layout зроблено на raw <div style={{...}}>, а не через DS-примітиви.
Токени використані правильно, але сама структура (flex-контейнер, картка-обгортка) не йде через @ds/components/**. Якщо в DS немає відповідного layout-примітиву (Stack/Box/Screen) — його варто додати, а не інлайнити стилі напряму в фолбек-екрані.
As per coding guidelines: "every UI element should come from @ds/components/**, and missing primitives should be added to the design system instead of using inline styles."
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@front-end/src/ui/ErrorBoundary.tsx` around lines 41 - 85, The error fallback
in ErrorBoundary is using raw divs with inline layout styles instead of DS
primitives. Refactor the render path in ErrorBoundary to build the full screen,
centering, and card layout using components from `@ds/components/`** (for example
a screen/container/stack/card primitive), and if the needed layout primitive
does not exist, add it to the design system rather than keeping inline styles in
the fallback UI.
Source: Coding guidelines
| Something broke. | ||
| </div> | ||
| <div | ||
| style={{ | ||
| fontSize: 'var(--text-body)', | ||
| color: 'var(--fg-2)', | ||
| marginTop: 'var(--space-2)', | ||
| }} | ||
| > | ||
| The remote screen ran into an unexpected error. Try reloading. | ||
| </div> |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Копірайт fallback-екрана не у "second person".
"Something broke." / "The remote screen ran into an unexpected error." — це опис від третьої особи, а не звернення до користувача.
As per coding guidelines: "Keep UI copy in English, sentence case, and second person."
✏️ Приклад переформулювання
- Something broke.
+ You've hit a snag.
...
- The remote screen ran into an unexpected error. Try reloading.
+ You ran into an unexpected error on the remote screen. Try reloading.📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| Something broke. | |
| </div> | |
| <div | |
| style={{ | |
| fontSize: 'var(--text-body)', | |
| color: 'var(--fg-2)', | |
| marginTop: 'var(--space-2)', | |
| }} | |
| > | |
| The remote screen ran into an unexpected error. Try reloading. | |
| </div> | |
| You've hit a snag. | |
| </div> | |
| <div | |
| style={{ | |
| fontSize: 'var(--text-body)', | |
| color: 'var(--fg-2)', | |
| marginTop: 'var(--space-2)', | |
| }} | |
| > | |
| You ran into an unexpected error on the remote screen. Try reloading. |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@front-end/src/ui/ErrorBoundary.tsx` around lines 67 - 77, The fallback copy
in ErrorBoundary is written in third person, but the UI guidelines require
English sentence case in second person. Update the strings in the ErrorBoundary
render path so they speak directly to the user (for example, revise the messages
shown in the “Something broke.” block and the “The remote screen ran into an
unexpected error...” block), while keeping the tone concise and consistent with
the existing ErrorBoundary component.
Source: Coding guidelines
| return ( | ||
| <div | ||
| aria-live="polite" | ||
| style={{ | ||
| position: 'fixed', | ||
| right: 'var(--space-8)', | ||
| bottom: 'var(--space-8)', | ||
| display: 'flex', | ||
| flexDirection: 'column', | ||
| gap: 'var(--space-3)', | ||
| zIndex: 1100, | ||
| pointerEvents: 'none', | ||
| }} | ||
| > |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win
Той самий inline-style-layout патерн, що й у ErrorBoundary.tsx.
Контейнер-стек toast-ів зроблено через raw <div style={{...}}> замість DS-примітиву. Токени коректні, але це ще один кандидат на layout-компонент у дизайн-системі.
As per coding guidelines: "every UI element should come from @ds/components/**, and missing primitives should be added to the design system instead of using inline styles."
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@front-end/src/ui/ToastHost.tsx` around lines 14 - 27, The toast container in
ToastHost uses a raw inline-styled <div> for layout, but the guideline requires
all UI elements to come from `@ds/components`. Replace this stack wrapper with an
appropriate design-system layout primitive (or add one to the DS if missing) and
keep the ToastHost composition using that component instead of inline styles.
Source: Coding guidelines
| export interface Toast { | ||
| id: string; | ||
| tone: ToastTone; | ||
| message: string; | ||
| createdAt: number; | ||
| /** Set when the toast is hovered; auto-dismiss timer pauses until leave. */ | ||
| pausedAt?: number; | ||
| /** Remaining ms until auto-dismiss when unpaused. */ | ||
| remainingMs: number; | ||
| } |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value
Назва типу Toast конфліктує з DS-компонентом Toast з @ds/components/feedback/Toast.jsx.
Наразі не колізує (не імпортуються в одному файлі), але варто перейменувати на ToastItem/ToastState, щоб уникнути потреби в alias-імпортах пізніше.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@front-end/src/ui/useToast.ts` around lines 14 - 23, The local Toast type name
conflicts conceptually with the DS Toast component, so rename the interface in
useToast.ts to a more specific identifier like ToastItem or ToastState. Update
all references within the toast hook/state helpers to use the new name,
especially the Toast interface and any functions or arrays in useToast that
consume it, to avoid future alias-import collisions.
| // Auto-dismiss: schedule a per-toast timer for whatever remainingMs is | ||
| // left, keyed by `toast.createdAt` so a resume that resets `createdAt` | ||
| // re-runs this effect and refreshes the timer. | ||
| useEffect(() => { | ||
| const timers: Array<ReturnType<typeof setTimeout>> = []; | ||
| for (const toast of state.toasts) { | ||
| if (toast.pausedAt) continue; | ||
| const timer = setTimeout(() => { | ||
| dispatch({ type: 'dismiss', id: toast.id }); | ||
| }, toast.remainingMs); | ||
| timers.push(timer); | ||
| } | ||
| return () => { | ||
| for (const t of timers) clearTimeout(t); | ||
| }; | ||
| }, [state.toasts]); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Auto-dismiss таймер перезапускається "з нуля" при будь-якій іншій дії з toast-ами — реальні toast-и живуть довше за задекларовані 5с.
useEffect залежить від [state.toasts], тож будь-який push/dismiss/pause/resume ІНШОГО toast чистить і перепланує таймер поточного toast-а з повним toast.remainingMs, а не з урахуванням часу, що вже минув з createdAt. При активному toast-трафіку (наприклад, кілька помилок поспіль) старіші toast-и можуть висіти довше 5с або взагалі "нескінченно" продовжувати оновлюватись.
Тест ToastHost.test.tsx цей сценарій не ловить, бо перевіряє лише один toast без паралельного push.
🐛 Пропонований фікс: рахувати delay від фактично минулого часу
useEffect(() => {
const timers: Array<ReturnType<typeof setTimeout>> = [];
for (const toast of state.toasts) {
if (toast.pausedAt) continue;
- const timer = setTimeout(() => {
- dispatch({ type: 'dismiss', id: toast.id });
- }, toast.remainingMs);
+ const delay = Math.max(0, toast.remainingMs - (clock() - toast.createdAt));
+ const timer = setTimeout(() => {
+ dispatch({ type: 'dismiss', id: toast.id });
+ }, delay);
timers.push(timer);
}
return () => {
for (const t of timers) clearTimeout(t);
};
- }, [state.toasts]);
+ }, [state.toasts, clock]);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // Auto-dismiss: schedule a per-toast timer for whatever remainingMs is | |
| // left, keyed by `toast.createdAt` so a resume that resets `createdAt` | |
| // re-runs this effect and refreshes the timer. | |
| useEffect(() => { | |
| const timers: Array<ReturnType<typeof setTimeout>> = []; | |
| for (const toast of state.toasts) { | |
| if (toast.pausedAt) continue; | |
| const timer = setTimeout(() => { | |
| dispatch({ type: 'dismiss', id: toast.id }); | |
| }, toast.remainingMs); | |
| timers.push(timer); | |
| } | |
| return () => { | |
| for (const t of timers) clearTimeout(t); | |
| }; | |
| }, [state.toasts]); | |
| // Auto-dismiss: schedule a per-toast timer for whatever remainingMs is | |
| // left, keyed by `toast.createdAt` so a resume that resets `createdAt` | |
| // re-runs this effect and refreshes the timer. | |
| useEffect(() => { | |
| const timers: Array<ReturnType<typeof setTimeout>> = []; | |
| for (const toast of state.toasts) { | |
| if (toast.pausedAt) continue; | |
| const delay = Math.max(0, toast.remainingMs - (clock() - toast.createdAt)); | |
| const timer = setTimeout(() => { | |
| dispatch({ type: 'dismiss', id: toast.id }); | |
| }, delay); | |
| timers.push(timer); | |
| } | |
| return () => { | |
| for (const t of timers) clearTimeout(t); | |
| }; | |
| }, [state.toasts, clock]); |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@front-end/src/ui/useToast.ts` around lines 152 - 167, The auto-dismiss timer
in useToast is being reset to the full remaining duration whenever any toast
list change reruns the effect, so existing toasts can live longer than intended.
Update the Auto-dismiss useEffect in useToast to compute each timeout from the
actual elapsed time since the toast’s createdAt (or equivalent persisted start
timestamp) instead of blindly using toast.remainingMs, and ensure dismiss
scheduling for a given toast is stable across unrelated state.toasts updates.
Also add/adjust a ToastHost.test.tsx case that simulates multiple concurrent
toast pushes so the original toast still dismisses after the intended 5s window.
✅ Action performedFull review finished. |
There was a problem hiding this comment.
Actionable comments posted: 113
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (4)
.agents/skills/react-vite-best-practices/README.md (1)
17-49: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick winПриведіть README до
markdownlint-сумісного вигляду.Зараз тут одночасно MD022 і MD040: заголовки без порожніх рядків та fenced block без мови. Це дрібно, але ламатиме doc-lint у CI.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In @.agents/skills/react-vite-best-practices/README.md around lines 17 - 49, The README markdown currently violates markdownlint rules MD022 and MD040. Add the required blank lines around the section headings in the React Vite best practices list, and update the fenced example block in the Usage section to include a language identifier. Keep the structure and content the same, just adjust the formatting so the README passes doc-lint.Source: Linters/SAST tools
back-end/src/logger.test.ts (1)
1-66: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winБракує тесту на редакцію ключа
authorization.
SENSITIVE_KEYSвlogger.tsмістить'authorization', але жоден тест тут це не перевіряє — покриття неповне для одного з трьох захищених ключів.✅ Пропонований тест
+test('redacts a lower-case `authorization` key', () => { + const { logger, output } = captureLogger(); + logger.info({ headers: { authorization: 'Bearer S3CR3T-AUTH' } }, 'incoming request'); + assert.ok(!output().includes('S3CR3T-AUTH')); +});🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@back-end/src/logger.test.ts` around lines 1 - 66, Add a test in logger.test.ts to cover redaction of the lower-case authorization key, since SENSITIVE_KEYS in logger.ts already includes it but no existing assertion verifies it. Extend the captureLogger-based tests with a new case that logs an object containing authorization and asserts the sensitive value does not appear in output, following the same pattern used by the AccessToken and token redaction tests.front-end/README.md (1)
1-76: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winREADME все ще шаблонний create-vite текст — але це не блокер.
Файл не описує сам продукт mytv (screens, dev/build команди, зв'язок із back-end на одному origin). Дрібниця для onboarding, не критично.
Окремо, як ментор курсу: опис PR дає гарні докази виконання вимог (1)-(4) — вказано реальне ім'я автора, є посилання на відео-демо, є змістовний опис агентних практик (maker≠checker через окремого
code-reviewerсуб-агента, цикл через/next-change, OpenSpec specs+verification gates), і результат доведено до робочого end-to-end стану, а не "згенеровано і кинуто". Гарна робота з процесом! 👏🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@front-end/README.md` around lines 1 - 76, The README still contains the default create-vite template instead of project-specific onboarding content. Update the top-level README to describe mytv: what the app does, the main screens/features, how to run dev/build, and how it connects to the back-end on the same origin. Keep it focused on this repo’s actual setup and remove the generic Vite/template guidance.Source: Path instructions
openspec/changes/archive/2026-07-06-systemd-installer/design.md (1)
1-162: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick winДодайте H1 на початок документа.
## Contextна першому рядку знову тригерить MD041. Краще почати з верхнього заголовка, а вже потім іти в секції.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@openspec/changes/archive/2026-07-06-systemd-installer/design.md` around lines 1 - 162, The document starts with a second-level heading, which triggers the markdown lint rule for missing top-level title. Add a single H1 at the beginning of the design doc, before the current Context section, and keep the existing section structure under it so the file still reads correctly.Source: Linters/SAST tools
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 98833b3b-6975-4c13-bf58-e9fbed6ec9b9
⛔ Files ignored due to path filters (87)
back-end/package-lock.jsonis excluded by!**/package-lock.jsondocs/orbit-tv-remote-design-system/SKILL.mdis excluded by!docs/orbit-tv-remote-design-system/**docs/orbit-tv-remote-design-system/_adherence.oxlintrc.jsonis excluded by!docs/orbit-tv-remote-design-system/**docs/orbit-tv-remote-design-system/_ds_bundle.jsis excluded by!docs/orbit-tv-remote-design-system/**docs/orbit-tv-remote-design-system/_ds_manifest.jsonis excluded by!docs/orbit-tv-remote-design-system/**docs/orbit-tv-remote-design-system/components/controls/AppShortcut.d.tsis excluded by!docs/orbit-tv-remote-design-system/**docs/orbit-tv-remote-design-system/components/controls/AppShortcut.jsxis excluded by!docs/orbit-tv-remote-design-system/**docs/orbit-tv-remote-design-system/components/controls/AppShortcut.prompt.mdis excluded by!docs/orbit-tv-remote-design-system/**docs/orbit-tv-remote-design-system/components/controls/DPad.d.tsis excluded by!docs/orbit-tv-remote-design-system/**docs/orbit-tv-remote-design-system/components/controls/DPad.jsxis excluded by!docs/orbit-tv-remote-design-system/**docs/orbit-tv-remote-design-system/components/controls/DPad.prompt.mdis excluded by!docs/orbit-tv-remote-design-system/**docs/orbit-tv-remote-design-system/components/controls/RotaryKnob.d.tsis excluded by!docs/orbit-tv-remote-design-system/**docs/orbit-tv-remote-design-system/components/controls/RotaryKnob.jsxis excluded by!docs/orbit-tv-remote-design-system/**docs/orbit-tv-remote-design-system/components/controls/RotaryKnob.prompt.mdis excluded by!docs/orbit-tv-remote-design-system/**docs/orbit-tv-remote-design-system/components/controls/controls.card.htmlis excluded by!docs/orbit-tv-remote-design-system/**docs/orbit-tv-remote-design-system/components/core/Badge.d.tsis excluded by!docs/orbit-tv-remote-design-system/**docs/orbit-tv-remote-design-system/components/core/Badge.jsxis excluded by!docs/orbit-tv-remote-design-system/**docs/orbit-tv-remote-design-system/components/core/Badge.prompt.mdis excluded by!docs/orbit-tv-remote-design-system/**docs/orbit-tv-remote-design-system/components/core/Button.d.tsis excluded by!docs/orbit-tv-remote-design-system/**docs/orbit-tv-remote-design-system/components/core/Button.jsxis excluded by!docs/orbit-tv-remote-design-system/**docs/orbit-tv-remote-design-system/components/core/Button.prompt.mdis excluded by!docs/orbit-tv-remote-design-system/**docs/orbit-tv-remote-design-system/components/core/Card.d.tsis excluded by!docs/orbit-tv-remote-design-system/**docs/orbit-tv-remote-design-system/components/core/Card.jsxis excluded by!docs/orbit-tv-remote-design-system/**docs/orbit-tv-remote-design-system/components/core/Card.prompt.mdis excluded by!docs/orbit-tv-remote-design-system/**docs/orbit-tv-remote-design-system/components/core/DeviceCard.d.tsis excluded by!docs/orbit-tv-remote-design-system/**docs/orbit-tv-remote-design-system/components/core/DeviceCard.jsxis excluded by!docs/orbit-tv-remote-design-system/**docs/orbit-tv-remote-design-system/components/core/DeviceCard.prompt.mdis excluded by!docs/orbit-tv-remote-design-system/**docs/orbit-tv-remote-design-system/components/core/IconButton.d.tsis excluded by!docs/orbit-tv-remote-design-system/**docs/orbit-tv-remote-design-system/components/core/IconButton.jsxis excluded by!docs/orbit-tv-remote-design-system/**docs/orbit-tv-remote-design-system/components/core/IconButton.prompt.mdis excluded by!docs/orbit-tv-remote-design-system/**docs/orbit-tv-remote-design-system/components/core/ListRow.d.tsis excluded by!docs/orbit-tv-remote-design-system/**docs/orbit-tv-remote-design-system/components/core/ListRow.jsxis excluded by!docs/orbit-tv-remote-design-system/**docs/orbit-tv-remote-design-system/components/core/core.card.htmlis excluded by!docs/orbit-tv-remote-design-system/**docs/orbit-tv-remote-design-system/components/feedback/Modal.d.tsis excluded by!docs/orbit-tv-remote-design-system/**docs/orbit-tv-remote-design-system/components/feedback/Modal.jsxis excluded by!docs/orbit-tv-remote-design-system/**docs/orbit-tv-remote-design-system/components/feedback/Modal.prompt.mdis excluded by!docs/orbit-tv-remote-design-system/**docs/orbit-tv-remote-design-system/components/feedback/Toast.d.tsis excluded by!docs/orbit-tv-remote-design-system/**docs/orbit-tv-remote-design-system/components/feedback/Toast.jsxis excluded by!docs/orbit-tv-remote-design-system/**docs/orbit-tv-remote-design-system/components/feedback/Toast.prompt.mdis excluded by!docs/orbit-tv-remote-design-system/**docs/orbit-tv-remote-design-system/components/feedback/feedback.card.htmlis excluded by!docs/orbit-tv-remote-design-system/**docs/orbit-tv-remote-design-system/components/forms/Input.d.tsis excluded by!docs/orbit-tv-remote-design-system/**docs/orbit-tv-remote-design-system/components/forms/Input.jsxis excluded by!docs/orbit-tv-remote-design-system/**docs/orbit-tv-remote-design-system/components/forms/Input.prompt.mdis excluded by!docs/orbit-tv-remote-design-system/**docs/orbit-tv-remote-design-system/components/forms/Slider.d.tsis excluded by!docs/orbit-tv-remote-design-system/**docs/orbit-tv-remote-design-system/components/forms/Slider.jsxis excluded by!docs/orbit-tv-remote-design-system/**docs/orbit-tv-remote-design-system/components/forms/Slider.prompt.mdis excluded by!docs/orbit-tv-remote-design-system/**docs/orbit-tv-remote-design-system/components/forms/Toggle.d.tsis excluded by!docs/orbit-tv-remote-design-system/**docs/orbit-tv-remote-design-system/components/forms/Toggle.jsxis excluded by!docs/orbit-tv-remote-design-system/**docs/orbit-tv-remote-design-system/components/forms/Toggle.prompt.mdis excluded by!docs/orbit-tv-remote-design-system/**docs/orbit-tv-remote-design-system/components/forms/forms.card.htmlis excluded by!docs/orbit-tv-remote-design-system/**docs/orbit-tv-remote-design-system/guidelines/colors-accent.htmlis excluded by!docs/orbit-tv-remote-design-system/**docs/orbit-tv-remote-design-system/guidelines/colors-neutrals.htmlis excluded by!docs/orbit-tv-remote-design-system/**docs/orbit-tv-remote-design-system/guidelines/colors-semantic.htmlis excluded by!docs/orbit-tv-remote-design-system/**docs/orbit-tv-remote-design-system/guidelines/iconography.htmlis excluded by!docs/orbit-tv-remote-design-system/**docs/orbit-tv-remote-design-system/guidelines/radius.htmlis excluded by!docs/orbit-tv-remote-design-system/**docs/orbit-tv-remote-design-system/guidelines/shadows.htmlis excluded by!docs/orbit-tv-remote-design-system/**docs/orbit-tv-remote-design-system/guidelines/spacing.htmlis excluded by!docs/orbit-tv-remote-design-system/**docs/orbit-tv-remote-design-system/guidelines/theme-comparison.htmlis excluded by!docs/orbit-tv-remote-design-system/**docs/orbit-tv-remote-design-system/guidelines/type-scale.htmlis excluded by!docs/orbit-tv-remote-design-system/**docs/orbit-tv-remote-design-system/guidelines/type-weights.htmlis excluded by!docs/orbit-tv-remote-design-system/**docs/orbit-tv-remote-design-system/readme.mdis excluded by!docs/orbit-tv-remote-design-system/**docs/orbit-tv-remote-design-system/styles.cssis excluded by!docs/orbit-tv-remote-design-system/**docs/orbit-tv-remote-design-system/tokens/base.cssis excluded by!docs/orbit-tv-remote-design-system/**docs/orbit-tv-remote-design-system/tokens/colors.cssis excluded by!docs/orbit-tv-remote-design-system/**docs/orbit-tv-remote-design-system/tokens/fonts.cssis excluded by!docs/orbit-tv-remote-design-system/**docs/orbit-tv-remote-design-system/tokens/shadows.cssis excluded by!docs/orbit-tv-remote-design-system/**docs/orbit-tv-remote-design-system/tokens/spacing.cssis excluded by!docs/orbit-tv-remote-design-system/**docs/orbit-tv-remote-design-system/tokens/typography.cssis excluded by!docs/orbit-tv-remote-design-system/**docs/orbit-tv-remote-design-system/ui_kits/tv-remote/DeviceListScreen.jsxis excluded by!docs/orbit-tv-remote-design-system/**docs/orbit-tv-remote-design-system/ui_kits/tv-remote/README.mdis excluded by!docs/orbit-tv-remote-design-system/**docs/orbit-tv-remote-design-system/ui_kits/tv-remote/RemoteScreen.jsxis excluded by!docs/orbit-tv-remote-design-system/**docs/orbit-tv-remote-design-system/ui_kits/tv-remote/index.htmlis excluded by!docs/orbit-tv-remote-design-system/**docs/samsung-ip-control-protocol/HTV_IPControl_Protocol_20251120_v.2.8.pdfis excluded by!**/*.pdf,!docs/samsung-ip-control-protocol/**docs/samsung-ip-control-protocol/IP CONTROL V2.postman_collection.jsonis excluded by!docs/samsung-ip-control-protocol/**front-end/package-lock.jsonis excluded by!**/package-lock.jsonfront-end/public/favicon.svgis excluded by!**/*.svgfront-end/public/fonts/material-symbols-rounded.woff2is excluded by!**/*.woff2front-end/public/fonts/montserrat-400.woff2is excluded by!**/*.woff2front-end/public/fonts/montserrat-500.woff2is excluded by!**/*.woff2front-end/public/fonts/montserrat-600.woff2is excluded by!**/*.woff2front-end/public/fonts/montserrat-700.woff2is excluded by!**/*.woff2front-end/public/fonts/montserrat-800.woff2is excluded by!**/*.woff2front-end/public/icons.svgis excluded by!**/*.svgfront-end/src/assets/hero.pngis excluded by!**/*.pngfront-end/src/assets/react.svgis excluded by!**/*.svgfront-end/src/assets/vite.svgis excluded by!**/*.svgpackage-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (266)
.agents/skills/fastify-best-practices/SKILL.md.agents/skills/fastify-best-practices/rules/authentication.md.agents/skills/fastify-best-practices/rules/configuration.md.agents/skills/fastify-best-practices/rules/content-type.md.agents/skills/fastify-best-practices/rules/cors-security.md.agents/skills/fastify-best-practices/rules/database.md.agents/skills/fastify-best-practices/rules/decorators.md.agents/skills/fastify-best-practices/rules/deployment.md.agents/skills/fastify-best-practices/rules/error-handling.md.agents/skills/fastify-best-practices/rules/hooks.md.agents/skills/fastify-best-practices/rules/http-proxy.md.agents/skills/fastify-best-practices/rules/logging.md.agents/skills/fastify-best-practices/rules/performance.md.agents/skills/fastify-best-practices/rules/plugins.md.agents/skills/fastify-best-practices/rules/routes.md.agents/skills/fastify-best-practices/rules/schemas.md.agents/skills/fastify-best-practices/rules/serialization.md.agents/skills/fastify-best-practices/rules/testing.md.agents/skills/fastify-best-practices/rules/typescript.md.agents/skills/fastify-best-practices/rules/websockets.md.agents/skills/fastify-best-practices/tile.json.agents/skills/react-vite-best-practices/AGENTS.md.agents/skills/react-vite-best-practices/README.md.agents/skills/react-vite-best-practices/SKILL.md.agents/skills/react-vite-best-practices/rules/_sections.md.agents/skills/react-vite-best-practices/rules/_template.md.agents/skills/react-vite-best-practices/rules/asset-fonts.md.agents/skills/react-vite-best-practices/rules/asset-image-optimization.md.agents/skills/react-vite-best-practices/rules/asset-public-dir.md.agents/skills/react-vite-best-practices/rules/asset-svg-components.md.agents/skills/react-vite-best-practices/rules/build-asset-hashing.md.agents/skills/react-vite-best-practices/rules/build-compression.md.agents/skills/react-vite-best-practices/rules/build-manual-chunks.md.agents/skills/react-vite-best-practices/rules/build-minification.md.agents/skills/react-vite-best-practices/rules/build-sourcemaps.md.agents/skills/react-vite-best-practices/rules/build-target-modern.md.agents/skills/react-vite-best-practices/rules/build-tree-shaking.md.agents/skills/react-vite-best-practices/rules/bundle-visualizer.md.agents/skills/react-vite-best-practices/rules/dev-dependency-prebundling.md.agents/skills/react-vite-best-practices/rules/dev-fast-refresh.md.agents/skills/react-vite-best-practices/rules/dev-hmr-config.md.agents/skills/react-vite-best-practices/rules/env-modes.md.agents/skills/react-vite-best-practices/rules/env-sensitive-data.md.agents/skills/react-vite-best-practices/rules/env-vite-prefix.md.agents/skills/react-vite-best-practices/rules/split-component-lazy.md.agents/skills/react-vite-best-practices/rules/split-dynamic-imports.md.agents/skills/react-vite-best-practices/rules/split-prefetch-hints.md.agents/skills/react-vite-best-practices/rules/split-route-lazy.md.agents/skills/react-vite-best-practices/rules/split-suspense-boundaries.md.claude/agents/code-reviewer.md.claude/commands/next-change.md.claude/commands/opsx/apply.md.claude/commands/opsx/archive.md.claude/commands/opsx/explore.md.claude/commands/opsx/propose.md.claude/commands/opsx/sync.md.claude/skills/frontend-design-check/SKILL.md.claude/skills/openspec-apply-change/SKILL.md.claude/skills/openspec-archive-change/SKILL.md.claude/skills/openspec-explore/SKILL.md.claude/skills/openspec-propose/SKILL.md.claude/skills/openspec-sync-specs/SKILL.md.claude/skills/orbit-tv-remote-design.claude/skills/samsung-ip-control-protocol/SKILL.md.coderabbit.yaml.gitignoreAGENTS.mdCLAUDE.mdDESIGN.mdLOOP.mdback-end/.env.exampleback-end/.gitignoreback-end/README.mdback-end/package.jsonback-end/src/app.test.tsback-end/src/app.tsback-end/src/discovery/description.test.tsback-end/src/discovery/description.tsback-end/src/discovery/discovery.test.tsback-end/src/discovery/index.test.tsback-end/src/discovery/index.tsback-end/src/discovery/registry.test.tsback-end/src/discovery/registry.tsback-end/src/discovery/ssdp.tsback-end/src/errors.tsback-end/src/index.tsback-end/src/logger.test.tsback-end/src/logger.tsback-end/src/mdns.test.tsback-end/src/mdns.tsback-end/src/plugins/static-spa.tsback-end/src/routes/browser.test.tsback-end/src/routes/browser.tsback-end/src/routes/devices.tsback-end/src/routes/health.tsback-end/src/routes/inputs.test.tsback-end/src/routes/inputs.tsback-end/src/routes/keys.test.tsback-end/src/routes/keys.tsback-end/src/routes/sessions.tsback-end/src/routes/volume.test.tsback-end/src/routes/volume.tsback-end/src/tv/browser.test.tsback-end/src/tv/browser.tsback-end/src/tv/errors.test.tsback-end/src/tv/errors.tsback-end/src/tv/inputs.test.tsback-end/src/tv/inputs.tsback-end/src/tv/jsonrpc.test.tsback-end/src/tv/jsonrpc.tsback-end/src/tv/keys.test.tsback-end/src/tv/keys.tsback-end/src/tv/manager.tsback-end/src/tv/session-integration.test.tsback-end/src/tv/session.test.tsback-end/src/tv/session.tsback-end/src/tv/token-store.test.tsback-end/src/tv/token-store.tsback-end/src/tv/types.tsback-end/src/tv/volume.test.tsback-end/src/tv/volume.tsback-end/src/websocket.test.tsback-end/src/ws/broker.tsback-end/tsconfig.jsondocs/capabilities.mddocs/current-state.mddocs/product-brief.mddocs/requirements.mdfront-end/.gitignorefront-end/README.mdfront-end/eslint.config.jsfront-end/index.htmlfront-end/package.jsonfront-end/src/App.cssfront-end/src/App.tsxfront-end/src/api/client.tsfront-end/src/data/inputs.tsfront-end/src/data/keys.tsfront-end/src/data/types.tsfront-end/src/data/useBrowserLaunch.tsfront-end/src/data/useDeviceSession.tsfront-end/src/data/useDevices.test.tsfront-end/src/data/useDevices.tsfront-end/src/data/useInputs.tsfront-end/src/data/useSendKey.test.tsxfront-end/src/data/useSendKey.tsfront-end/src/data/useVolume.tsfront-end/src/data/useWsHealth.tsfront-end/src/ds.d.tsfront-end/src/errors/messages.test.tsfront-end/src/errors/messages.tsfront-end/src/fonts.cssfront-end/src/index.cssfront-end/src/main.tsxfront-end/src/screens/DeviceListScreen.test.tsxfront-end/src/screens/DeviceListScreen.tsxfront-end/src/screens/InputsModal.test.tsxfront-end/src/screens/InputsModal.tsxfront-end/src/screens/RemoteScreen.test.tsxfront-end/src/screens/RemoteScreen.tsxfront-end/src/ui/ErrorBoundary.tsxfront-end/src/ui/ToastHost.test.tsxfront-end/src/ui/ToastHost.tsxfront-end/src/ui/WsHealthChip.tsxfront-end/src/ui/useToast.tsfront-end/tsconfig.app.jsonfront-end/tsconfig.jsonfront-end/tsconfig.node.jsonfront-end/vite.config.tsopenspec/changes/archive/2026-07-04-default-port-80/.openspec.yamlopenspec/changes/archive/2026-07-04-default-port-80/design.mdopenspec/changes/archive/2026-07-04-default-port-80/proposal.mdopenspec/changes/archive/2026-07-04-default-port-80/specs/platform-foundation/spec.mdopenspec/changes/archive/2026-07-04-default-port-80/tasks.mdopenspec/changes/archive/2026-07-04-device-list-ui/.openspec.yamlopenspec/changes/archive/2026-07-04-device-list-ui/design.mdopenspec/changes/archive/2026-07-04-device-list-ui/proposal.mdopenspec/changes/archive/2026-07-04-device-list-ui/specs/device-list-ui/spec.mdopenspec/changes/archive/2026-07-04-device-list-ui/tasks.mdopenspec/changes/archive/2026-07-04-mdns-advertisement/.openspec.yamlopenspec/changes/archive/2026-07-04-mdns-advertisement/design.mdopenspec/changes/archive/2026-07-04-mdns-advertisement/proposal.mdopenspec/changes/archive/2026-07-04-mdns-advertisement/specs/mdns-advertisement/spec.mdopenspec/changes/archive/2026-07-04-mdns-advertisement/tasks.mdopenspec/changes/archive/2026-07-04-platform-foundation/.openspec.yamlopenspec/changes/archive/2026-07-04-platform-foundation/design.mdopenspec/changes/archive/2026-07-04-platform-foundation/proposal.mdopenspec/changes/archive/2026-07-04-platform-foundation/specs/platform-foundation/spec.mdopenspec/changes/archive/2026-07-04-platform-foundation/tasks.mdopenspec/changes/archive/2026-07-04-remote-control-keys/.openspec.yamlopenspec/changes/archive/2026-07-04-remote-control-keys/design.mdopenspec/changes/archive/2026-07-04-remote-control-keys/proposal.mdopenspec/changes/archive/2026-07-04-remote-control-keys/specs/remote-control-keys/spec.mdopenspec/changes/archive/2026-07-04-remote-control-keys/tasks.mdopenspec/changes/archive/2026-07-04-smart-view-ws-transport/.openspec.yamlopenspec/changes/archive/2026-07-04-smart-view-ws-transport/design.mdopenspec/changes/archive/2026-07-04-smart-view-ws-transport/proposal.mdopenspec/changes/archive/2026-07-04-smart-view-ws-transport/specs/tv-connection-lifecycle/spec.mdopenspec/changes/archive/2026-07-04-smart-view-ws-transport/specs/upnp-tv-discovery/spec.mdopenspec/changes/archive/2026-07-04-smart-view-ws-transport/tasks.mdopenspec/changes/archive/2026-07-04-tv-connection-lifecycle/.openspec.yamlopenspec/changes/archive/2026-07-04-tv-connection-lifecycle/design.mdopenspec/changes/archive/2026-07-04-tv-connection-lifecycle/proposal.mdopenspec/changes/archive/2026-07-04-tv-connection-lifecycle/specs/tv-connection-lifecycle/spec.mdopenspec/changes/archive/2026-07-04-tv-connection-lifecycle/tasks.mdopenspec/changes/archive/2026-07-04-upnp-tv-discovery/.openspec.yamlopenspec/changes/archive/2026-07-04-upnp-tv-discovery/design.mdopenspec/changes/archive/2026-07-04-upnp-tv-discovery/proposal.mdopenspec/changes/archive/2026-07-04-upnp-tv-discovery/specs/upnp-tv-discovery/spec.mdopenspec/changes/archive/2026-07-04-upnp-tv-discovery/tasks.mdopenspec/changes/archive/2026-07-05-error-surfacing/.openspec.yamlopenspec/changes/archive/2026-07-05-error-surfacing/design.mdopenspec/changes/archive/2026-07-05-error-surfacing/proposal.mdopenspec/changes/archive/2026-07-05-error-surfacing/specs/error-surfacing/spec.mdopenspec/changes/archive/2026-07-05-error-surfacing/tasks.mdopenspec/changes/archive/2026-07-05-input-management/.openspec.yamlopenspec/changes/archive/2026-07-05-input-management/design.mdopenspec/changes/archive/2026-07-05-input-management/proposal.mdopenspec/changes/archive/2026-07-05-input-management/specs/input-management/spec.mdopenspec/changes/archive/2026-07-05-input-management/tasks.mdopenspec/changes/archive/2026-07-05-volume-control/.openspec.yamlopenspec/changes/archive/2026-07-05-volume-control/design.mdopenspec/changes/archive/2026-07-05-volume-control/proposal.mdopenspec/changes/archive/2026-07-05-volume-control/specs/volume-control/spec.mdopenspec/changes/archive/2026-07-05-volume-control/tasks.mdopenspec/changes/archive/2026-07-06-build-install-split/.openspec.yamlopenspec/changes/archive/2026-07-06-build-install-split/design.mdopenspec/changes/archive/2026-07-06-build-install-split/proposal.mdopenspec/changes/archive/2026-07-06-build-install-split/specs/deploy-build/spec.mdopenspec/changes/archive/2026-07-06-build-install-split/specs/systemd-installer/spec.mdopenspec/changes/archive/2026-07-06-build-install-split/tasks.mdopenspec/changes/archive/2026-07-06-systemd-installer/.openspec.yamlopenspec/changes/archive/2026-07-06-systemd-installer/design.mdopenspec/changes/archive/2026-07-06-systemd-installer/proposal.mdopenspec/changes/archive/2026-07-06-systemd-installer/specs/systemd-installer/spec.mdopenspec/changes/archive/2026-07-06-systemd-installer/tasks.mdopenspec/changes/archive/2026-07-06-tv-browser-launch/.openspec.yamlopenspec/changes/archive/2026-07-06-tv-browser-launch/design.mdopenspec/changes/archive/2026-07-06-tv-browser-launch/proposal.mdopenspec/changes/archive/2026-07-06-tv-browser-launch/specs/tv-browser-launch/spec.mdopenspec/changes/archive/2026-07-06-tv-browser-launch/tasks.mdopenspec/changes/archive/2026-07-06-volume-rotary-knob/.openspec.yamlopenspec/changes/archive/2026-07-06-volume-rotary-knob/design.mdopenspec/changes/archive/2026-07-06-volume-rotary-knob/proposal.mdopenspec/changes/archive/2026-07-06-volume-rotary-knob/specs/volume-control/spec.mdopenspec/changes/archive/2026-07-06-volume-rotary-knob/tasks.mdopenspec/config.yamlopenspec/specs/deploy-build/spec.mdopenspec/specs/device-list-ui/spec.mdopenspec/specs/error-surfacing/spec.mdopenspec/specs/input-management/spec.mdopenspec/specs/mdns-advertisement/spec.mdopenspec/specs/platform-foundation/spec.mdopenspec/specs/remote-control-keys/spec.mdopenspec/specs/systemd-installer/spec.mdopenspec/specs/tv-browser-launch/spec.mdopenspec/specs/tv-connection-lifecycle/spec.mdopenspec/specs/upnp-tv-discovery/spec.mdopenspec/specs/volume-control/spec.mdpackage.jsonscripts/build.shscripts/install.shscripts/install.test.batsscripts/mytv.service.tmplscripts/uninstall.shskills-lock.json
| app.post('/auth/refresh', async (request, reply) => { | ||
| const { refreshToken } = request.body; | ||
| const stored = refreshTokens.get(refreshToken); | ||
|
|
||
| if (!stored || stored.expires < Date.now()) { | ||
| refreshTokens.delete(refreshToken); | ||
| return reply.code(401).send({ error: 'Invalid refresh token' }); | ||
| } | ||
|
|
||
| // Delete old token (rotation) | ||
| refreshTokens.delete(refreshToken); | ||
|
|
||
| const user = await db.users.findById(stored.userId); | ||
| const accessToken = app.jwt.sign({ id: user.id, role: user.role }); | ||
| const newRefreshToken = randomBytes(32).toString('hex'); |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
Перевірте, що користувач ще існує перед sign().
db.users.findById може повернути null; тоді user.id впаде, і застарілий refresh token дасть 500 замість 401.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In @.agents/skills/fastify-best-practices/rules/authentication.md around lines
110 - 124, In the refresh flow handled by app.post('/auth/refresh'),
db.users.findById can return null, so add an explicit user existence check
before calling app.jwt.sign and before reading user.id or user.role. If the user
is missing, treat the refresh token as invalid by deleting the token if needed
and returning a 401 response instead of letting the handler throw a 500.
| import fastifyOauth2 from '@fastify/oauth2'; | ||
|
|
||
| app.register(fastifyOauth2, { | ||
| name: 'googleOAuth2', | ||
| scope: ['profile', 'email'], | ||
| credentials: { | ||
| client: { | ||
| id: process.env.GOOGLE_CLIENT_ID, | ||
| secret: process.env.GOOGLE_CLIENT_SECRET, | ||
| }, | ||
| }, | ||
| startRedirectPath: '/auth/google', | ||
| callbackUri: 'http://localhost:3000/auth/google/callback', | ||
| discovery: { | ||
| issuer: 'https://accounts.google.com', | ||
| }, | ||
| }); | ||
|
|
||
| app.get('/auth/google/callback', async (request, reply) => { | ||
| const { token } = await app.googleOAuth2.getAccessTokenFromAuthorizationCodeFlow(request); | ||
|
|
||
| // Fetch user info from Google | ||
| const userInfo = await fetch('https://www.googleapis.com/oauth2/v2/userinfo', { | ||
| headers: { Authorization: `Bearer ${token.access_token}` }, | ||
| }).then((r) => r.json()); | ||
|
|
||
| // Find or create user | ||
| let user = await db.users.findByEmail(userInfo.email); | ||
| if (!user) { | ||
| user = await db.users.create({ | ||
| email: userInfo.email, | ||
| name: userInfo.name, | ||
| provider: 'google', | ||
| providerId: userInfo.id, | ||
| }); | ||
| } | ||
|
|
||
| // Generate JWT | ||
| const jwt = app.jwt.sign({ id: user.id, role: user.role }); | ||
|
|
||
| // Redirect to frontend with token | ||
| return reply.redirect(`/auth/success?token=${jwt}`); | ||
| }); |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
sed -n '260,360p' .agents/skills/fastify-best-practices/rules/authentication.mdRepository: AntoniPetrenko/2026-fwdays-agentic-greenfield-task-AP
Length of output: 2693
🏁 Script executed:
rg -n "callbackUri: 'http://localhost:3000/auth/google/callback'|reply.redirect\(`/auth/success\?token=\$\{jwt\}`\)|session-based|HttpOnly cookie|callbackUri" .agents/skills/fastify-best-practices/rules/authentication.mdRepository: AntoniPetrenko/2026-fwdays-agentic-greenfield-task-AP
Length of output: 324
Не кладіть JWT у query string редіректа. ?token=... потрапляє в history, Referer і логи. Завершуйте OAuth через session або HttpOnly cookie; callbackUri винесіть у конфіг, а не фіксуйте localhost, щоб приклад працював поза dev.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In @.agents/skills/fastify-best-practices/rules/authentication.md around lines
293 - 335, The Google OAuth callback flow in app.get('/auth/google/callback') is
exposing the signed JWT via reply.redirect query string, and the callbackUri is
hardcoded to localhost. Move the auth result into a session or an HttpOnly
cookie instead of appending token=... to the redirect URL, and make callbackUri
configurable via environment/config so the fastifyOauth2 setup works outside
local development.
| app.addHook('onRequest', async (request, reply) => { | ||
| if (dynamicConfig.maintenanceMode && !request.url.startsWith('/health')) { | ||
| reply.code(503).send({ error: 'Service under maintenance' }); | ||
| } | ||
| }); |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🌐 Web query:
Fastify async hook reply.send without return lifecycle docs onRequest
💡 Result:
In Fastify, using an async hook (such as onRequest) and calling reply.send without returning the reply instance—or awaiting the send operation—will cause the request lifecycle to continue, often leading to errors like FST_ERR_REP_ALREADY_SENT [1][2]. When you use an async function for a hook, Fastify expects the function to either return a value or resolve its promise to determine when the hook is finished [1][2]. If you call reply.send asynchronously (e.g., inside a setTimeout, setImmediate, or an asynchronous operation) and do not signal that the hook should wait, the hook function will resolve immediately, allowing the rest of the request lifecycle (including subsequent hooks and the main route handler) to execute [1][2]. To correctly respond from an async hook: 1. If calling reply.send synchronously within the async function: You can simply call it and return the result, or await it [1][3]. 2. If calling reply.send asynchronously: You must return the reply instance (e.g., return reply) to signal to Fastify that it should wait for the response to be sent before proceeding [1][2]. Example of correct pattern for asynchronous response: fastify.addHook('onRequest', async (request, reply) => { // Asynchronous operation setImmediate( => { reply.send('Early response'); }); // Mandatory: signals Fastify to wait for the response return reply; }); It is critical to avoid mixing callback-style (using the done parameter) and async/Promise-style hooks, as this can cause the hook chain to execute multiple times or lead to unexpected lifecycle behavior [1][2]. If you do not return the reply instance, Fastify will assume the hook has completed without sending a response, and will continue to process the request as normal [1][2].
Citations:
- 1: https://github.com/fastify/fastify/blob/main/docs/Reference/Hooks.md
- 2: https://github.com/fastify/fastify/blob/HEAD/docs/Reference/Hooks.md
- 3: https://fastify.dev/docs/latest/Reference/Routes/
Поверніть відповідь із async onRequest
Тут треба return reply.code(503).send(...) або throw: без цього Fastify може продовжити lifecycle, і maintenance-guard не зупинить наступні хуки чи handler.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In @.agents/skills/fastify-best-practices/rules/configuration.md around lines
212 - 216, The async onRequest hook in addHook currently sends a 503 response
for maintenance mode but does not stop the request lifecycle. Update the
onRequest callback to either return reply.code(503).send(...) or throw after
sending so Fastify does not continue to later hooks or the route handler; use
the existing onRequest and dynamicConfig.maintenanceMode guard to locate the
fix.
| // Simple CORS - allow all origins | ||
| app.register(cors); | ||
|
|
||
| // Configured CORS | ||
| app.register(cors, { | ||
| origin: ['https://example.com', 'https://app.example.com'], | ||
| methods: ['GET', 'POST', 'PUT', 'DELETE'], | ||
| allowedHeaders: ['Content-Type', 'Authorization'], | ||
| exposedHeaders: ['X-Total-Count'], | ||
| credentials: true, | ||
| maxAge: 86400, // 24 hours | ||
| }); |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Show file size and the relevant slices with line numbers.
wc -l .agents/skills/fastify-best-practices/rules/cors-security.md
sed -n '1,120p' .agents/skills/fastify-best-practices/rules/cors-security.md | cat -n
# Also inspect any mentions of CORS, dev-only, security guide, or allowlist in nearby docs.
rg -n -C 3 "CORS|cors|dev-only|allowlist|allow list|credentials: true|origin:" .agents/skills/fastify-best-practices -g '*.md'Repository: AntoniPetrenko/2026-fwdays-agentic-greenfield-task-AP
Length of output: 12902
🌐 Web query:
@fastify/cors default options origin true credentials true reflect request origin security documentation
💡 Result:
In the @fastify/cors plugin, setting origin: true configures the server to reflect the value of the incoming Origin header in the Access-Control-Allow-Origin response header [1][2]. When combined with credentials: true, the server instructs the browser to allow cross-origin requests to include credentials (such as cookies or HTTP authentication) for the reflected origin [1][3]. This configuration is generally not recommended for production environments due to significant security risks, primarily related to reflection exploits [1][2][3]. When origin is set to true, any site can potentially make authenticated requests to your server, as the server will dynamically trust whichever origin is provided in the request [1][4][5]. An attacker could exploit this by crafting a malicious site that, when visited by a logged-in user, makes authorized requests to your API, effectively bypassing the intended security boundaries of CORS [5]. To secure your application, you should: 1. Use an explicit allowlist: Instead of setting origin to true, provide an array of trusted origin strings or regular expressions [1][2][3]. 2. Use a validation function: If you need dynamic logic, use a function for the origin option to strictly validate the incoming request origin against a known set of allowed hosts before returning it [1][2]. 3. Avoid wildcard origins with credentials: Browsers strictly forbid the use of the wildcard '*' origin when credentials: true is set; the server must respond with a specific, non-wildcard origin [3][6]. By explicitly validating origins, you ensure that only trusted clients can perform authenticated cross-origin operations against your service [3].
Citations:
- 1: https://registry.npmjs.org/%40fastify%2Fcors
- 2: https://www.npmjs.com/package/@fastify/cors
- 3: https://context7.com/fastify/fastify-cors/llms.txt
- 4: https://snyk.io/blog/security-implications-cors-node-js/
- 5: https://security.stackexchange.com/questions/277870/cors-credentials-option-set-to-true
- 6: fastify-cors not working fastify/help#583
🌐 Web query:
@fastify/cors default behavior app.register(cors) allow all origins default origin false docs
💡 Result:
In the @fastify/cors plugin, the default value for the origin option is '' [1][2][3]. This special '' value configures the Access-Control-Allow-Origin header to allow any origin [2][4]. Regarding your specific question about setting origin to false: 1. Setting origin: false (in the plugin configuration or via a dynamic callback) disables CORS for those requests [2][5][6]. 2. It does not mean the default behavior of the plugin is to disable CORS; rather, it is one of several specific configuration options available to control behavior [2][4]. To summarize the origin option behaviors: - '*': The default value, which allows all origins [1][3]. - true: Reflects the request origin in the Access-Control-Allow-Origin header (e.g., dynamically allows the requester) [2][5]. - false: Disables CORS entirely [2][4]. - Specific String, RegExp, Array, or Function: Used for custom origin validation [2][6]. You can override these defaults globally when calling fastify.register(cors, { origin:... }) or per-route using the config.cors option [2][4][5].
Citations:
- 1: https://github.com/fastify/fastify-cors
- 2: https://www.npmjs.com/package/@fastify/cors
- 3: https://context7.com/fastify/fastify-cors/llms.txt
- 4: https://registry.npmjs.org/%40fastify%2Fcors
- 5: https://npmx.dev/package/@fastify/cors/v/11.1.0
- 6: https://github.com/fastify/fastify-cors/blob/main/README.md
Не показуйте permissive CORS як базовий приклад. app.register(cors) за замовчуванням дозволяє всі origins, а origin: true разом із credentials: true віддзеркалює будь-який Origin. Для security-гайду це занадто небезпечний шаблон для копіювання — залиште лише явний allowlist або позначте ці варіанти як dev-only.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In @.agents/skills/fastify-best-practices/rules/cors-security.md around lines 20
- 31, The CORS examples in the Fastify best-practices guide are too permissive
for a security template: remove the default “allow all origins” usage from the
`app.register(cors)` example and avoid presenting `origin: true` with
`credentials: true` as a safe baseline. Update the `cors-security.md` guidance
to show only an explicit allowlist configuration, or clearly label any
permissive setup as development-only, so readers are steered toward secure
defaults.
| app.addHook('onRequest', async (request, reply) => { | ||
| if (request.url.startsWith('/admin')) { | ||
| const clientIp = request.ip; | ||
|
|
||
| if (!isIpAllowed(clientIp, allowedIps)) { | ||
| reply.code(403).send({ error: 'Forbidden' }); | ||
| } | ||
| } | ||
| }); |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
Поверніть return із onRequest після send(). Без цього хук виглядає як блокуючий, але lifecycle може піти далі до handler’а й упертися в already sent.
🔧 Мінімальний фікс
if (!isIpAllowed(clientIp, allowedIps)) {
- reply.code(403).send({ error: 'Forbidden' });
+ return reply.code(403).send({ error: 'Forbidden' });
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| app.addHook('onRequest', async (request, reply) => { | |
| if (request.url.startsWith('/admin')) { | |
| const clientIp = request.ip; | |
| if (!isIpAllowed(clientIp, allowedIps)) { | |
| reply.code(403).send({ error: 'Forbidden' }); | |
| } | |
| } | |
| }); | |
| app.addHook('onRequest', async (request, reply) => { | |
| if (request.url.startsWith('/admin')) { | |
| const clientIp = request.ip; | |
| if (!isIpAllowed(clientIp, allowedIps)) { | |
| return reply.code(403).send({ error: 'Forbidden' }); | |
| } | |
| } | |
| }); |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In @.agents/skills/fastify-best-practices/rules/cors-security.md around lines
359 - 367, The onRequest hook in app.addHook currently sends a 403 for
disallowed admin IPs but does not stop execution afterward, so the request
lifecycle can continue into the handler and trigger an already sent error.
Update the onRequest logic to return immediately after reply.code(403).send(...)
when isIpAllowed(clientIp, allowedIps) is false, keeping the control flow in the
hook explicit and preventing downstream execution.
| { | ||
| "name": "2026-fwdays-agentic-greenfield-task-ap", | ||
| "version": "1.0.0", | ||
| "private": true, | ||
| "description": "Курс fwdays Academy · Agentic Engineering: Greenfield.", | ||
| "homepage": "https://github.com/AntoniPetrenko/2026-fwdays-agentic-greenfield-task-AP#readme", | ||
| "bugs": { | ||
| "url": "https://github.com/AntoniPetrenko/2026-fwdays-agentic-greenfield-task-AP/issues" | ||
| }, | ||
| "repository": { | ||
| "type": "git", | ||
| "url": "git+https://github.com/AntoniPetrenko/2026-fwdays-agentic-greenfield-task-AP.git" | ||
| }, | ||
| "license": "ISC", | ||
| "author": "", | ||
| "directories": { | ||
| "doc": "docs" | ||
| }, | ||
| "scripts": { | ||
| "skills:sync": "metaskills", | ||
| "skills:claude": "metaskills claude", | ||
| "back:dev": "npm --prefix back-end run dev", | ||
| "back:build": "npm --prefix back-end run build", | ||
| "back:test": "npm --prefix back-end run test", | ||
| "back:install": "npm --prefix back-end install", | ||
| "back:discover": "npm --prefix back-end run discover", | ||
| "front:dev": "npm --prefix front-end run dev", | ||
| "front:build": "npm --prefix front-end run build", | ||
| "front:test": "npm --prefix front-end run test", | ||
| "front:install": "npm --prefix front-end install", | ||
| "install:all": "npm install && npm run back:install && npm run front:install", | ||
| "deploy:build": "bash scripts/build.sh", | ||
| "deploy:install": "bash scripts/install.sh", | ||
| "deploy:uninstall": "bash scripts/uninstall.sh" | ||
| }, | ||
| "devDependencies": { | ||
| "metaskills": "^1.0.4" | ||
| } | ||
| } |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Процесна оцінка (Agentic Engineering rubric).
Привіт, Антоне! Гарна робота з інфраструктурою деплою — тепер по суті рубрики курсу:
- Реальне ім'я автора — вказано в описі PR ("Anton Petrenko"). ✅
- Відео-демо — посилання є в описі PR. ✅
- Змістовний опис Agentic Engineering практик — є: контекст-файли,
/next-changeцикли, maker≠checker через окремогоcode-reviewerсуб-агента, OpenSpec специфікації, build/test verification gates. ✅ - Доведено до кінця — заявлено end-to-end робочий стан. ✅
Дрібне зауваження: поле "author" тут порожнє (Line 15). Було б акуратніше вказати реальне ім'я прямо в package.json, а не лише в описі PR — це дрібниця, але прямо стосується пункту (1) рубрики.
Based on path instructions: "оціни ДОКАЗИ: (1) вказане справжнє імʼя автора; (2) є посилання на відео-демо...; (3) є ЗМІСТОВНИЙ опис застосованих практик...; (4) результат доведено до кінця".
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@package.json` around lines 1 - 39, The package metadata is missing the real
author name even though the PR description contains it; update the package.json
author field in the main package metadata to the actual author’s name so the
project records authorship directly in the manifest, using the existing
package.json structure as the place to fix it.
Source: Path instructions
| resolve_paths() { | ||
| local script_dir | ||
| script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" | ||
| REPO_ROOT="$(cd "$script_dir/.." && pwd)" | ||
| TEMPLATE_PATH="$script_dir/mytv.service.tmpl" | ||
|
|
||
| [[ -d "$REPO_ROOT/back-end" ]] || die "REPO_ROOT missing back-end/: $REPO_ROOT" | ||
| [[ -d "$REPO_ROOT/front-end" ]] || die "REPO_ROOT missing front-end/: $REPO_ROOT" | ||
| [[ -f "$TEMPLATE_PATH" ]] || die "systemd unit template missing: $TEMPLATE_PATH" | ||
|
|
||
| NODE_BIN="$(readlink -f "$(command -v node)")" | ||
| [[ -x "$NODE_BIN" ]] || die "resolved node binary not executable: $NODE_BIN" | ||
|
|
||
| log "REPO_ROOT=$REPO_ROOT" | ||
| log "NODE_BIN=$NODE_BIN" | ||
| } | ||
|
|
||
| verify_artifacts() { | ||
| local be="$REPO_ROOT/back-end/dist/index.js" | ||
| local fe="$REPO_ROOT/front-end/dist/index.html" | ||
| local missing=() | ||
| [[ -s "$be" ]] || missing+=("$be") | ||
| [[ -s "$fe" ]] || missing+=("$fe") | ||
| if (( ${#missing[@]} > 0 )); then | ||
| for m in "${missing[@]}"; do | ||
| printf '[install] ERROR: missing artefact: %s\n' "$m" >&2 | ||
| done | ||
| die "run ./scripts/build.sh from a machine with node >= 20 (locally, or rsync the dists in from a dev laptop) and try again" | ||
| fi | ||
| log "artefacts present: $be" | ||
| log "artefacts present: $fe" | ||
| } | ||
|
|
||
| install_prod_deps() { | ||
| log "installing production runtime deps (npm ci --omit=dev in back-end/)" | ||
| npm ci --omit=dev --prefix "$REPO_ROOT/back-end" | ||
| } |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Перевірка: чи є явна обробка прав доступу для SERVICE_USER на REPO_ROOT
rg -n 'SERVICE_USER|chmod|setfacl|ReadWritePaths' scripts/install.sh scripts/mytv.service.tmplRepository: AntoniPetrenko/2026-fwdays-agentic-greenfield-task-AP
Length of output: 651
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo '--- scripts/install.sh:1-220 ---'
sed -n '1,220p' scripts/install.sh | cat -n
echo
echo '--- scripts/mytv.service.tmpl ---'
cat -n scripts/mytv.service.tmpl
echo
echo '--- permission-related checks ---'
rg -n 'REPO_ROOT|SERVICE_HOME|SERVICE_USER|WorkingDirectory|ExecStart|setfacl|chmod|chown|install -d|ReadWritePaths' scripts/install.sh scripts/mytv.service.tmplRepository: AntoniPetrenko/2026-fwdays-agentic-greenfield-task-AP
Length of output: 11006
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo '--- scripts/install.sh:220-end ---'
sed -n '220,360p' scripts/install.sh | cat -n
echo
echo '--- repo docs mentioning install path / permissions ---'
rg -n 'install.sh|/opt|/home|mytv.service|REPO_ROOT|permissions|permission' README* docs scripts/*.md scripts/*.txtRepository: AntoniPetrenko/2026-fwdays-agentic-greenfield-task-AP
Length of output: 12280
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo '--- docs / README install instructions ---'
rg -n 'sudo ./scripts/install.sh|./scripts/build.sh|clone|/opt/mytv|/home/pi|working directory|REPO_ROOT|permissions' README* docs back-end/README.md front-end/README.md
echo
echo '--- any explicit path assumptions in scripts ---'
rg -n 'cd "\$script_dir/.."|WorkingDirectory=`@REPO_ROOT`@|User=mytv|Group=mytv|HOME=/var/lib/mytv|install -d -o "\$SERVICE_USER"' scripts/install.sh scripts/mytv.service.tmplRepository: AntoniPetrenko/2026-fwdays-agentic-greenfield-task-AP
Length of output: 6233
Перевір доступність REPO_ROOT для mytv
scripts/install.sh:62-98 — WorkingDirectory=@REPO_ROOT@/back-end запускається під User=mytv, але скрипт не гарантує x на всіх батьківських директоріях до REPO_ROOT. Якщо репозиторій лежить у приватному шляху, сервіс впаде з Permission denied на старті. Або задокументуй вимогу тримати клон у traversable path (наприклад, /opt/mytv), або додай явну перевірку/ACL після create_service_user.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@scripts/install.sh` around lines 62 - 98, The install flow currently assumes
the mytv user can traverse the full REPO_ROOT path, but resolve_paths() only
checks for the repo subdirectories and not execute/search permission on parent
directories. Update scripts/install.sh around resolve_paths()/verify_artifacts()
to either enforce and document that the clone must live in a traversable
location like /opt/mytv, or add an explicit post-create_service_user permission
check (or ACL setup) that verifies mytv can access REPO_ROOT before generating
the systemd unit.
| grant_port_capability() { | ||
| local current | ||
| current="$(getcap "$NODE_BIN" 2>/dev/null || true)" | ||
| if [[ "$current" == *"cap_net_bind_service"*"ep"* ]]; then | ||
| log "cap_net_bind_service already set on $NODE_BIN" | ||
| return 0 | ||
| fi | ||
| log "granting cap_net_bind_service to $NODE_BIN" | ||
| run setcap 'cap_net_bind_service=+ep' "$NODE_BIN" | ||
| if (( DRY_RUN == 0 )); then | ||
| current="$(getcap "$NODE_BIN" 2>/dev/null || true)" | ||
| [[ "$current" == *"cap_net_bind_service"*"ep"* ]] \ | ||
| || die "setcap succeeded but capability not present on $NODE_BIN (got: '$current')" | ||
| fi | ||
| } |
There was a problem hiding this comment.
🔒 Security & Privacy | 🔵 Trivial | 🏗️ Heavy lift
Розглянь AmbientCapabilities замість setcap на спільному бінарнику node.
setcap cap_net_bind_service=+ep (Line 108) видає можливість слухати привілейовані порти будь-якому процесу, який виконує саме цей файл NODE_BIN — не лише сервісу mytv. Якщо цей самий node використовується іншими скриптами/сервісами на хості, вони теж неявно отримують цю можливість (ширша поверхня атаки, ніж потрібно). Крім того, ця можливість "злітає" при оновленні/заміні бінарника node (наприклад через nvm), і install.sh довелось би перезапускати для повторного setcap.
Стандартний і безпечніший підхід для systemd — AmbientCapabilities=CAP_NET_BIND_SERVICE (+ CapabilityBoundingSet=CAP_NET_BIND_SERVICE) прямо в mytv.service.tmpl: можливість буде обмежена саме цим сервісом і не залежить від конкретного шляху/версії бінарника node.
🔒 Приклад заміни в mytv.service.tmpl замість setcap в install.sh
[Service]
Type=simple
User=mytv
Group=mytv
+AmbientCapabilities=CAP_NET_BIND_SERVICE
+CapabilityBoundingSet=CAP_NET_BIND_SERVICE
WorkingDirectory=`@REPO_ROOT`@/back-end
ExecStart=`@NODE_BIN`@ dist/index.js🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@scripts/install.sh` around lines 100 - 114, `grant_port_capability()`
currently uses `setcap` on the shared `NODE_BIN`, which grants
CAP_NET_BIND_SERVICE to every process running that binary and can be lost on
node updates. Remove this capability setup from the installer and move the
privilege to the systemd service template (`mytv.service.tmpl`) by configuring
`AmbientCapabilities=CAP_NET_BIND_SERVICE` and
`CapabilityBoundingSet=CAP_NET_BIND_SERVICE` so only `mytv` gets the
port-binding permission.
| @test "resolve_paths resolves REPO_ROOT and NODE_BIN to absolute existing paths" { | ||
| # Skip on non-Linux because check_prereqs would otherwise refuse. | ||
| case "${OSTYPE:-}" in | ||
| darwin*) : ;; # still runnable — we skip check_prereqs manually below | ||
| esac | ||
|
|
||
| source_install | ||
| resolve_paths | ||
| [ -n "$REPO_ROOT" ] | ||
| [[ "$REPO_ROOT" = /* ]] | ||
| [ -d "$REPO_ROOT/back-end" ] | ||
| [ -d "$REPO_ROOT/front-end" ] | ||
| [ -n "$NODE_BIN" ] | ||
| [[ "$NODE_BIN" = /* ]] | ||
| [ -x "$NODE_BIN" ] | ||
| } |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Коментар про "skip on non-Linux" нічого насправді не пропускає.
case "${OSTYPE:-}" in darwin*) : ;; esac (Line 73-75) — це no-op незалежно від значення OSTYPE: немає *) гілки й немає return/skip, тож заявлений у коментарі "skip" не відбувається. На практиці тест все одно проходить на macOS, бо resolve_paths не викликає check_prereqs, — але коментар вводить в оману майбутнього читача.
🧹 Пропозиція: прибрати мертвий код або зробити реальний skip
- # Skip on non-Linux because check_prereqs would otherwise refuse.
- case "${OSTYPE:-}" in
- darwin*) : ;; # still runnable — we skip check_prereqs manually below
- esac
-
source_install📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| @test "resolve_paths resolves REPO_ROOT and NODE_BIN to absolute existing paths" { | |
| # Skip on non-Linux because check_prereqs would otherwise refuse. | |
| case "${OSTYPE:-}" in | |
| darwin*) : ;; # still runnable — we skip check_prereqs manually below | |
| esac | |
| source_install | |
| resolve_paths | |
| [ -n "$REPO_ROOT" ] | |
| [[ "$REPO_ROOT" = /* ]] | |
| [ -d "$REPO_ROOT/back-end" ] | |
| [ -d "$REPO_ROOT/front-end" ] | |
| [ -n "$NODE_BIN" ] | |
| [[ "$NODE_BIN" = /* ]] | |
| [ -x "$NODE_BIN" ] | |
| } | |
| `@test` "resolve_paths resolves REPO_ROOT and NODE_BIN to absolute existing paths" { | |
| source_install | |
| resolve_paths | |
| [ -n "$REPO_ROOT" ] | |
| [[ "$REPO_ROOT" = /* ]] | |
| [ -d "$REPO_ROOT/back-end" ] | |
| [ -d "$REPO_ROOT/front-end" ] | |
| [ -n "$NODE_BIN" ] | |
| [[ "$NODE_BIN" = /* ]] | |
| [ -x "$NODE_BIN" ] | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@scripts/install.test.bats` around lines 71 - 86, The "skip on non-Linux"
block in the resolve_paths Bats test is dead code because the current case
statement in install.test.bats does nothing and does not actually skip anything.
Update the test around source_install and resolve_paths so the intent matches
behavior: either remove the misleading darwin* no-op entirely, or replace it
with a real skip/return path for the test. Keep the test expectations for
REPO_ROOT and NODE_BIN unchanged, but ensure the control flow in this test
clearly reflects whether macOS should run this case.
| [Service] | ||
| Type=simple | ||
| User=mytv | ||
| Group=mytv | ||
| WorkingDirectory=@REPO_ROOT@/back-end | ||
| ExecStart=@NODE_BIN@ dist/index.js | ||
| Restart=on-failure | ||
| RestartSec=5 | ||
| Environment=NODE_ENV=production | ||
| Environment=PORT=80 | ||
| Environment=HOST=0.0.0.0 | ||
| Environment=LOG_LEVEL=info | ||
| Environment=SERVE_SPA=1 | ||
| # HOME is what back-end/src/tv/token-store.ts resolves against when | ||
| # XDG_CONFIG_HOME is unset: tokens land at /var/lib/mytv/.mytv/tokens.json | ||
| # (0600, owned by the mytv user). Do NOT set XDG_CONFIG_HOME here — it | ||
| # would shift the token file to $XDG_CONFIG_HOME/mytv/tokens.json and | ||
| # desync from the path the docs / uninstall recipe reference. | ||
| Environment=HOME=/var/lib/mytv | ||
| StandardOutput=journal | ||
| StandardError=journal | ||
| SyslogIdentifier=mytv |
There was a problem hiding this comment.
🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win
Додай базове systemd-хардення до [Service].
Юніт нічого не обмежує (NoNewPrivileges, ProtectSystem, PrivateTmp, ProtectHome тощо відсутні). Для сервісу, що слухає привілейований порт і керує пристроями в локальній мережі, це доступний "defense in depth", який коштує кілька рядків.
🛡️ Приклад мінімального хардення
[Service]
Type=simple
User=mytv
Group=mytv
+NoNewPrivileges=yes
+ProtectSystem=strict
+PrivateTmp=yes
+ReadWritePaths=/var/lib/mytv
WorkingDirectory=`@REPO_ROOT`@/back-end
ExecStart=`@NODE_BIN`@ dist/index.js📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| [Service] | |
| Type=simple | |
| User=mytv | |
| Group=mytv | |
| WorkingDirectory=@REPO_ROOT@/back-end | |
| ExecStart=@NODE_BIN@ dist/index.js | |
| Restart=on-failure | |
| RestartSec=5 | |
| Environment=NODE_ENV=production | |
| Environment=PORT=80 | |
| Environment=HOST=0.0.0.0 | |
| Environment=LOG_LEVEL=info | |
| Environment=SERVE_SPA=1 | |
| # HOME is what back-end/src/tv/token-store.ts resolves against when | |
| # XDG_CONFIG_HOME is unset: tokens land at /var/lib/mytv/.mytv/tokens.json | |
| # (0600, owned by the mytv user). Do NOT set XDG_CONFIG_HOME here — it | |
| # would shift the token file to $XDG_CONFIG_HOME/mytv/tokens.json and | |
| # desync from the path the docs / uninstall recipe reference. | |
| Environment=HOME=/var/lib/mytv | |
| StandardOutput=journal | |
| StandardError=journal | |
| SyslogIdentifier=mytv | |
| [Service] | |
| Type=simple | |
| User=mytv | |
| Group=mytv | |
| NoNewPrivileges=yes | |
| ProtectSystem=strict | |
| PrivateTmp=yes | |
| ReadWritePaths=/var/lib/mytv | |
| WorkingDirectory=`@REPO_ROOT`@/back-end | |
| ExecStart=`@NODE_BIN`@ dist/index.js | |
| Restart=on-failure | |
| RestartSec=5 | |
| Environment=NODE_ENV=production | |
| Environment=PORT=80 | |
| Environment=HOST=0.0.0.0 | |
| Environment=LOG_LEVEL=info | |
| Environment=SERVE_SPA=1 | |
| # HOME is what back-end/src/tv/token-store.ts resolves against when | |
| # XDG_CONFIG_HOME is unset: tokens land at /var/lib/mytv/.mytv/tokens.json | |
| # (0600, owned by the mytv user). Do NOT set XDG_CONFIG_HOME here — it | |
| # would shift the token file to $XDG_CONFIG_HOME/mytv/tokens.json and | |
| # desync from the path the docs / uninstall recipe reference. | |
| Environment=HOME=/var/lib/mytv | |
| StandardOutput=journal | |
| StandardError=journal | |
| SyslogIdentifier=mytv |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@scripts/mytv.service.tmpl` around lines 7 - 28, The mytv systemd service in
the [Service] section lacks basic hardening directives, leaving it unnecessarily
permissive. Update the service template around the existing ExecStart,
Environment, and logging settings to add sensible protections such as
NoNewPrivileges, PrivateTmp, ProtectSystem, and ProtectHome, while keeping the
service functional. Use the existing mytv service stanza as the place to apply
these restrictions and ensure any added settings do not break token storage or
runtime requirements.
There was a problem hiding this comment.
Actionable comments posted: 70
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
openspec/changes/archive/2026-07-04-tv-connection-lifecycle/tasks.md (1)
38-43: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick winНе відмічайте live-smoke як done, якщо
Connectedне досягнуто.У цьому блоці ви самі пишете, що під час 6.2
Connectedне було досягнуто, а 6.3 відкладено. Для архівної задачі це виглядає як завищений статус виконання; краще рознестиverified,partialіdeferredявно, щоб не продавати незавершений цикл як готовий. As per path instructions, тут треба показати end-to-end доказ, а не лише проміжний прогрес.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@openspec/changes/archive/2026-07-04-tv-connection-lifecycle/tasks.md` around lines 38 - 43, Adjust the 6.2/6.3 archive status in tasks.md so the live-smoke entry is not marked fully done unless `Connected` was actually reached. In the `6.2` and `6.3` bullets, use explicit `partial`/`deferred` wording and keep the `Connected` outcome separate from the verified transport and pairing steps; refer to the existing `6.2`, `6.3`, and the `Connected → Reconnecting → Connected` session tests only as supporting evidence, not as completion proof.Source: Path instructions
♻️ Duplicate comments (11)
scripts/uninstall.sh (1)
30-35: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
systemctl list-unit-filesне підходить як перевірка існування юніта — вже піднімалось раніше.Ця команда повертає
0навіть коли збігів немає, тож гілка "not registered" ніколи реально не спрацює і лог буде хибним. Краще перевіряти[[ -f "$UNIT_PATH" ]]або просто робити best-effortdisable --nowіз придушенням помилки.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@scripts/uninstall.sh` around lines 30 - 35, The unit-existence check in the uninstall flow is wrong because `systemctl list-unit-files` can succeed even when `mytv.service` is not present, so the “not registered” branch in the uninstall script is misleading. Update the logic in the uninstall script around the `mytv.service` handling to use a real unit-file check such as `[[ -f "$UNIT_PATH" ]]`, or remove the precheck and make `systemctl disable --now mytv` a best-effort call with errors suppressed. Keep the existing `log` messages aligned with the actual behavior.scripts/mytv.service.tmpl (1)
7-29: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick winХардення systemd-юніта досі відсутнє — вже піднімалось раніше.
Немає
NoNewPrivileges,ProtectSystem,ProtectHome,PrivateTmpдля мережевого сервісу.cap_net_bind_serviceвиставляється черезsetcapна бінарник (неAmbientCapabilities), тож ці директиви безпечно додати без ризику зламати біндинг порту.🛡️ Пропоноване доповнення
[Service] Type=simple User=mytv Group=mytv WorkingDirectory=`@REPO_ROOT`@/back-end ExecStart=`@NODE_BIN`@ dist/index.js Restart=on-failure RestartSec=5 +NoNewPrivileges=true +ProtectSystem=strict +ProtectHome=true +PrivateTmp=true +ReadWritePaths=/var/lib/mytv Environment=NODE_ENV=production🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@scripts/mytv.service.tmpl` around lines 7 - 29, The systemd unit in mytv.service.tmpl is missing basic hardening directives, so update the service section to add NoNewPrivileges, ProtectSystem, ProtectHome, and PrivateTmp while keeping the existing ExecStart and environment setup intact. Since the binary already gets cap_net_bind_service via setcap rather than AmbientCapabilities, these protections can be added safely without affecting port binding; apply the change directly in the unit template alongside the current [Service] settings.scripts/build.sh (1)
14-45: 📐 Maintainability & Code Quality | 🟠 Major | 🏗️ Heavy liftДублювання спільної логіки з install.sh (і частково uninstall.sh) — вже піднімалось раніше.
check_prereqs/resolve_paths/log/die/need_cmdтут майже ідентичні до однойменних уscripts/install.sh. Витягнути вscripts/lib.shі підключати черезsource— це не зашкодить bats-тестам.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@scripts/build.sh` around lines 14 - 45, The helper logic in build.sh is duplicated with install.sh, so extract the shared functions need_cmd, check_prereqs, resolve_paths, log, and die into a reusable scripts/lib.sh and source it from build.sh (and the matching install/uninstall scripts if needed). Keep the behavior identical by moving the common implementations without changing their call sites such as check_prereqs and resolve_paths, so the shared shell tests and bats setup continue to work.back-end/src/mdns.test.ts (1)
109-127: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winБракує тесту на async socket-error шлях (реальний Avahi-конфлікт) — досі не додано.
Як і в попередньому review: реальний port-conflict (
EADDRINUSEна 5353) обробляється асинхронно черезrawClient.server?.mdns?.on('error', ...), а не через синхронний throw уpublish(). Саме цей шлях задокументованийAVAHI_HINTяк типова причина збою на Orange Pi — і саме він лишається без тесту.Додай стаб зі структурою
{ server: { mdns: { on } } }, який симулює asyncEADDRINUSE, і перевір ті саміhandle.state/handle.error/handle.address.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@back-end/src/mdns.test.ts` around lines 109 - 127, The current test covers only the synchronous publish() throw path, but the review asks for the async socket-error path handled via rawClient.server?.mdns?.on('error', ...). Update the mdns.test.ts coverage around startMdns to use a stub client shaped like { server: { mdns: { on } } } that emits an EADDRINUSE error asynchronously, matching the AVAHI_HINT conflict case. Verify the same handle.state, handle.error, and handle.address assertions for the async error path, and keep the existing error-state expectations in the startMdns behavior.back-end/src/routes/browser.test.ts (1)
134-210: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick winБракує тесту на відхилення команди на рівні транспорту — досі не додано.
Як і раніше: немає кейсу, де
session.enqueue()резолвиться відхиленням (наприклад,TvError) вже після проходження перевіркиConnected— це єдиний шлях, що фактично перевіряє, чи глобальний error-handler мапитьTvErrorв дозволений union кодів, а не «сирі» wire-рівневі деталі, як вимагає coding guideline дляback-end/**/*.ts.Мокни
session.enqueue()так, щоб він відхилявсяTvErrorвже післяconnect, і перевір, що POST/browserповертає код з дозволеного union, а не сирі деталі, і що фрейм не надсилається.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@back-end/src/routes/browser.test.ts` around lines 134 - 210, The browser route tests are missing coverage for transport-level command rejection after the session is already connected. Add a case in browser.test.ts around the POST /browser flow that connects first, then mocks session.enqueue() to reject with TvError, and assert the response maps to an allowed union error code rather than exposing raw wire details. Also verify no frame is sent by checking stub.calls does not increase, using the existing post() helper and the browser route behavior as the reference point.Source: Coding guidelines
back-end/src/logger.ts (1)
3-3: 🔒 Security & Privacy | 🟡 Minor | ⚡ Quick winПорівняння ключів досі регістрозалежне — токен може просочитись у лог.
Це зауваження вже піднімалось раніше і лишилось невиправленим:
SENSITIVE_KEYS.has(key)не спрацює дляToken,AccessTokenу іншому регістрі чиAuthorization(як HTTP-заголовок може прийти "як є" зundici-відповіді, минаючи Fastify-нормалізацію заголовків запиту). Guideline explicitly вимагає редагувати pairing token "from every log line including nested objects" — регістр тут не застережений.🛡️ Пропозиція (як і в попередньому review)
-const SENSITIVE_KEYS = new Set(['AccessToken', 'authorization', 'token']); +const SENSITIVE_KEYS = new Set(['accesstoken', 'authorization', 'token']);- if (SENSITIVE_KEYS.has(key)) continue; + if (SENSITIVE_KEYS.has(key.toLowerCase())) continue;Also applies to: 38-39
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@back-end/src/logger.ts` at line 3, SENSITIVE_KEYS matching in logger.ts is still case-sensitive, so secret fields like Token, AccessToken, or Authorization can slip into logs. Update the redaction logic used by the logger’s object traversal/redaction path to normalize keys before checking SENSITIVE_KEYS (for example, compare lowercased key names) and make sure the same normalization applies recursively to nested objects and header-like payloads. Use the existing SENSITIVE_KEYS set and the logging/redaction helper in logger.ts so all log lines are sanitized consistently regardless of key casing.Source: Coding guidelines
front-end/src/screens/DeviceListScreen.tsx (1)
14-42: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick winХардкод spacing замість design-токенів — досі не виправлено.
Верхній layout (
padding: '40px 48px',marginBottom: 32,gap: 16,marginBottom: 6) використовує сирі пікселі, тоді якEmptyStateнижче у цьому ж файлі (рядки 86-94) вже коректно береvar(--space-8) var(--space-6)іvar(--space-3). Токени для spacing в DS явно є — просто застосовані не послідовно.♻️ Пропозиція (як і в попередньому review)
- <div style={{ minHeight: '100%', padding: '40px 48px' }}> + <div style={{ minHeight: '100%', padding: 'var(--space-10) var(--space-8)' }}> <div style={{ maxWidth: 640, margin: '0 auto' }}> - <div style={{ marginBottom: 32, display: 'flex', alignItems: 'flex-end', justifyContent: 'space-between', gap: 16 }}> + <div style={{ marginBottom: 'var(--space-6)', display: 'flex', alignItems: 'flex-end', justifyContent: 'space-between', gap: 'var(--space-3)' }}> <div> <div style={{ ... - marginBottom: 6, + marginBottom: 'var(--space-1)', }}🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@front-end/src/screens/DeviceListScreen.tsx` around lines 14 - 42, The top-level layout in DeviceListScreen still uses hardcoded pixel spacing instead of design tokens. Update the wrapping container and header spacing in DeviceListScreen to use the same spacing tokens already used by EmptyState (for example the existing var(--space-*) values) instead of raw values like 40px, 48px, 32, 16, and 6. Keep the component structure the same, but make the spacing consistent with the design system across this screen.Source: Coding guidelines
openspec/changes/archive/2026-07-06-tv-browser-launch/design.md (1)
106-109: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick winНе логуйте сирий
urlуinfo-події.У Line 108 ви все ще пишете повний user-provided URL. Це вже піднімалось раніше: query string може містити секрети, а нинішній scrubber закриває лише
token=фрагменти, не весь потенційно чутливий URL. Логуйте лишеhost/originабо scrubbed URL тут, а не покладайтесь на глобальну redaction.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@openspec/changes/archive/2026-07-06-tv-browser-launch/design.md` around lines 106 - 109, The D6 logging note still allows a raw user-provided url to be emitted at info, which should be removed or sanitized in the design. Update the logging guidance in the archive design so the entry log uses only safe fields like correlationId, udn, and host/origin, or explicitly a scrubbed URL, and make it clear not to rely on logger.ts redaction alone. Use the D6 Logging & redaction section as the place to adjust this guidance.openspec/changes/archive/2026-07-04-smart-view-ws-transport/specs/upnp-tv-discovery/spec.md (1)
5-7: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winЗвузьте Samsung-сигнатуру у pre-fetch фільтрі.
uuid:— загальний UPnP-маркер, тож цей критерій пропускає не-Samsung пристрої й зводить нанівець відсікання шуму доfetch. Залиште лише справді Samsung-специфічний патерн, інакше вимога не дає очікуваного ефекту.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@openspec/changes/archive/2026-07-04-smart-view-ws-transport/specs/upnp-tv-discovery/spec.md` around lines 5 - 7, The pre-fetch Samsung filter in the SSDP discovery flow is too broad because the `uuid:` check matches generic UPnP devices; tighten the header inspection in the discovery orchestrator so only truly Samsung-specific signatures are accepted before enqueueing a fetch. Update the logic that examines `USN`, `SERVER`, and `ST` to rely on Samsung-specific markers and keep the post-fetch `isSamsungTv` check as the fallback for ambiguous cases..agents/skills/react-vite-best-practices/rules/build-tree-shaking.md (1)
111-129: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winВинесіть
optimizeDepsіз tree-shaking прикладу.
optimizeDeps— це dev pre-bundling, а не production tree shaking, тож цей блок тут змішує два різні механізми. Лишіть тут тількиbuild.rollupOptions.treeshakeабо перенесіть цей фрагмент уdev-dependency-prebundling.md.Пропонований diff
export default defineConfig({ plugins: [react()], build: { rollupOptions: { treeshake: { moduleSideEffects: 'no-external', propertyReadSideEffects: false, tryCatchDeoptimization: false, }, }, }, - optimizeDeps: { - include: ['lodash-es'], - }, })🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In @.agents/skills/react-vite-best-practices/rules/build-tree-shaking.md around lines 111 - 129, The tree-shaking example in the Vite config mixes production treeshake settings with dev-only dependency pre-bundling. Remove the optimizeDeps block from this example and keep only the build.rollupOptions.treeshake configuration in the snippet, or move the optimizeDeps guidance to the dev-dependency-prebundling documentation. Use the defineConfig example as the reference point when updating the tree-shaking rule content..agents/skills/react-vite-best-practices/rules/bundle-visualizer.md (1)
106-121: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick winДодайте мову до fenced-блоку.
```на лінії 106 ламає MD040; позначте блок якtext, щоб приклад був коректним для markdownlint і читався однозначно. Якщо MD031 ще шумітиме, тримайте блок відокремленим порожніми рядками.Пропонований diff
-``` +```textSource: Linters/SAST tools
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In @.agents/skills/fastify-best-practices/rules/authentication.md:
- Around line 91-107: The login flow in the authentication examples is exposing
access tokens to the client, which should be avoided in this repo. Update the
`app.post('/auth/login')` handler to stop returning `accessToken` in the
response and instead keep it server-side or place it in an `httpOnly` cookie,
using the same session/cookie approach in the redirect flow that currently
appends `token=`. Refer to `validateCredentials`, `app.jwt.sign`, and the
login/redirect auth handlers when rewriting the examples so they demonstrate
cookie-based or session-based auth only.
In @.agents/skills/fastify-best-practices/rules/cors-security.md:
- Around line 75-78: The CORS example using cors should not present origin: true
together with credentials: true as a general pattern. Update the example to use
an explicit allowlist in the app.register(cors, ...) configuration, or clearly
mark the current setup as a narrow exception for trusted auth-free routes. Keep
the guidance tied to the cors security rule so readers understand when to use
origin and credentials safely.
In @.agents/skills/fastify-best-practices/rules/database.md:
- Around line 234-245: The update logic in the update(id, data) method builds
the SQL SET clause directly from Object.keys(data), which allows arbitrary field
names and can break on empty updates. Replace this with an explicit allowlist of
permitted user columns before constructing the SET clause, and reject or
short-circuit when data has no updatable fields. Keep the fix localized to
update() and its SQL assembly around setClause and app.pg.query.
- Around line 48-63: The /transfer handler in app.post currently commits the
transaction without verifying that both UPDATE statements affected exactly one
row. After each client.query UPDATE inside the try block, check the returned
rowCount and throw an error if it is not 1 so the catch path can perform
ROLLBACK; keep the fix localized to the transfer flow and use the existing
client transaction logic.
In @.agents/skills/fastify-best-practices/rules/decorators.md:
- Around line 154-171: The user service methods are returning the raw database
query result instead of the actual user object, so update the `userService` in
`fastify.decorate` to unwrap `rows[0]` from both `findById()` and `create()`.
Keep the route usage in `app.get('/users/:id', ...)` aligned with that change so
`this.userService.findById(...)` returns the user record directly rather than
the driver wrapper.
In @.agents/skills/fastify-best-practices/rules/error-handling.md:
- Around line 72-83: The validation branch in app.setErrorHandler is returning
Fastify’s raw error.validation array, but the shared httpError contract expects
details to be an array of { field, message }. Update the validation handling in
this error handler to map error.validation into that shape before sending the
400 response, or adjust the httpError schema if you intend to keep the raw
payload; keep the response format consistent across all error branches.
In @.agents/skills/fastify-best-practices/rules/hooks.md:
- Around line 167-177: The preSerialization hook in app.addHook is injecting
_meta into every payload, but this will be stripped or behave inconsistently on
routes that use a response schema. Update the shared response contract used by
the hook so _meta is explicitly included in the relevant schemas, or move the
metadata into a response header instead; reference the preSerialization hook and
the payload transformation logic when making the change.
- Around line 145-159: The transaction lifecycle in the hook setup is misplaced:
committing in onResponse happens after the response is already flushed, so
transaction failures cannot be surfaced to the client. Update the pattern around
app.addHook('preHandler'), app.addHook('onResponse'), and app.addHook('onError')
to commit or rollback before sending the response, using a try/finally wrapper
around the handler or another pre-send hook, while still keeping
request.transaction cleanup tied to the request flow.
In @.agents/skills/fastify-best-practices/rules/logging.md:
- Around line 249-257: In the error handler using app.setErrorHandler and
request.log.error, stop logging raw request.body and request.query because they
may contain sensitive data. Update the logging pattern to keep only safe request
metadata (for example url and method) and, if request payload details are
needed, apply redaction or sanitize them before logging.
In @.agents/skills/fastify-best-practices/rules/performance.md:
- Around line 191-207: The streaming export handler in the app.get('/export')
example writes to reply.raw inside a for-await loop without honoring
backpressure, so replace the direct writes with a pipeline-based approach or
explicitly await the drain event after write returns false. Update the
cursor-to-response flow in this route so db.users.findCursor() is consumed in a
backpressure-aware way, keeping the JSON array framing but preventing buffer
growth on slow clients.
In @.agents/skills/fastify-best-practices/rules/routes.md:
- Around line 350-364: The 405 handling in app.setNotFoundHandler is too narrow
because app.hasRoute is only checking method 'GET', so non-GET routes can still
fall through to 404. Update the logic in this handler to inspect the request
path without the query string and check across all supported methods, using
app.hasRoute with the route’s path and method detection rather than a hardcoded
GET, so the correct 405 response is returned whenever the path exists under a
different method.
In @.agents/skills/fastify-best-practices/rules/schemas.md:
- Around line 101-114: The schema registration is putting $id into the object
shape instead of the TypeBox schema options, so references like ErrorResponse#
and PaginationQuery# will not resolve correctly. Update the schema definitions
around ErrorResponse and PaginationQuery to pass $id through the Type.Object
options, then register those schemas directly with app.addSchema using the named
schema objects rather than reconstructing them with spread syntax.
In @.agents/skills/fastify-best-practices/rules/serialization.md:
- Around line 187-195: The serializerCompiler example uses fastJson(schema)
without importing or defining it, so make the snippet self-contained by adding
the fast-json-stringify import in the example and using it within
serializerCompiler on the Fastify app setup. Keep the reference consistent with
serializerCompiler and the stringify callback so the sample can run as written.
In @.agents/skills/fastify-best-practices/rules/typescript.md:
- Around line 22-30: The TypeScript startup example is inconsistent with the
documented Node.js version. Update the package.json example in typescript.md so
the start and dev scripts match Node.js 22.6+ behavior by including the required
--experimental-strip-types flag, or revise the documented minimum Node version
to 23.6+ if you want to rely on default type stripping. Refer to the
package.json scripts shown in the example and keep the version guidance aligned
with the command syntax.
In @.agents/skills/fastify-best-practices/rules/websockets.md:
- Around line 265-280: The websocket auth example still accepts a token from
request.query.token, which exposes secrets in the URL. Update the preValidation
logic in the app.get('/ws') example to use only the Authorization header (via
request.headers.authorization) and remove all query-string token handling,
keeping the rest of the verifyToken flow unchanged.
In @.agents/skills/react-vite-best-practices/rules/asset-fonts.md:
- Around line 12-125: The font example in this rule conflicts with the repo’s UI
guideline because it teaches Inter/Google Fonts instead of the required
Montserrat stack. Update the examples in the font asset rule to use Montserrat
everywhere, including the self-hosted `@font-face` declarations, preload example,
and any font-family references, so the guidance matches the front-end standard
in the rule content. Keep the focus on the existing font-loading best practices
while replacing Inter-specific symbols with the Montserrat equivalents.
In @.agents/skills/react-vite-best-practices/rules/asset-image-optimization.md:
- Around line 107-115: Remove the contradictory WebP example setting that uses
lossless mode in the ViteImageOptimizer config, and update the example in
asset-image-optimization.md to show a lossy WebP optimization instead. Keep the
png and jpeg settings as-is, but adjust the webp option so the snippet aligns
with the rule’s intended guidance and the ViteImageOptimizer usage shown in the
diff.
In @.agents/skills/react-vite-best-practices/rules/build-asset-hashing.md:
- Around line 87-105: The express static asset example mixes ESM imports with
__dirname, which is not available in native ESM and will break when copied
as-is. Update the snippet around the app setup and express.static calls to
either use CommonJS consistently or derive the directory using
fileURLToPath(import.meta.url) before joining dist paths, so the example works
in an ESM context.
In @.agents/skills/react-vite-best-practices/rules/build-minification.md:
- Around line 107-112: Add a caution to the property-mangling example in the
build/minification guidance: the mangle.properties configuration with regex /^_/
should be presented as safe only for fully internal objects, and the note should
warn that applying it to JSON/API/localStorage-backed data can break consumer
contracts and runtime behavior. Update the surrounding explanation in the
build-minification rule to explicitly mention this limitation so readers
understand when property mangling should not be used.
In @.agents/skills/react-vite-best-practices/rules/dev-fast-refresh.md:
- Around line 17-83: The Fast Refresh rule is too broad because it treats named
component exports as inherently bad, even though they usually work; narrow the
guidance to mixed component/non-component exports and module-level side effects
only. Update the examples and “Problems” text in dev-fast-refresh to remove the
blanket warning about multiple named component exports, and keep the focus on
cases where component files also export constants/utilities or run top-level
side effects. Preserve references to the relevant examples in the rule so
readers understand when Fast Refresh actually breaks versus when named exports
are acceptable.
In @.agents/skills/react-vite-best-practices/rules/split-route-lazy.md:
- Around line 84-103: The second example in the split-route-lazy rule uses Link
inside NavLink without importing it, so the snippet will not compile. Update the
example’s imports to include Link from the appropriate source alongside the
existing lazy usage, and keep the NavLink/preloadDashboard example otherwise
unchanged.
In @.agents/skills/react-vite-best-practices/SKILL.md:
- Around line 98-101: The Vite config example currently uses __dirname in a way
that breaks in ESM contexts; update the react-vite-best-practices snippet by
removing __dirname or explicitly marking it as CommonJS, and prefer the ESM-safe
pattern using fileURLToPath(import.meta.url) with dirname, consistent with the
existing vite.config.ts example.
In @.claude/commands/opsx/apply.md:
- Around line 10-37: The apply command flow is missing the selected store flag
on subsequent OpenSpec calls, so store-scoped operations can hit the wrong root.
Update the opsx/apply.md workflow to thread `--store <id>` through every
specs/changes command after store selection, including `openspec status`,
`openspec instructions apply`, and any `openspec list`/follow-up calls, using
the same store id discovered in the store-selection step.
In @.claude/commands/opsx/archive.md:
- Around line 10-37: The `/opsx:archive` flow is missing store scoping for
repository reads, so it can inspect the wrong OpenSpec root in store mode.
Update the archive command logic to detect when a store is selected (using the
same store-selection handling used elsewhere) and pass `--store <id>` to the
`openspec list` and `openspec status` calls in this command. Keep the existing
prompt/selection behavior intact, and ensure the `archive` command path
consistently uses the selected store for all spec/change reads.
In @.claude/commands/opsx/explore.md:
- Around line 14-115: The `/opsx:explore` flow is missing store scoping for
OpenSpec reads, so it can query the wrong context when a store is selected.
Update the explore command logic to resolve the store with `openspec store list
--json` and then pass `--store <id>` to the initial `openspec list --json` and
any follow-up `openspec status --change ... --json` calls, keeping the flag on
all spec/change reads just like the other OpenSpec commands.
In @.claude/skills/openspec-apply-change/SKILL.md:
- Around line 30-32: The markdown examples in SKILL.md contain unlabeled fence
blocks, which will keep triggering markdownlint. Update the affected examples in
the skill docs by adding bash to command fences and text to output fences, and
keep the surrounding blank lines around each fenced block. Use the existing
fenced example sections in the document to locate and fix all untagged blocks
consistently.
In @.claude/skills/openspec-archive-change/SKILL.md:
- Around line 74-86: In the archive-change examples within SKILL.md, one shell
fence and the final output fence are missing language tags, which will fail
markdownlint. Update the relevant fenced code blocks in the archive instructions
so the command block uses bash and the summary/output block uses text, keeping
the surrounding archive flow and symbols like changeRoot and
planningHome.changesDir unchanged.
In @.claude/skills/openspec-explore/SKILL.md:
- Around line 56-71: Several fenced examples in SKILL.md are missing language
tags, causing markdownlint MD040/MD031 warnings; update the affected fences to
use the appropriate identifiers consistently (for example, use text for ASCII
diagrams and dialogue examples, and bash for shell commands) so all fenced
blocks in the open-spec guide have explicit languages. Use the existing fenced
sections in the documentation as the targets and keep the content unchanged
while only adding the correct fence labels.
In @.claude/skills/openspec-propose/SKILL.md:
- Around line 23-87: The command examples in the Steps section still omit the
store flag after the Store selection guidance, so update the `openspec
new/status/instructions` examples in `SKILL.md` to consistently include `--store
<id>` wherever the store-selection rule applies. Keep the existing
nearest-local-root behavior only for the no-store case, and make sure the change
is reflected in the step-by-step commands by referencing the same `openspec`
commands already listed.
In @.claude/skills/openspec-sync-specs/SKILL.md:
- Around line 16-35: The openspec sync workflow is missing --store on the
discovery and status steps, so it can target the nearest local root instead of
the selected store. Update the instructions in SKILL.md so the `openspec list
--json` and `openspec status --change "<name>" --json` steps also use the same
store id resolved earlier, and keep the flag consistent with the store-selection
guidance for commands like `list` and `status`.
In `@back-end/src/discovery/description.ts`:
- Around line 27-50: `fetchDescription` currently trusts SSDP-provided
`location` values and reads the full response body, which can enable SSRF/DoS.
Update `fetchDescription` to validate the target host before calling `request`
(for example by allowing only private/local addresses or otherwise explicitly
trusted hosts) and add a maximum response size check before/while consuming
`body.text()`. If broad LAN trust is intentional, document that assumption in
the security/threat-model notes tied to `fetchDescription`, `request`, and
`parseDescriptionXml`.
In `@back-end/src/discovery/registry.test.ts`:
- Around line 98-119: The current registry behavior leaves an old offline UDN
row behind when a new UDN takes over the same IP, so the snapshot can accumulate
duplicate physical-device entries. Update the device registry logic around
createDeviceRegistry()/upsert() and snapshot() to either merge the old offline
record into the new one or evict stale offline rows for the same IP, keeping
only one physical TV row per IP. If this history is intentionally preserved,
clarify that in the test expectations; otherwise adjust the registry to collapse
the offline “ghost” entry before snapshotting.
In `@back-end/src/discovery/ssdp.ts`:
- Around line 67-117: The SSDP transport start flow in start() can leave a
half-initialized instance if client.start() succeeds but notifySocket.bind() or
notifySocket.addMembership() fails. Wrap the startup sequence in try/catch, and
on failure stop the NodeSsdpClient via client?.stop(), clear
client/notifySocket, and reset started so retries or embedded restarts can
recover cleanly. Use the existing start(), client, notifySocket, and started
symbols to locate and update the lifecycle handling.
In `@back-end/src/routes/browser.ts`:
- Around line 53-60: The `request.log.info` call in `browser` is logging the
full user-provided `url`, which can leak unrelated secrets that
`TOKEN_QUERY_FRAGMENT` does not redact. Update the `browser` route logging to
avoid emitting the raw `url` and instead log only safe fields like `host` or
`origin` alongside `correlationId` and `udn`. Keep the
`request.id`/`parsed.host` context for debugging, and ensure no full query
string from the user input is sent to logs.
In `@back-end/src/routes/inputs.test.ts`:
- Around line 1-73: The test setup boilerplate in this spec is duplicated from
the keys route tests, so extract the shared harness into a reusable test helper.
Move `StubSsdpTransport`, `stubMdnsClient`, and the `createApp` bootstrap logic
into a common factory such as `createTestApp()` in a shared test-helpers module,
and have both `inputs.test.ts` and `keys.test.ts` use it while still allowing
each test to inject its own stub transport and receive `{ app, baseUrl, ssdp }`.
In `@back-end/src/routes/keys.ts`:
- Around line 30-40: The session lookup and connected-state guard in the keys
route duplicates the same get-or-ensure + Connected check used elsewhere.
Extract this logic into a shared helper such as ensureConnectedSession(manager,
udn) in the tv module, and have both the keys route and sendKey in tv/volume.ts
call it. Keep the helper responsible for manager.get/ensure, unknown UDN
notFound, and the SessionNotConnected 409 so the behavior stays consistent in
one place.
In `@back-end/src/routes/volume.test.ts`:
- Around line 149-160: The mute route tests are order-dependent because they
assume the session state is set by earlier tests. Update the related tests
around POST /mute and the connected-state scenario to explicitly establish their
own state using the existing connect/disconnect flow before asserting, so each
test is isolated and no longer depends on node:test execution order or file
position.
In `@back-end/src/routes/volume.ts`:
- Around line 31-32: `registerVolumeRoutes` currently uses a factory parameter
(`volume: VolumeModule`) while `keys.ts`/`sessions.ts` resolve dependencies from
`app.discovery?.sessions`; align the dependency-injection pattern across these
route modules for consistency. Update `registerVolumeRoutes` to follow the same
style used by the other routes, or refactor the other routes to match this
factory-based approach, and keep the implementation centered around the
`registerVolumeRoutes` entry point and its Fastify registration flow.
In `@back-end/src/tv/errors.ts`:
- Around line 94-118: `mapWsError` is leaking raw WebSocket close codes and
Smart View event names into `TvError.message`, which then gets exposed through
`toHttpEnvelope`. Update `mapWsError` in `errors.ts` so `TvFailed`,
`TvNotReachable`, and `TvUnknown` use sanitized, user-safe messages instead of
interpolating `signal.code` or `signal.event`. If you still need the raw values
for debugging, keep them out of `message` and log or store them separately; add
or extend tests in `errors.test.ts` to verify the HTTP envelope does not contain
raw WS codes or event names.
In `@back-end/src/tv/keys.ts`:
- Around line 1-12: No code change is needed here: the review comment is
confirming that keys.ts already documents the enum-only key contract and the
AGENTS.md/D1 references correctly, so leave this docstring as-is and keep the
related session-integration.test.ts coverage intact.
In `@back-end/src/tv/session-integration.test.ts`:
- Around line 217-227: The test in test('heartbeat failure that never recovers
ends in Disconnected (retry cap)') relies on a fixed setTimeout sleep before
asserting the session state, which can flake under CI load. Replace that single
hard-coded delay with a short polling loop that repeatedly fetches the session
endpoint until the state becomes Disconnected or a timeout is reached, then keep
the existing assertion and cleanup around fakeTv.setOffline().
In `@docs/requirements.md`:
- Around line 69-72: Update the requirements text to make Smart View WebSocket
the source of truth instead of IP Control. In docs/requirements.md, revise the
TV Connection, Remote Control, and Constraints wording so it consistently
references Smart View WS on port 8001; use the existing requirement IDs/section
titles to locate the affected entries and keep the brief aligned with the same
transport terminology.
In `@front-end/package.json`:
- Line 2: The package name is still using the old mislabeled product identifier,
so update the name field in package.json to match the actual mytv product naming
used elsewhere in the app. Use the package metadata entry as the source of truth
and replace the legacy samsung-ip-controll-ui value with the correct
mytv-aligned package name so the project identity is consistent.
In `@front-end/src/App.css`:
- Around line 1-184: Remove the dead Vite starter stylesheet by deleting the
unused App.css content and any related references, since App.tsx does not import
it and the remaining selectors like .counter, .hero, `#center`, and `#next-steps`
are only legacy scaffolding. Verify the app still uses its intended styling from
the active entry point and that no components depend on these obsolete classes
before removing the file.
In `@front-end/src/App.tsx`:
- Around line 7-13: App currently ignores the loading state from useDevices(),
so DeviceListScreen can show the empty state before the initial sync finishes.
Update App to read and pass loading (or an equivalent sync-complete flag) from
useDevices into DeviceListScreen, and make RemoteScreen/DeviceListScreen
rendering depend on that state so EmptyState is only shown after loading
completes.
In `@front-end/src/data/useVolume.ts`:
- Around line 56-119: The volume WebSocket in useEffect is one-shot and never
reconnects, so muted can freeze after disconnects. Add reconnect handling for
the socket by wiring onclose/onerror (or, better, extracting the existing
reconnect logic from useWsHealth into a shared useReconnectingSocket helper) and
use it here instead of only listening for message events. Keep the initial fetch
logic, but ensure the socket is recreated with backoff and listeners are
reattached after failures.
In `@front-end/src/ds.d.ts`:
- Around line 8-71: The `declare module` blocks in `ds.d.ts` are repeating the
same `import type ...` plus `export const ...: ComponentType<...>` pattern for
each component, so replace the boilerplate with a shared helper/type or
generator-friendly structure instead of manually duplicating 13 nearly identical
declarations. Keep the existing module names like
`@ds/components/core/Button.jsx` and `@ds/components/feedback/Toast.jsx`, but
centralize the typing pattern so new `@ds/**` components can be added with
minimal duplication.
In `@front-end/src/errors/messages.ts`:
- Around line 30-34: The TvUnknown message in messages.ts uses overly technical
wording (“check the back-end log”) that does not match the user-facing tone of
the other error messages. Update the message returned by the TvUnknown case in
the message mapping to use clearer, second-person guidance consistent with
nearby messages such as Tap Connect and Try again, while keeping the warning
tone and the same TvUnknown branch.
In `@front-end/src/screens/InputsModal.tsx`:
- Around line 1-41: The InputsModal changes need explicit visual verification
beyond tests/build output, since a successful build or npm run front:test does
not confirm the modal behaves correctly in the browser. Open InputsModal in the
front-end dev server, click through the input rows, and verify the modal closes
and the disabled/connected states render as expected; if anything is off, adjust
the component behavior in InputsModal/handleRowClick accordingly before merging.
- Around line 14-25: The row click flow in InputsModal is not guarded against
in-flight setInput calls, so a second tap can trigger another request before the
first finishes. Add a local pending state in InputsModal and set it around
handleRowClick so rows are disabled while setInput(entry.id) is pending, then
clear it in finally before calling onClose(). Use the existing InputsModal and
handleRowClick symbols to keep the fix localized.
In `@LOOP.md`:
- Around line 60-62: The fenced block in the commit template needs an explicit
language tag and cleaner surrounding markdown spacing. Update the fenced section
in LOOP.md around the Co-Authored-By example to use the same block but with a
text fence, and ensure the indentation/blank lines around the block match
markdownlint expectations so the template passes lint.
In
`@openspec/changes/archive/2026-07-04-default-port-80/specs/platform-foundation/spec.md`:
- Around line 7-10: Update the `GET /api/health` contract in the `Scenario: API
responds under /api` so it matches the actual response shape from `health.ts`;
the current spec incorrectly narrows the JSON to only `{ "status": "ok" }`.
Adjust the expected body to include the additional `mdns` field returned by the
health route, keeping the scenario aligned with the real `GET /api/health`
behavior.
In `@openspec/changes/archive/2026-07-04-default-port-80/tasks.md`:
- Line 10: The sync step references the wrong change path, so update the task
wording to use the archive location for this change instead of the non-archive
`openspec/changes/default-port-80/...` path. Adjust the instruction in the task
entry that describes syncing `platform-foundation/spec.md` so it points to the
existing archive change directory, preserving the same delta details and
scenarios.
In `@openspec/changes/archive/2026-07-04-mdns-advertisement/tasks.md`:
- Around line 23-24: The verification note mixes two different ports, so make
the mDNS health-check example consistent by using one actual endpoint throughout
or explicitly stating why `3302` differs from the advertised `3000`. Update the
task note in this archive entry so the `curl` example, hostname, and
health-check result all refer to the same port, keeping the `mdns.state`
verification and `mDNS interface change detected; re-advertising` note intact.
In `@openspec/changes/archive/2026-07-04-platform-foundation/design.md`:
- Around line 42-48: The design doc currently describes Pino/Fastify path-based
redaction, but the actual implementation uses recursive formatting to strip
nested AccessToken values. Update the D3 section to match the real behavior in
logger.ts by describing the formatter-based redaction approach rather than
pino.redact, and keep the references to req.id/correlationId and JSON-RPC id so
the logging contract remains clear.
In
`@openspec/changes/archive/2026-07-04-platform-foundation/specs/platform-foundation/spec.md`:
- Around line 45-57: The requirement is too broad because it forbids absolute
hosts everywhere in the front-end change set, including Vite dev-proxy
configuration that must point to localhost. Update the “Front-end uses relative
HTTP paths” requirement in platform-foundation/spec.md to scope the ban only to
runtime HTTP/WebSocket calls in app code, and explicitly exempt vite.config.ts
or other proxy/server configuration; keep the scenarios in terms of client calls
through relative paths while allowing the dev proxy target to remain absolute.
In `@openspec/changes/archive/2026-07-04-remote-control-keys/proposal.md`:
- Line 7: The proposal still references samsung-ip-control-protocol, but this
transport should be aligned with the Smart View D1 wording used in design.md.
Update the command endpoint description in proposal.md to describe dispatching
remoteKeyControl through the Smart View transport/session queue owned by
tv-connection-lifecycle, keeping the request/response behavior unchanged. Use
the existing remoteKeyControl and tv-connection-lifecycle symbols as anchors
when revising the text.
In `@openspec/changes/archive/2026-07-04-tv-connection-lifecycle/tasks.md`:
- Around line 13-14: Update the pairing task in the archive notes so it is not
marked as complete while the real TV flow remains speculative. In the `2.4
Implement pairing` entry, replace the completed status with a
blocked/experimental state or add explicit wording that the production
`connect()` path for Samsung TVs is not yet verified, since `getAccessToken`,
`TvNotSupported`, and the `MYTV_TOKEN_<UDN>` preload path are only
test/speculative fallbacks. Keep the wording aligned with the existing
`connect()`/`getAccessToken` flow description and make sure the task status
reflects the actual implementation maturity.
In `@openspec/changes/archive/2026-07-04-upnp-tv-discovery/design.md`:
- Around line 23-29: Clarify the D1 architecture note so it no longer implies
node-ssdp handles both discovery directions end-to-end. Update the SSDP section
in the design doc referenced by the D1 heading to state that node-ssdp is used
for M-SEARCH/client discovery, while NOTIFY handling is implemented separately
via a raw dgram socket on port 1900. Make the responsibility split explicit so
readers can locate the transport boundary without ambiguity.
In `@openspec/changes/archive/2026-07-05-error-surfacing/tasks.md`:
- Around line 34-35: Step 5.5 is still shown as pending, so the task record
should either be completed with a real manual QA result or explicitly marked as
deferred in the current state notes. Update the task entry in tasks.md and, if
TV/LAN is unavailable, add a brief note in docs/current-state.md referencing the
manual QA gap so the status is not misleading. Use the existing “5.5 Manual QA”
and “docs/current-state.md” references to locate the affected documentation.
In
`@openspec/changes/archive/2026-07-05-input-management/specs/input-management/spec.md`:
- Line 1: The spec markdown starts with a level-2 heading, which triggers the
MD041 lint rule; update the document to begin with a top-level H1 heading before
the existing “ADDED Requirements” section. Make the change in the spec content
itself so the file remains valid Markdown and navigable, using the document’s
main title as the H1.
- Around line 54-57: The acceptance criterion is too narrow because it only
checks InputsModal.tsx, so hard-coded colors in Modal, ListRow, or shared styles
can still slip through. Broaden the scenario in the inputs-management spec to
cover the full inputs flow or replace the grep-based check with an automated
lint/test that validates all relevant inputs UI components and shared styling
symbols.
In `@openspec/changes/archive/2026-07-05-input-management/tasks.md`:
- Around line 4-6: The inputs endpoint is defined under one path in the backend
plan but consumed under a different path in the frontend, which will cause the
initial mount to fail with 404 and duplicate the contract. Align the route so
the same inputs list is served from one canonical path, then update the related
frontend hook and tests to use that single endpoint. Use the existing inputs
route registration in app.ts and the broker wiring in createDevicesBroker as the
places to verify the final contract.
In `@openspec/config.yaml`:
- Around line 4-16: Update the root config in the config’s project/architecture
description to reflect Smart View WebSocket control instead of the old IP
Control JSON-RPC/HTTPS contract. Replace references in the affected config block
to JSON-RPC, Samsung IP Control, HTTPS ports, and request IDs with Smart View WS
details such as a single WS session per TV, ws:// on port 8001,
ms.channel.connect, and token redaction in logs. Use the existing config
sections in openspec/config.yaml as the anchor for the rewrite so future
seed-context points to the correct transport.
In `@openspec/specs/platform-foundation/spec.md`:
- Around line 11-15: The /api/health scenario is too narrow and omits the
existing mdns contract used by diagnostics. Update the spec in the Scenario
under the API health behavior so it references the mdns-related
response/behavior from health handling in health.ts, and make the wording
consistent with mdns-advertisement/spec.md so both specs describe the same
/api/health contract.
In `@openspec/specs/upnp-tv-discovery/spec.md`:
- Around line 92-100: The SSDP pre-filter in the discovery orchestrator is too
broad because the generic `uuid:` check will match most non-Samsung devices and
let them through to fetch. Tighten the header inspection logic around the
Samsung-specific discriminator used before enqueueing description fetches so
only `USN`, `SERVER`, or `ST` values with an actual Samsung signature are
accepted, and keep the `debug`-level drop path for everything else. Update the
behavior described in the SSDP discovery scenario so the fast-path truly blocks
non-Samsung hits before `undici.request` is reached.
In `@scripts/install.sh`:
- Around line 33-37: The `case` in `scripts/install.sh` has a dead pattern
because `linux*` already matches `linux-gnu*`, making the extra alternative
unreachable. Simplify the `OSTYPE` match in the host check by removing the
redundant `linux-gnu*` branch and keep the existing no-op handling in the
`linux*`/Linux path.
- Around line 95-98: `install_prod_deps()` is still running `npm ci --omit=dev`
directly, so `--dry-run` does not stay non-mutating and can change
`back-end/node_modules`; wrap that install call in `run()` so it is skipped in
dry-run mode, and update the help/logging around the non-mutating command list
to remove `npm ci` from what preview mode implies. Use `install_prod_deps`,
`run`, and the install/help output in `scripts/install.sh` to locate the change.
In `@scripts/install.test.bats`:
- Around line 120-135: Add a dry-run test for install_prod_deps, since the
current test only covers the run() helper and not the npm ci path. Update the
tests in scripts/install.test.bats to invoke install_prod_deps with DRY_RUN=1
and verify it goes through run() instead of executing a real npm ci. Use the
install_prod_deps function and the existing DRY-RUN behavior to assert the
command is logged, and consider stubbing npm with a shim that fails if a real
execution happens.
In `@scripts/uninstall.sh`:
- Around line 16-20: The OSTYPE check in uninstall.sh still includes the
redundant linux-gnu* pattern, which is the same host-detection issue as in
install.sh. Update the case statement in the host validation logic to use a
single Linux match that correctly covers Linux OSTYPE values, and keep the die
paths unchanged; use the existing OSTYPE-based switch in the uninstall script to
make the fix consistent with the corresponding installer check.
---
Outside diff comments:
In `@openspec/changes/archive/2026-07-04-tv-connection-lifecycle/tasks.md`:
- Around line 38-43: Adjust the 6.2/6.3 archive status in tasks.md so the
live-smoke entry is not marked fully done unless `Connected` was actually
reached. In the `6.2` and `6.3` bullets, use explicit `partial`/`deferred`
wording and keep the `Connected` outcome separate from the verified transport
and pairing steps; refer to the existing `6.2`, `6.3`, and the `Connected →
Reconnecting → Connected` session tests only as supporting evidence, not as
completion proof.
---
Duplicate comments:
In @.agents/skills/react-vite-best-practices/rules/build-tree-shaking.md:
- Around line 111-129: The tree-shaking example in the Vite config mixes
production treeshake settings with dev-only dependency pre-bundling. Remove the
optimizeDeps block from this example and keep only the
build.rollupOptions.treeshake configuration in the snippet, or move the
optimizeDeps guidance to the dev-dependency-prebundling documentation. Use the
defineConfig example as the reference point when updating the tree-shaking rule
content.
In `@back-end/src/logger.ts`:
- Line 3: SENSITIVE_KEYS matching in logger.ts is still case-sensitive, so
secret fields like Token, AccessToken, or Authorization can slip into logs.
Update the redaction logic used by the logger’s object traversal/redaction path
to normalize keys before checking SENSITIVE_KEYS (for example, compare
lowercased key names) and make sure the same normalization applies recursively
to nested objects and header-like payloads. Use the existing SENSITIVE_KEYS set
and the logging/redaction helper in logger.ts so all log lines are sanitized
consistently regardless of key casing.
In `@back-end/src/mdns.test.ts`:
- Around line 109-127: The current test covers only the synchronous publish()
throw path, but the review asks for the async socket-error path handled via
rawClient.server?.mdns?.on('error', ...). Update the mdns.test.ts coverage
around startMdns to use a stub client shaped like { server: { mdns: { on } } }
that emits an EADDRINUSE error asynchronously, matching the AVAHI_HINT conflict
case. Verify the same handle.state, handle.error, and handle.address assertions
for the async error path, and keep the existing error-state expectations in the
startMdns behavior.
In `@back-end/src/routes/browser.test.ts`:
- Around line 134-210: The browser route tests are missing coverage for
transport-level command rejection after the session is already connected. Add a
case in browser.test.ts around the POST /browser flow that connects first, then
mocks session.enqueue() to reject with TvError, and assert the response maps to
an allowed union error code rather than exposing raw wire details. Also verify
no frame is sent by checking stub.calls does not increase, using the existing
post() helper and the browser route behavior as the reference point.
In `@front-end/src/screens/DeviceListScreen.tsx`:
- Around line 14-42: The top-level layout in DeviceListScreen still uses
hardcoded pixel spacing instead of design tokens. Update the wrapping container
and header spacing in DeviceListScreen to use the same spacing tokens already
used by EmptyState (for example the existing var(--space-*) values) instead of
raw values like 40px, 48px, 32, 16, and 6. Keep the component structure the
same, but make the spacing consistent with the design system across this screen.
In
`@openspec/changes/archive/2026-07-04-smart-view-ws-transport/specs/upnp-tv-discovery/spec.md`:
- Around line 5-7: The pre-fetch Samsung filter in the SSDP discovery flow is
too broad because the `uuid:` check matches generic UPnP devices; tighten the
header inspection in the discovery orchestrator so only truly Samsung-specific
signatures are accepted before enqueueing a fetch. Update the logic that
examines `USN`, `SERVER`, and `ST` to rely on Samsung-specific markers and keep
the post-fetch `isSamsungTv` check as the fallback for ambiguous cases.
In `@openspec/changes/archive/2026-07-06-tv-browser-launch/design.md`:
- Around line 106-109: The D6 logging note still allows a raw user-provided url
to be emitted at info, which should be removed or sanitized in the design.
Update the logging guidance in the archive design so the entry log uses only
safe fields like correlationId, udn, and host/origin, or explicitly a scrubbed
URL, and make it clear not to rely on logger.ts redaction alone. Use the D6
Logging & redaction section as the place to adjust this guidance.
In `@scripts/build.sh`:
- Around line 14-45: The helper logic in build.sh is duplicated with install.sh,
so extract the shared functions need_cmd, check_prereqs, resolve_paths, log, and
die into a reusable scripts/lib.sh and source it from build.sh (and the matching
install/uninstall scripts if needed). Keep the behavior identical by moving the
common implementations without changing their call sites such as check_prereqs
and resolve_paths, so the shared shell tests and bats setup continue to work.
In `@scripts/mytv.service.tmpl`:
- Around line 7-29: The systemd unit in mytv.service.tmpl is missing basic
hardening directives, so update the service section to add NoNewPrivileges,
ProtectSystem, ProtectHome, and PrivateTmp while keeping the existing ExecStart
and environment setup intact. Since the binary already gets cap_net_bind_service
via setcap rather than AmbientCapabilities, these protections can be added
safely without affecting port binding; apply the change directly in the unit
template alongside the current [Service] settings.
In `@scripts/uninstall.sh`:
- Around line 30-35: The unit-existence check in the uninstall flow is wrong
because `systemctl list-unit-files` can succeed even when `mytv.service` is not
present, so the “not registered” branch in the uninstall script is misleading.
Update the logic in the uninstall script around the `mytv.service` handling to
use a real unit-file check such as `[[ -f "$UNIT_PATH" ]]`, or remove the
precheck and make `systemctl disable --now mytv` a best-effort call with errors
suppressed. Keep the existing `log` messages aligned with the actual behavior.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
| ```typescript | ||
| // ✅ Good: Preload on hover for instant navigation | ||
| const Dashboard = lazy(() => import('./pages/Dashboard')) | ||
|
|
||
| function NavLink() { | ||
| const preloadDashboard = () => { | ||
| import('./pages/Dashboard') | ||
| } | ||
|
|
||
| return ( | ||
| <Link | ||
| to="/dashboard" | ||
| onMouseEnter={preloadDashboard} | ||
| onFocus={preloadDashboard} | ||
| > | ||
| Dashboard | ||
| </Link> | ||
| ) | ||
| } | ||
| ``` |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE=".agents/skills/react-vite-best-practices/rules/split-route-lazy.md"
echo "== file stats =="
wc -l "$FILE"
echo "== relevant excerpt =="
sed -n '1,170p' "$FILE" | cat -nRepository: AntoniPetrenko/2026-fwdays-agentic-greenfield-task-AP
Length of output: 4459
Додайте Link до імпорту в другому прикладі. Другий snippet не збереться як є: Link використано, але не імпортовано.
🛠️ Можливий фікс
-import { BrowserRouter, Routes, Route } from 'react-router-dom'
+import { BrowserRouter, Routes, Route, Link } from 'react-router-dom'🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In @.agents/skills/react-vite-best-practices/rules/split-route-lazy.md around
lines 84 - 103, The second example in the split-route-lazy rule uses Link inside
NavLink without importing it, so the snippet will not compile. Update the
example’s imports to include Link from the appropriate source alongside the
existing lazy usage, and keep the NavLink/preloadDashboard example otherwise
unchanged.
| export async function fetchDescription( | ||
| location: string, | ||
| options: FetchDescriptionOptions = {}, | ||
| ): Promise<DeviceDescription | null> { | ||
| const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS; | ||
| const controller = new AbortController(); | ||
| const timer = setTimeout(() => controller.abort(), timeoutMs); | ||
| try { | ||
| const { statusCode, body } = await request(location, { | ||
| method: 'GET', | ||
| signal: controller.signal, | ||
| }); | ||
| if (statusCode < 200 || statusCode >= 300) { | ||
| await body.dump(); | ||
| return null; | ||
| } | ||
| const xml = await body.text(); | ||
| return parseDescriptionXml(xml); | ||
| } catch { | ||
| return null; | ||
| } finally { | ||
| clearTimeout(timer); | ||
| } | ||
| } |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '\n== Files ==\n'
git ls-files | rg '(^|/)(description\.ts|discovery|smart-view-ws-transport|upnp|design\.md|AGENTS\.md)$'
printf '\n== Outline: back-end/src/discovery/description.ts ==\n'
ast-grep outline back-end/src/discovery/description.ts --view expanded || true
printf '\n== Search fetchDescription usage ==\n'
rg -n "fetchDescription\(" back-end || true
printf '\n== Search relevant docs ==\n'
rg -n "SSDP|LOCATION|SSRF|threat model|design\.md|D1|smart-view-ws-transport|upnp-tv-discovery" . || trueRepository: AntoniPetrenko/2026-fwdays-agentic-greenfield-task-AP
Length of output: 50409
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '\n== back-end/src/discovery/description.ts ==\n'
sed -n '1,140p' back-end/src/discovery/description.ts
printf '\n== back-end/src/discovery/index.ts (location fetch path) ==\n'
sed -n '70,130p' back-end/src/discovery/index.ts
printf '\n== openspec/specs/upnp-tv-discovery/spec.md relevant lines ==\n'
sed -n '1,140p' openspec/specs/upnp-tv-discovery/spec.md
printf '\n== archived smart-view-ws-transport spec relevant lines ==\n'
sed -n '1,80p' openspec/changes/archive/2026-07-04-smart-view-ws-transport/specs/upnp-tv-discovery/spec.mdRepository: AntoniPetrenko/2026-fwdays-agentic-greenfield-task-AP
Length of output: 16683
fetchDescription треба обмежити довірені location-адреси
request(location, ...) бере URL із SSDP без перевірки хоста й читає тіло цілком через body.text(). Будь-який пристрій у LAN може підсунути LOCATION на внутрішній сервіс або дуже велике тіло, тож тут лишається blind SSRF/DoS. Додай перевірку на приватний/локальний хост і ліміт на розмір відповіді; якщо такий рівень довіри до LAN задуманий, зафіксуй це в threat model.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@back-end/src/discovery/description.ts` around lines 27 - 50,
`fetchDescription` currently trusts SSDP-provided `location` values and reads
the full response body, which can enable SSRF/DoS. Update `fetchDescription` to
validate the target host before calling `request` (for example by allowing only
private/local addresses or otherwise explicitly trusted hosts) and add a maximum
response size check before/while consuming `body.text()`. If broad LAN trust is
intentional, document that assumption in the security/threat-model notes tied to
`fetchDescription`, `request`, and `parseDescriptionXml`.
| test('IP-based dedup: once the winner ages offline, a new UDN at the same IP is accepted', () => { | ||
| const registry = createDeviceRegistry(); | ||
|
|
||
| registry.upsert( | ||
| { udn: 'udn-a', name: 'Living Room', model: 'UN65', ip: '10.0.0.42', port: 9197 }, | ||
| 0, | ||
| ); | ||
| registry.markOfflineOlderThan(60_000, 120_000); | ||
|
|
||
| const second = registry.upsert( | ||
| { udn: 'udn-b', name: 'Living Room', model: 'UN65', ip: '10.0.0.42', port: 9119 }, | ||
| 120_000, | ||
| ); | ||
| assert.ok(second, 'a new UDN may take over an IP once the previous holder is offline'); | ||
|
|
||
| const snap = registry.snapshot(); | ||
| assert.equal(snap.length, 2); | ||
| const a = snap.find((d) => d.udn === 'udn-a')!; | ||
| const b = snap.find((d) => d.udn === 'udn-b')!; | ||
| assert.equal(a.status, 'offline'); | ||
| assert.equal(b.status, 'online'); | ||
| }); |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🔵 Trivial
Перевір: чи офлайн-рядки при зміні UDN на тій самій IP колись зливаються/видаляються?
Тест підтверджує, що основний "collapse" сценарій (два живі UDN на одній IP → одна запис) працює правильно й відповідає house rule "discovery should collapse multiple UPnP UDNs that share one IP into a single physical TV row". Але коли перший власник IP іде в офлайн і нова IP займається іншим UDN, snapshot зберігає ОБИДВА рядки (line 114: assert.equal(snap.length, 2)) — старий залишається офлайн назавжди. Немає доступу до registry.ts, щоб перевірити, чи є десь merge/eviction для таких історичних записів — якщо ні, UI пристроїв з часом накопичуватиме "привидів" для тієї самої фізичної IP.
Чи це свідомий продуктовий вибір (історія пристроїв), чи варто зливати/чистити такі записи?
Based on coding guidelines: "discovery should collapse multiple UPnP UDNs that share one IP into a single physical TV row."
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@back-end/src/discovery/registry.test.ts` around lines 98 - 119, The current
registry behavior leaves an old offline UDN row behind when a new UDN takes over
the same IP, so the snapshot can accumulate duplicate physical-device entries.
Update the device registry logic around createDeviceRegistry()/upsert() and
snapshot() to either merge the old offline record into the new one or evict
stale offline rows for the same IP, keeping only one physical TV row per IP. If
this history is intentionally preserved, clarify that in the test expectations;
otherwise adjust the registry to collapse the offline “ghost” entry before
snapshotting.
| async function start(): Promise<void> { | ||
| if (started) return; | ||
| started = true; | ||
|
|
||
| client = new NodeSsdpClient({ reuseAddr: true }); | ||
| client.on('response', (headers) => { | ||
| const location = typeof headers.LOCATION === 'string' ? headers.LOCATION : ''; | ||
| const st = typeof headers.ST === 'string' ? headers.ST : ''; | ||
| const usn = typeof headers.USN === 'string' ? headers.USN : ''; | ||
| const server = typeof headers.SERVER === 'string' ? headers.SERVER : undefined; | ||
| handleHit({ | ||
| location, | ||
| st, | ||
| usn, | ||
| ...(server !== undefined && { server }), | ||
| source: 'msearch', | ||
| }); | ||
| }); | ||
| await client.start(); | ||
| logger.info('SSDP client started'); | ||
|
|
||
| notifySocket = createSocket({ type: 'udp4', reuseAddr: true }); | ||
| notifySocket.on('error', (err) => { | ||
| logger.warn( | ||
| { err, port: SSDP_PORT }, | ||
| 'SSDP NOTIFY listener socket error', | ||
| ); | ||
| }); | ||
| notifySocket.on('message', (msg) => { | ||
| const parsed = parseNotify(msg.toString('utf8')); | ||
| if (!parsed) return; | ||
| handleHit({ ...parsed, source: 'notify' }); | ||
| }); | ||
|
|
||
| await new Promise<void>((resolve, reject) => { | ||
| notifySocket!.bind(SSDP_PORT, () => { | ||
| try { | ||
| notifySocket!.addMembership(SSDP_MULTICAST_IP); | ||
| notifySocket!.unref(); | ||
| logger.info( | ||
| { port: SSDP_PORT, group: SSDP_MULTICAST_IP }, | ||
| 'SSDP NOTIFY listener joined multicast group', | ||
| ); | ||
| resolve(); | ||
| } catch (err) { | ||
| reject(err); | ||
| } | ||
| }); | ||
| notifySocket!.once('error', reject); | ||
| }); | ||
| } |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Map the relevant file and nearby symbols first.
ast-grep outline back-end/src/discovery/ssdp.ts --view expanded
# Read the full file with line numbers.
wc -l back-end/src/discovery/ssdp.ts
cat -n back-end/src/discovery/ssdp.ts | sed -n '1,240p'
# Find stop/cleanup logic and any callers of start().
rg -n "async function start|function stop|client\.stop|notifySocket|createSocket|NodeSsdpClient|started =" back-end/src/discovery/ssdp.ts back-end/src/discovery -g '*.ts' -g '*.js'
# Check whether start() is paired with external cleanup assumptions.
rg -n "\.start\(\)|\.stop\(\)|ssdp" back-end/src -g '*.ts' -g '*.js'Repository: AntoniPetrenko/2026-fwdays-agentic-greenfield-task-AP
Length of output: 15052
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Inspect the discovery startup/teardown flow.
ast-grep outline back-end/src/discovery/index.ts --view expanded
wc -l back-end/src/discovery/index.ts
cat -n back-end/src/discovery/index.ts | sed -n '1,260p'
# Inspect app lifecycle around discovery startup failures.
ast-grep outline back-end/src/app.ts --view expanded
wc -l back-end/src/app.ts
cat -n back-end/src/app.ts | sed -n '120,240p'
# Look for tests around SSDP transport start/stop behavior.
wc -l back-end/src/discovery/discovery.test.ts
cat -n back-end/src/discovery/discovery.test.ts | sed -n '1,260p'Repository: AntoniPetrenko/2026-fwdays-agentic-greenfield-task-AP
Length of output: 21822
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Find the server entrypoint and inspect startup error handling.
git ls-files 'back-end/src/*' 'back-end/*.ts' | rg 'main|server|index|app\.ts$'
rg -n "listen\(|app\.listen|close\(\)|catch \(.*listen|onReady" back-end/src back-end -g '*.ts' -g '*.js'
# Read the likely entrypoint(s) if present.
for f in back-end/src/main.ts back-end/src/server.ts back-end/src/index.ts; do
if [ -f "$f" ]; then
echo "### $f"
wc -l "$f"
cat -n "$f" | sed -n '1,240p'
fi
doneRepository: AntoniPetrenko/2026-fwdays-agentic-greenfield-task-AP
Length of output: 11260
🌐 Web query:
Fastify onReady failure onClose hooks listen rejects cleanup startup failure
💡 Result:
In Fastify, the startup and shutdown lifecycles are distinct processes, and failures during startup do not automatically trigger onClose hooks unless the server has reached a state where they are registered and effectively invoked [1][2]. Startup Lifecycle and Failure The onReady hooks are triggered before the server begins listening [1]. If an error occurs during the execution of onReady hooks or other startup procedures (such as plugin registration), the startup process is halted, and the error is propagated to the caller of.ready or the.listen promise [1][3][4]. Because the server has not yet successfully entered the listening state, it is generally not considered fully booted, and the standard graceful shutdown sequence—which includes the execution of onClose hooks—is not automatically initiated as a recovery mechanism for a failed startup [1][2]. onClose Lifecycle The onClose hooks are specifically designed for the graceful shutdown sequence, which is triggered when fastify.close is explicitly called [1][5][2]. At this stage: 1. The server stops accepting new requests (and may reject them with 503 if configured) [1][2]. 2. In-flight requests are allowed to complete [5][2]. 3. Connection draining occurs [2]. 4. Finally, onClose hooks execute to allow for the release of resources like database connections [1][2]. Handling Cleanup on Startup Failure If a startup failure occurs (e.g., a plugin fails to connect to a database), you must manually ensure that any partially initialized resources are cleaned up [4]. Fastify does not implicitly track and revert the state of all plugins if a startup hook fails. Developers are encouraged to use proper error handling during registration (e.g., using.after or checking the promise returned by register) to catch these failures early and perform necessary cleanup actions before the application exits or retries [4]. Regarding listen and lifecycle state, if.listen is called and fails, the promise is rejected [6]. It is important to note that if a listen attempt fails, Fastify may not implicitly clear internal state associated with that failed attempt, which can lead to complications if multiple listen attempts are made [7][6]. Always ensure that resources are managed cleanly within your plugin or application logic to avoid dangling connections or unfinished cleanup tasks [4].
Citations:
- 1: https://github.com/fastify/fastify/blob/main/docs/Reference/Hooks.md
- 2: https://github.com/fastify/fastify/blob/HEAD/docs/Reference/Lifecycle.md
- 3: fix(#5049): Remove duplicated calls to onReady fastify/fastify#5051
- 4: https://github.com/fastify/fastify/blob/HEAD/docs/Guides/Plugins-Guide.md
- 5: https://fastify.io/docs/latest/Reference/Server/
- 6: listen() registers a callback even when listen() fails, calling the callback twice - once when it fails and again if it succeeds in a later call fastify/fastify#5519
- 7: Aborting
signalof server.lister method before actual listeting begin put alistenpromise into a floating state fastify/fastify#6230
Почисти частковий старт SSDP-транспорту back-end/src/discovery/ssdp.ts:67-117
Якщо client.start() уже спрацював, а bind() або addMembership() падає, started лишається true, а клієнт — без зупинки. Додай try/catch з client?.stop() і скиданням полів, щоб не лишати напівзапущений transport у retry/embedded сценаріях.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@back-end/src/discovery/ssdp.ts` around lines 67 - 117, The SSDP transport
start flow in start() can leave a half-initialized instance if client.start()
succeeds but notifySocket.bind() or notifySocket.addMembership() fails. Wrap the
startup sequence in try/catch, and on failure stop the NodeSsdpClient via
client?.stop(), clear client/notifySocket, and reset started so retries or
embedded restarts can recover cleanly. Use the existing start(), client,
notifySocket, and started symbols to locate and update the lifecycle handling.
| // Log the full URL alongside host — `logger.ts`'s TOKEN_QUERY_FRAGMENT | ||
| // regex redacts any `?token=…` / `&token=…` fragment before it hits | ||
| // the sink, so a user-pasted URL that carries a session token | ||
| // won't leak. | ||
| request.log.info( | ||
| { correlationId: request.id, udn, url, host: parsed.host }, | ||
| 'browser launch', | ||
| ); |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
Логування повного URL — потенційний витік чужих секретів у логи.
Регулярка TOKEN_QUERY_FRAGMENT у logger.ts вирізає лише token=/&token= — вона захищає саме pairing-токен TV. Але сюди прилітає довільний URL, який вставляє користувач: якщо в ньому є query-параметри іншого сервісу (session id, auth code, api key тощо), вони підуть у лог без редагування. Варто логувати лише host/origin, а не повний url, якщо повний рядок не є критичним для діагностики.
🔒 Пропозиція
request.log.info(
- { correlationId: request.id, udn, url, host: parsed.host },
+ { correlationId: request.id, udn, host: parsed.host, path: parsed.pathname },
'browser launch',
);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // Log the full URL alongside host — `logger.ts`'s TOKEN_QUERY_FRAGMENT | |
| // regex redacts any `?token=…` / `&token=…` fragment before it hits | |
| // the sink, so a user-pasted URL that carries a session token | |
| // won't leak. | |
| request.log.info( | |
| { correlationId: request.id, udn, url, host: parsed.host }, | |
| 'browser launch', | |
| ); | |
| // Log the full URL alongside host — `logger.ts`'s TOKEN_QUERY_FRAGMENT | |
| // regex redacts any `?token=…` / `&token=…` fragment before it hits | |
| // the sink, so a user-pasted URL that carries a session token | |
| // won't leak. | |
| request.log.info( | |
| { correlationId: request.id, udn, host: parsed.host, path: parsed.pathname }, | |
| 'browser launch', | |
| ); |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@back-end/src/routes/browser.ts` around lines 53 - 60, The `request.log.info`
call in `browser` is logging the full user-provided `url`, which can leak
unrelated secrets that `TOKEN_QUERY_FRAGMENT` does not redact. Update the
`browser` route logging to avoid emitting the raw `url` and instead log only
safe fields like `host` or `origin` alongside `correlationId` and `udn`. Keep
the `request.id`/`parsed.host` context for debugging, and ensure no full query
string from the user input is sent to logs.
Source: Coding guidelines
| Before enqueueing an SSDP hit for a description-XML fetch, the discovery orchestrator SHALL inspect the SSDP headers (`USN`, `SERVER`, `ST`) and drop the hit at level `debug` if none of them contain a Samsung signature — either the case-insensitive substring `samsung` or a Samsung UUID prefix pattern (`uuid:` prefix used by Samsung Tizen TVs). Post-fetch `isSamsungTv` remains as a defensive backstop for edge-case firmware that hides the vendor in headers but exposes it in the description XML. | ||
|
|
||
| The intent is efficiency, not correctness: on a busy LAN with dozens of non-Samsung UPnP devices (Sonos speakers, printers, Chromecasts) each SSDP hit currently rides through an `undici` fetch + XML parse before being rejected. The pre-filter drops these before we ever touch the network. | ||
|
|
||
| #### Scenario: Non-Samsung SSDP hit is dropped before description fetch | ||
|
|
||
| - **WHEN** an SSDP `M-SEARCH` response or `NOTIFY` arrives with headers containing no Samsung signature (e.g. `USN: uuid:5f9e…::urn:schemas-upnp-org:device:MediaRenderer:1`, `SERVER: Linux/… IpBridge/1.…`) | ||
| - **THEN** the discovery orchestrator does NOT enqueue a description fetch for that hit, no `undici.request` runs for its `LOCATION`, and a `debug`-level log line records the drop reason | ||
|
|
There was a problem hiding this comment.
🚀 Performance & Scalability | 🟡 Minor | ⚡ Quick win
Приберіть занадто широкий uuid:-критерій.
Гілка з UUID майже не відсікає шум: більшість SSDP USN починаються саме так, тож non-Samsung відповіді все одно підуть у fetch і зламають fast-path. Якщо мета — економити undici/XML, тут потрібен Samsung-специфічний discriminator.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@openspec/specs/upnp-tv-discovery/spec.md` around lines 92 - 100, The SSDP
pre-filter in the discovery orchestrator is too broad because the generic
`uuid:` check will match most non-Samsung devices and let them through to fetch.
Tighten the header inspection logic around the Samsung-specific discriminator
used before enqueueing description fetches so only `USN`, `SERVER`, or `ST`
values with an actual Samsung signature are accepted, and keep the `debug`-level
drop path for everything else. Update the behavior described in the SSDP
discovery scenario so the fast-path truly blocks non-Samsung hits before
`undici.request` is reached.
| case "${OSTYPE:-}" in | ||
| darwin*) die "unsupported host: this script requires systemd-based Linux (detected macOS via OSTYPE=$OSTYPE)" ;; | ||
| linux*|linux-gnu*) : ;; | ||
| *) die "unsupported host: this script requires systemd-based Linux (OSTYPE=${OSTYPE:-unknown})" ;; | ||
| esac |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value
Мертвий патерн у case: linux-gnu* ніколи не спрацює.
linux* вже покриває linux-gnu*, тож друга альтернатива в тому ж пункті недосяжна (shellcheck SC2221/SC2222). Функціонально нешкідливо (обидва варіанти роблять : ), але варто прибрати зайве.
🧹 Пропоноване спрощення
- linux*|linux-gnu*) : ;;
+ linux*) : ;;📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| case "${OSTYPE:-}" in | |
| darwin*) die "unsupported host: this script requires systemd-based Linux (detected macOS via OSTYPE=$OSTYPE)" ;; | |
| linux*|linux-gnu*) : ;; | |
| *) die "unsupported host: this script requires systemd-based Linux (OSTYPE=${OSTYPE:-unknown})" ;; | |
| esac | |
| case "${OSTYPE:-}" in | |
| darwin*) die "unsupported host: this script requires systemd-based Linux (detected macOS via OSTYPE=$OSTYPE)" ;; | |
| linux*) : ;; | |
| *) die "unsupported host: this script requires systemd-based Linux (OSTYPE=${OSTYPE:-unknown})" ;; | |
| esac |
🧰 Tools
🪛 Shellcheck (0.11.0)
[warning] 35-35: This pattern always overrides a later one on line 35.
(SC2221)
[warning] 35-35: This pattern never matches because of a previous pattern on line 35.
(SC2222)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@scripts/install.sh` around lines 33 - 37, The `case` in `scripts/install.sh`
has a dead pattern because `linux*` already matches `linux-gnu*`, making the
extra alternative unreachable. Simplify the `OSTYPE` match in the host check by
removing the redundant `linux-gnu*` branch and keep the existing no-op handling
in the `linux*`/Linux path.
Source: Linters/SAST tools
| install_prod_deps() { | ||
| log "installing production runtime deps (npm ci --omit=dev in back-end/)" | ||
| npm ci --omit=dev --prefix "$REPO_ROOT/back-end" | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '\n== File map ==\n'
git ls-files scripts/install.sh
printf '\n== Relevant sections of scripts/install.sh ==\n'
wc -l scripts/install.sh
sed -n '1,140p' scripts/install.sh
printf '\n--- help section ---\n'
sed -n '190,245p' scripts/install.sh
printf '\n--- search for run()/dry-run ---\n'
rg -n '^\s*(run|install_prod_deps|dry-run|mutating|non-mutating)\b|npm ci --omit=dev|verify_artifacts' scripts/install.shRepository: AntoniPetrenko/2026-fwdays-agentic-greenfield-task-AP
Length of output: 7273
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Inspect the file in smaller slices to avoid noise.
nl -ba scripts/install.sh | sed -n '1,130p'
printf '\n---\n'
nl -ba scripts/install.sh | sed -n '190,245p'
printf '\n--- grep ---\n'
grep -nE '^(run|install_prod_deps|dry-run|verify_artifacts)|npm ci --omit=dev|non-mutating|mutating' scripts/install.shRepository: AntoniPetrenko/2026-fwdays-agentic-greenfield-task-AP
Length of output: 231
scripts/install.sh:95-98, 216-224 — --dry-run має пропускати npm ci --omit=dev. Зараз install_prod_deps() виконує його напряму, тож preview все одно перевстановлює back-end/node_modules і прибирає devDependencies. Обгорни цей виклик у run() і прибери npm ci зі списку non-mutating у help/логах.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@scripts/install.sh` around lines 95 - 98, `install_prod_deps()` is still
running `npm ci --omit=dev` directly, so `--dry-run` does not stay non-mutating
and can change `back-end/node_modules`; wrap that install call in `run()` so it
is skipped in dry-run mode, and update the help/logging around the non-mutating
command list to remove `npm ci` from what preview mode implies. Use
`install_prod_deps`, `run`, and the install/help output in `scripts/install.sh`
to locate the change.
| # --- 4.7: --dry-run prints mutating commands without executing them ------ | ||
|
|
||
| @test "dry-run flag records mutating commands via run() without executing them" { | ||
| source_install | ||
| DRY_RUN=1 | ||
| run run useradd --system --shell /usr/sbin/nologin mytv | ||
| [ "$status" -eq 0 ] | ||
| [[ "$output" == *"[DRY-RUN]"* ]] | ||
| [[ "$output" == *"useradd"* ]] | ||
| # And confirm the same for a systemctl invocation. | ||
| run run systemctl daemon-reload | ||
| [ "$status" -eq 0 ] | ||
| [[ "$output" == *"[DRY-RUN]"* ]] | ||
| [[ "$output" == *"systemctl daemon-reload"* ]] | ||
| } | ||
|
|
There was a problem hiding this comment.
🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win
Варто додати тест на install_prod_deps під --dry-run.
Тест 4.7 перевіряє лише сам хелпер run(), але не факт, що install_prod_deps ним користується. Якщо застосуєте фікс із scripts/install.sh (обгортання npm ci через run), додайте тест, що при DRY_RUN=1 виклик install_prod_deps не виконує реальний npm ci (наприклад, підмінити npm шимом, який фейлить за спробою реального виконання).
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@scripts/install.test.bats` around lines 120 - 135, Add a dry-run test for
install_prod_deps, since the current test only covers the run() helper and not
the npm ci path. Update the tests in scripts/install.test.bats to invoke
install_prod_deps with DRY_RUN=1 and verify it goes through run() instead of
executing a real npm ci. Use the install_prod_deps function and the existing
DRY-RUN behavior to assert the command is logged, and consider stubbing npm with
a shim that fails if a real execution happens.
| case "${OSTYPE:-}" in | ||
| darwin*) die "unsupported host: this script requires systemd-based Linux (detected macOS via OSTYPE=$OSTYPE)" ;; | ||
| linux*|linux-gnu*) : ;; | ||
| *) die "unsupported host: this script requires systemd-based Linux (OSTYPE=${OSTYPE:-unknown})" ;; | ||
| esac |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value
Той самий мертвий linux-gnu* патерн, що і в install.sh.
🧰 Tools
🪛 Shellcheck (0.11.0)
[warning] 18-18: This pattern always overrides a later one on line 18.
(SC2221)
[warning] 18-18: This pattern never matches because of a previous pattern on line 18.
(SC2222)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@scripts/uninstall.sh` around lines 16 - 20, The OSTYPE check in uninstall.sh
still includes the redundant linux-gnu* pattern, which is the same
host-detection issue as in install.sh. Update the case statement in the host
validation logic to use a single Linux match that correctly covers Linux OSTYPE
values, and keep the die paths unchanged; use the existing OSTYPE-based switch
in the uninstall script to make the fix consistent with the corresponding
installer check.
Source: Linters/SAST tools
| app.post('/auth/login', async (request, reply) => { | ||
| const { email, password } = request.body; | ||
| const user = await validateCredentials(email, password); | ||
|
|
||
| if (!user) { | ||
| return reply.code(401).send({ error: 'Invalid credentials' }); | ||
| } | ||
|
|
||
| const accessToken = app.jwt.sign({ id: user.id, role: user.role }); | ||
| const refreshToken = randomBytes(32).toString('hex'); | ||
|
|
||
| refreshTokens.set(refreshToken, { | ||
| userId: user.id, | ||
| expires: Date.now() + 7 * 24 * 60 * 60 * 1000, // 7 days | ||
| }); | ||
|
|
||
| return { accessToken, refreshToken }; |
There was a problem hiding this comment.
🔒 Security & Privacy | 🔴 Critical | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Map the file structure first
ast-grep outline .agents/skills/fastify-best-practices/rules/authentication.md --view expanded || true
# Read the relevant sections with line numbers
sed -n '1,220p' .agents/skills/fastify-best-practices/rules/authentication.md | cat -n
# Find all token-related mentions in the file
rg -n -C 3 -i 'token|jwt|refresh|cookie|http-only|http only|bearer|accesstoken' .agents/skills/fastify-best-practices/rules/authentication.mdRepository: AntoniPetrenko/2026-fwdays-agentic-greenfield-task-AP
Length of output: 13182
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Search for repo-wide policy references around AccessToken / frontend exposure
rg -n -C 3 -i 'AccessToken|access token|bearer token|frontend|httpOnly|http-only|token.*frontend|frontend.*token' .
# Inspect nearby auth guidance files if present
fd -t f -i 'authentication.md|auth*.md|security*.md|jwt*.md' .
# Read any likely policy/docs files that mention token handling (first 250 lines max each)
for f in $(fd -t f -i 'authentication.md|auth*.md|security*.md|jwt*.md' .); do
echo "===== $f ====="
sed -n '1,250p' "$f" | cat -n
doneRepository: AntoniPetrenko/2026-fwdays-agentic-greenfield-task-AP
Length of output: 50410
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Find actual policy docs in the repository root and nearby
fd -a -t f 'AGENTS.md|agents.md|DESIGN.md|README.md|*.md' .
# Search for the exact "AccessToken never leaves the back-end" rule outside archived specs
rg -n -C 2 'AccessToken never leaves the back-end|never leaves the back-end|AccessToken' . \
--glob '!openspec/changes/archive/**'
# Inspect the auth skill file location and any surrounding guidance files in .agents
fd -a -t f .agentsRepository: AntoniPetrenko/2026-fwdays-agentic-greenfield-task-AP
Length of output: 555
🏁 Script executed:
#!/bin/bash
set -euo pipefail
file=".agents/skills/fastify-best-practices/rules/authentication.md"
# Show the sections around the cited lines
sed -n '85,115p' "$file" | cat -n
echo
sed -n '320,340p' "$file" | cat -n
echo
sed -n '470,490p' "$file" | cat -n
# Narrow search for token-returning examples in this file
rg -n -C 2 'return \{ token \}|return \{ accessToken, refreshToken \}|redirect\(/auth/success\?token=|httpOnly: true' "$file"Repository: AntoniPetrenko/2026-fwdays-agentic-greenfield-task-AP
Length of output: 3205
Не віддавайте AccessToken у клієнтські відповіді.
У 91-107 і 330-335 приклади повертають токен браузеру ({ accessToken, refreshToken } та token= в redirect URL). Для цього репо токен має лишатися server-side або йти в httpOnly cookie; тут краще показати session/cookie flow замість SPA-експозиції.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In @.agents/skills/fastify-best-practices/rules/authentication.md around lines
91 - 107, The login flow in the authentication examples is exposing access
tokens to the client, which should be avoided in this repo. Update the
`app.post('/auth/login')` handler to stop returning `accessToken` in the
response and instead keep it server-side or place it in an `httpOnly` cookie,
using the same session/cookie approach in the redirect flow that currently
appends `token=`. Refer to `validateCredentials`, `app.jwt.sign`, and the
login/redirect auth handlers when rewriting the examples so they demonstrate
cookie-based or session-based auth only.
| app.register(cors, { | ||
| origin: true, // Reflect request origin | ||
| credentials: true, | ||
| }); |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
git ls-files '.agents/skills/fastify-best-practices/rules/cors-security.md' \
'.agents/skills/fastify-best-practices/rules/*' \
| sed 's#^`#FILE` #'
echo '--- cors-security.md (excerpt) ---'
wc -l .agents/skills/fastify-best-practices/rules/cors-security.md
sed -n '1,220p' .agents/skills/fastify-best-practices/rules/cors-security.md
echo '--- search for cors / credentials / origin guidance ---'
rg -n "cors|credentials|origin: true|allowlist|dev-only|credentialed" .agents/skills/fastify-best-practices/rules -SRepository: AntoniPetrenko/2026-fwdays-agentic-greenfield-task-AP
Length of output: 9095
🏁 Script executed:
#!/bin/bash
set -euo pipefail
python3 - <<'PY'
from pathlib import Path
p = Path('.agents/skills/fastify-best-practices/rules/cors-security.md')
text = p.read_text(encoding='utf-8')
for needle in [
'origin: true',
'credentials: true',
'dev-only',
'allowlist',
'production',
'local',
]:
print(f'== {needle} ==')
for i, line in enumerate(text.splitlines(), 1):
if needle in line:
start = max(1, i-4)
end = min(len(text.splitlines()), i+4)
for j in range(start, end+1):
print(f'{j:4d}: {text.splitlines()[j-1]}')
print()
PYRepository: AntoniPetrenko/2026-fwdays-agentic-greenfield-task-AP
Length of output: 2521
Не показуйте origin: true разом із credentials: true як загальний приклад.
Це відкриває credentialed-запити з будь-якого походження; для такого гіда краще показати явний allowlist або явно позначити кейс як виняток для довірених, auth-free роутів.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In @.agents/skills/fastify-best-practices/rules/cors-security.md around lines 75
- 78, The CORS example using cors should not present origin: true together with
credentials: true as a general pattern. Update the example to use an explicit
allowlist in the app.register(cors, ...) configuration, or clearly mark the
current setup as a narrow exception for trusted auth-free routes. Keep the
guidance tied to the cors security rule so readers understand when to use origin
and credentials safely.
| app.post('/transfer', async (request) => { | ||
| const { fromId, toId, amount } = request.body; | ||
| const client = await app.pg.connect(); | ||
|
|
||
| try { | ||
| await client.query('BEGIN'); | ||
| await client.query( | ||
| 'UPDATE accounts SET balance = balance - $1 WHERE id = $2', | ||
| [amount, fromId], | ||
| ); | ||
| await client.query( | ||
| 'UPDATE accounts SET balance = balance + $1 WHERE id = $2', | ||
| [amount, toId], | ||
| ); | ||
| await client.query('COMMIT'); | ||
| return { success: true }; |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🔴 Critical | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
file=".agents/skills/fastify-best-practices/rules/database.md"
# Show the relevant ranges with line numbers
sed -n '1,120p' "$file" | nl -ba | sed -n '40,80p'
echo "----"
sed -n '220,270p' "$file" | nl -ba | sed -n '220,270p'Repository: AntoniPetrenko/2026-fwdays-agentic-greenfield-task-AP
Length of output: 231
🏁 Script executed:
#!/bin/bash
set -euo pipefail
file=".agents/skills/fastify-best-practices/rules/database.md"
# Map structure first, then inspect surrounding examples more precisely if needed.
ast-grep outline "$file" --view expanded || trueRepository: AntoniPetrenko/2026-fwdays-agentic-greenfield-task-AP
Length of output: 261
🏁 Script executed:
#!/bin/bash
set -euo pipefail
file=".agents/skills/fastify-best-practices/rules/database.md"
# Search for transaction and dynamic-update examples to inspect precise behavior.
rg -n "transfer|UPDATE accounts|rowCount|COMMIT|ROLLBACK|identifier|dynamic" "$file"Repository: AntoniPetrenko/2026-fwdays-agentic-greenfield-task-AP
Length of output: 573
🏁 Script executed:
#!/bin/bash
set -euo pipefail
file=".agents/skills/fastify-best-practices/rules/database.md"
echo "== lines around 48-63 =="
sed -n '48,63p' "$file" | cat -n
echo
echo "== lines around 234-245 =="
sed -n '234,245p' "$file" | cat -n
echo
echo "== quick grep for transfer / dynamic SQL markers =="
rg -n "transfer|rowCount|ROLLBACK|COMMIT|identifier|sql" "$file"Repository: AntoniPetrenko/2026-fwdays-agentic-greenfield-task-AP
Length of output: 1866
🏁 Script executed:
#!/bin/bash
set -euo pipefail
file=".agents/skills/fastify-best-practices/rules/database.md"
sed -n '60,70p' "$file" | cat -nRepository: AntoniPetrenko/2026-fwdays-agentic-greenfield-task-AP
Length of output: 478
Перевіряйте rowCount після кожного UPDATE перед COMMIT. Якщо будь-який запит не зачепив рівно 1 рядок, кидайте помилку, щоб catch зробив ROLLBACK; інакше переказ може зафіксуватися з порушеним інваріантом балансу.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In @.agents/skills/fastify-best-practices/rules/database.md around lines 48 -
63, The /transfer handler in app.post currently commits the transaction without
verifying that both UPDATE statements affected exactly one row. After each
client.query UPDATE inside the try block, check the returned rowCount and throw
an error if it is not 1 so the catch path can perform ROLLBACK; keep the fix
localized to the transfer flow and use the existing client transaction logic.
| async update(id: string, data: Partial<User>): Promise<User | null> { | ||
| const fields = Object.keys(data); | ||
| const values = Object.values(data); | ||
| const setClause = fields | ||
| .map((f, i) => `${f} = $${i + 2}`) | ||
| .join(', '); | ||
|
|
||
| const { rows } = await app.pg.query( | ||
| `UPDATE users SET ${setClause} WHERE id = $1 RETURNING *`, | ||
| [id, ...values], | ||
| ); | ||
| return rows[0] || null; |
There was a problem hiding this comment.
🔒 Security & Privacy | 🔴 Critical | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Map the target file and inspect the relevant range.
git ls-files .agents/skills/fastify-best-practices/rules/database.md
wc -l .agents/skills/fastify-best-practices/rules/database.md
sed -n '220,260p' .agents/skills/fastify-best-practices/rules/database.mdRepository: AntoniPetrenko/2026-fwdays-agentic-greenfield-task-AP
Length of output: 1427
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# 1) Inspect nearby context in the same file.
sed -n '200,255p' .agents/skills/fastify-best-practices/rules/database.md
# 2) Use a tiny read-only probe to show the emitted SQL for a safe, malicious, and empty payload.
python3 - <<'PY'
def build_query(data):
fields = list(data.keys())
values = list(data.values())
set_clause = ', '.join(f"{f} = ${i + 2}" for i, f in enumerate(fields))
query = f"UPDATE users SET {set_clause} WHERE id = $1 RETURNING *"
return query, [None, *values]
cases = [
{"name": "Alice"},
{"name": "Alice", "email = 'x' --": "ignored"},
{},
]
for case in cases:
q, params = build_query(case)
print("CASE:", case)
print("QUERY:", q)
print("PARAMS:", params)
print("---")
PYRepository: AntoniPetrenko/2026-fwdays-agentic-greenfield-task-AP
Length of output: 2049
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Check whether there are any allowlist/validation helpers nearby in the repository.
rg -n "allowlist|whitelist|validate.*field|Object\.keys\(data\)|UPDATE users SET" .agents -SRepository: AntoniPetrenko/2026-fwdays-agentic-greenfield-task-AP
Length of output: 418
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Inspect the exact docs section to see whether this is guidance text or production code.
ast-grep outline .agents/skills/fastify-best-practices/rules/database.md || trueRepository: AntoniPetrenko/2026-fwdays-agentic-greenfield-task-AP
Length of output: 261
Не збирай SET із довільних ключів — Object.keys(data) тут напряму стає SQL-ідентифікаторами без allowlist, тож можливі ін’єкція через назву поля й поламаний UPDATE ... SET на порожньому data. Залиш явний список дозволених колонок і відхиляй empty update.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In @.agents/skills/fastify-best-practices/rules/database.md around lines 234 -
245, The update logic in the update(id, data) method builds the SQL SET clause
directly from Object.keys(data), which allows arbitrary field names and can
break on empty updates. Replace this with an explicit allowlist of permitted
user columns before constructing the SET clause, and reject or short-circuit
when data has no updatable fields. Keep the fix localized to update() and its
SQL assembly around setClause and app.pg.query.
| const userService = { | ||
| findById: (id: string) => fastify.db.query('SELECT * FROM users WHERE id = $1', [id]), | ||
| create: (data: CreateUserInput) => fastify.db.query( | ||
| 'INSERT INTO users (name, email) VALUES ($1, $2) RETURNING *', | ||
| [data.name, data.email] | ||
| ), | ||
| }; | ||
|
|
||
| fastify.decorate('userService', userService); | ||
| }, { | ||
| dependencies: ['database-plugin'], | ||
| }); | ||
|
|
||
| // Use in routes | ||
| app.get('/users/:id', async function (request) { | ||
| const user = await this.userService.findById(request.params.id); | ||
| return user; | ||
| }); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
file=".agents/skills/fastify-best-practices/rules/decorators.md"
echo "== file info =="
wc -l "$file"
echo
echo "== relevant section (lines 140-185) =="
sed -n '140,185p' "$file" | cat -n
echo
echo "== search for related decorator/type guidance in repository =="
rg -n --hidden -S "FastifyInstance|decorate\('userService'|userService|QueryResult|rows\[0\]|database-plugin" .agents . 2>/dev/null || trueRepository: AntoniPetrenko/2026-fwdays-agentic-greenfield-task-AP
Length of output: 11849
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== decorators.md around FastifyInstance augmentation =="
sed -n '50,85p' .agents/skills/fastify-best-practices/rules/decorators.md | cat -n
echo
echo "== decorators.md around the userService example =="
sed -n '145,175p' .agents/skills/fastify-best-practices/rules/decorators.md | cat -n
echo
echo "== decorators.md around later FastifyInstance augmentation =="
sed -n '355,375p' .agents/skills/fastify-best-practices/rules/decorators.md | cat -n
echo
echo "== database.md around repository return values =="
sed -n '35,55p' .agents/skills/fastify-best-practices/rules/database.md | cat -n
echo
echo "== database.md around createUserRepository examples =="
sed -n '200,250p' .agents/skills/fastify-best-practices/rules/database.md | cat -nRepository: AntoniPetrenko/2026-fwdays-agentic-greenfield-task-AP
Length of output: 5654
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== search for DatabaseClient definition and query return type =="
rg -n --hidden -S "interface DatabaseClient|type DatabaseClient|class DatabaseClient|query\\(" .agents/skills/fastify-best-practices/rules/database.md .agents/skills/fastify-best-practices/rules/decorators.md | sed -n '1,200p'
echo
echo "== surrounding context in database.md where DatabaseClient is introduced =="
sed -n '1,90p' .agents/skills/fastify-best-practices/rules/database.md | cat -nRepository: AntoniPetrenko/2026-fwdays-agentic-greenfield-task-AP
Length of output: 5266
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== decorators.md around db decorator definition =="
sed -n '120,155p' .agents/skills/fastify-best-practices/rules/decorators.md | cat -n
echo
echo "== decorators.md around the userService example =="
sed -n '145,172p' .agents/skills/fastify-best-practices/rules/decorators.md | cat -n
echo
echo "== any db type/contract mentions in decorators.md =="
rg -n --hidden -S "db:|decorate\\('db'|interface .*db|DatabaseClient|query: async|query\\(" .agents/skills/fastify-best-practices/rules/decorators.md | sed -n '1,200p'Repository: AntoniPetrenko/2026-fwdays-agentic-greenfield-task-AP
Length of output: 2894
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== decorators.md around the Fastify type augmentation =="
sed -n '60,75p' .agents/skills/fastify-best-practices/rules/decorators.md | cat -n
echo
echo "== all DatabaseClient mentions in the fastify guide rules =="
rg -n --hidden -S "DatabaseClient|createDatabaseConnection|interface FastifyInstance|declare module 'fastify'" .agents/skills/fastify-best-practices/rules | sed -n '1,220p'
echo
echo "== decorators.md around asyncService example near line 403 =="
sed -n '395,415p' .agents/skills/fastify-best-practices/rules/decorators.md | cat -nRepository: AntoniPetrenko/2026-fwdays-agentic-greenfield-task-AP
Length of output: 2997
Поверніть rows[0] із сервісу. Зараз findById() і create() віддають сирий результат запиту, тож приклад маршруту повертає обгортку драйвера замість користувача.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In @.agents/skills/fastify-best-practices/rules/decorators.md around lines 154 -
171, The user service methods are returning the raw database query result
instead of the actual user object, so update the `userService` in
`fastify.decorate` to unwrap `rows[0]` from both `findById()` and `create()`.
Keep the route usage in `app.get('/users/:id', ...)` aligned with that change so
`this.userService.findById(...)` returns the user record directly rather than
the driver wrapper.
| ```bash | ||
| mkdir -p "<planningHome.changesDir>/archive" | ||
| ``` | ||
|
|
||
| Generate target name using current date: `YYYY-MM-DD-<change-name>` | ||
|
|
||
| **Check if target already exists:** | ||
| - If yes: Fail with error, suggest renaming existing archive or using different date | ||
| - If no: Move `changeRoot` to the archive directory | ||
|
|
||
| ```bash | ||
| mv "<changeRoot>" "<planningHome.changesDir>/archive/YYYY-MM-DD-<name>" | ||
| ``` |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Додайте мови до shell- та output-fence-ів.
У прикладах архівації один shell-блок і підсумковий output-блок лишаються без мовного тегу, тому markdownlint не пройде. Позначте перший як bash, другий як text.
🛠️ Можлива правка
-```
+```bash-```
+```textAlso applies to: 99-108
🧰 Tools
🪛 markdownlint-cli2 (0.22.1)
[warning] 74-74: Fenced code blocks should be surrounded by blank lines
(MD031, blanks-around-fences)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In @.claude/skills/openspec-archive-change/SKILL.md around lines 74 - 86, In the
archive-change examples within SKILL.md, one shell fence and the final output
fence are missing language tags, which will fail markdownlint. Update the
relevant fenced code blocks in the archive instructions so the command block
uses bash and the summary/output block uses text, keeping the surrounding
archive flow and symbols like changeRoot and planningHome.changesDir unchanged.
Source: Linters/SAST tools
| ``` | ||
| ┌─────────────────────────────────────────┐ | ||
| │ Use ASCII diagrams liberally │ | ||
| ├─────────────────────────────────────────┤ | ||
| │ │ | ||
| │ ┌────────┐ ┌────────┐ │ | ||
| │ │ State │────────▶│ State │ │ | ||
| │ │ A │ │ B │ │ | ||
| │ └────────┘ └────────┘ │ | ||
| │ │ | ||
| │ System diagrams, state machines, │ | ||
| │ data flows, architecture sketches, │ | ||
| │ dependency graphs, comparison tables │ | ||
| │ │ | ||
| └─────────────────────────────────────────┘ | ||
| ``` |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Проставте мови для всіх fenced-прикладів у цій довідці.
Тут змішані ASCII-діаграми, shell-команди й діалоги, але кілька fences досі без мови. Уніфікуйте їх як text або bash, і markdownlint перестане сипати MD040/MD031.
🛠️ Можлива правка
-```
+```text-```
+```bashAlso applies to: 87-89, 150-170, 173-201, 204-218, 221-247, 262-274
🧰 Tools
🪛 markdownlint-cli2 (0.22.1)
[warning] 56-56: Fenced code blocks should be surrounded by blank lines
(MD031, blanks-around-fences)
[warning] 56-56: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In @.claude/skills/openspec-explore/SKILL.md around lines 56 - 71, Several
fenced examples in SKILL.md are missing language tags, causing markdownlint
MD040/MD031 warnings; update the affected fences to use the appropriate
identifiers consistently (for example, use text for ASCII diagrams and dialogue
examples, and bash for shell commands) so all fenced blocks in the open-spec
guide have explicit languages. Use the existing fenced sections in the
documentation as the targets and keep the content unchanged while only adding
the correct fence labels.
Source: Linters/SAST tools
| **Store selection:** If the user names a store (a store is a standalone OpenSpec repo registered on this machine) or the work lives in one, run `openspec store list --json` to discover registered store ids, then pass `--store <id>` on the commands that read or write specs and changes (`new change`, `status`, `instructions`, `list`, `show`, `validate`, `archive`, `doctor`, `context`). Other commands do not take the flag. Hints printed by commands already carry the flag; keep it on follow-ups. Without a store, commands act on the nearest local `openspec/` root. | ||
|
|
||
| **Input**: The user's request should include a change name (kebab-case) OR a description of what they want to build. | ||
|
|
||
| **Steps** | ||
|
|
||
| 1. **If no clear input provided, ask what they want to build** | ||
|
|
||
| Use the **AskUserQuestion tool** (open-ended, no preset options) to ask: | ||
| > "What change do you want to work on? Describe what you want to build or fix." | ||
|
|
||
| From their description, derive a kebab-case name (e.g., "add user authentication" → `add-user-auth`). | ||
|
|
||
| **IMPORTANT**: Do NOT proceed without understanding what the user wants to build. | ||
|
|
||
| 2. **Create the change directory** | ||
| ```bash | ||
| openspec new change "<name>" | ||
| ``` | ||
| This creates a scaffolded change in the planning home resolved by the CLI with `.openspec.yaml`. | ||
|
|
||
| 3. **Get the artifact build order** | ||
| ```bash | ||
| openspec status --change "<name>" --json | ||
| ``` | ||
| Parse the JSON to get: | ||
| - `applyRequires`: array of artifact IDs needed before implementation (e.g., `["tasks"]`) | ||
| - `artifacts`: list of all artifacts with their status and dependencies | ||
| - `planningHome`, `changeRoot`, `artifactPaths`, and `actionContext`: path and scope context. Use these instead of assuming repo-local paths. | ||
|
|
||
| 4. **Create artifacts in sequence until apply-ready** | ||
|
|
||
| Use the **TodoWrite tool** to track progress through the artifacts. | ||
|
|
||
| Loop through artifacts in dependency order (artifacts with no pending dependencies first): | ||
|
|
||
| a. **For each artifact that is `ready` (dependencies satisfied)**: | ||
| - Get instructions: | ||
| ```bash | ||
| openspec instructions <artifact-id> --change "<name>" --json | ||
| ``` | ||
| - The instructions JSON includes: | ||
| - `context`: Project background (constraints for you - do NOT include in output) | ||
| - `rules`: Artifact-specific rules (constraints for you - do NOT include in output) | ||
| - `template`: The structure to use for your output file | ||
| - `instruction`: Schema-specific guidance for this artifact type | ||
| - `resolvedOutputPath`: Resolved path or pattern to write the artifact | ||
| - `dependencies`: Completed artifacts to read for context | ||
| - Read any completed dependency files for context | ||
| - Create the artifact file using `template` as the structure and write it to `resolvedOutputPath` | ||
| - Apply `context` and `rules` as constraints - but do NOT copy them into the file | ||
| - Show brief progress: "Created <artifact-id>" | ||
|
|
||
| b. **Continue until all `applyRequires` artifacts are complete** | ||
| - After creating each artifact, re-run `openspec status --change "<name>" --json` | ||
| - Check if every artifact ID in `applyRequires` has `status: "done"` in the artifacts array | ||
| - Stop when all `applyRequires` artifacts are done | ||
|
|
||
| c. **If an artifact requires user input** (unclear context): | ||
| - Use **AskUserQuestion tool** to clarify | ||
| - Then continue with creation | ||
|
|
||
| 5. **Show final status** | ||
| ```bash | ||
| openspec status --change "<name>" |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Додай --store до прикладів команд.
Після блоку Store selection кроки 2–5 все ще викликають openspec ... без флага, тож у multi-store середовищі вони підуть у nearest local root і можуть змінити не той репозиторій.
Пропонований фікс
- openspec new change "<name>"
+ openspec new change "<name>" --store "<id>"
- openspec status --change "<name>" --json
+ openspec status --change "<name>" --json --store "<id>"🧰 Tools
🪛 LanguageTool
[style] ~23-~23: It’s more common nowadays to write this noun as one word.
Context: ...apply --- Store selection: If the user names a store (a store is a standalone OpenSp...
(RECOMMENDED_COMPOUNDS)
[style] ~29-~29: You have already used this phrasing in nearby sentences. Consider replacing it to add variety to your writing.
Context: ... no clear input provided, ask what they want to build** Use the **AskUserQuestion t...
(REP_WANT_TO_VB)
[style] ~32-~32: You have already used this phrasing in nearby sentences. Consider replacing it to add variety to your writing.
Context: ... you want to work on? Describe what you want to build or fix." From their descripti...
(REP_WANT_TO_VB)
[style] ~36-~36: You have already used this phrasing in nearby sentences. Consider replacing it to add variety to your writing.
Context: ...eed without understanding what the user wants to build. 2. **Create the change director...
(REP_WANT_TO_VB)
🪛 markdownlint-cli2 (0.22.1)
[warning] 39-39: Fenced code blocks should be surrounded by blank lines
(MD031, blanks-around-fences)
[warning] 41-41: Fenced code blocks should be surrounded by blank lines
(MD031, blanks-around-fences)
[warning] 45-45: Fenced code blocks should be surrounded by blank lines
(MD031, blanks-around-fences)
[warning] 47-47: Fenced code blocks should be surrounded by blank lines
(MD031, blanks-around-fences)
[warning] 61-61: Fenced code blocks should be surrounded by blank lines
(MD031, blanks-around-fences)
[warning] 63-63: Fenced code blocks should be surrounded by blank lines
(MD031, blanks-around-fences)
[warning] 86-86: Fenced code blocks should be surrounded by blank lines
(MD031, blanks-around-fences)
🪛 SkillSpector (2.3.7)
[error] 81: [MP3] Memory Manipulation: Skill manipulates agent memory, state, or stored context. Memory corruption can alter personality, override safety rules, or cause unpredictable behavior.
Remediation: Protect agent memory and state from modification by untrusted content. Use read-only memory for critical instructions and validate all state changes.
(Memory Poisoning (MP3))
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In @.claude/skills/openspec-propose/SKILL.md around lines 23 - 87, The command
examples in the Steps section still omit the store flag after the Store
selection guidance, so update the `openspec new/status/instructions` examples in
`SKILL.md` to consistently include `--store <id>` wherever the store-selection
rule applies. Keep the existing nearest-local-root behavior only for the
no-store case, and make sure the change is reflected in the step-by-step
commands by referencing the same `openspec` commands already listed.
| **Store selection:** If the user names a store (a store is a standalone OpenSpec repo registered on this machine) or the work lives in one, run `openspec store list --json` to discover registered store ids, then pass `--store <id>` on the commands that read or write specs and changes (`new change`, `status`, `instructions`, `list`, `show`, `validate`, `archive`, `doctor`, `context`). Other commands do not take the flag. Hints printed by commands already carry the flag; keep it on follow-ups. Without a store, commands act on the nearest local `openspec/` root. | ||
|
|
||
| **Input**: Optionally specify a change name. If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes. | ||
|
|
||
| **Steps** | ||
|
|
||
| 1. **If no change name provided, prompt for selection** | ||
|
|
||
| Run `openspec list --json` to get available changes. Use the **AskUserQuestion tool** to let the user select. | ||
|
|
||
| Show changes that have delta specs (under `specs/` directory). | ||
|
|
||
| **IMPORTANT**: Do NOT guess or auto-select a change. Always let the user choose. | ||
|
|
||
| 2. **Resolve change context** | ||
|
|
||
| Run: | ||
| ```bash | ||
| openspec status --change "<name>" --json | ||
| ``` |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Ті самі приклади теж мають нести --store.
openspec list --json і openspec status --change ... без флага знову підуть у nearest local root, а не в обраний store. Для цього workflow це легко дає зміну не того набору spec-файлів.
Пропонований фікс
- openspec list --json
+ openspec list --json --store "<id>"
- openspec status --change "<name>" --json
+ openspec status --change "<name>" --json --store "<id>"📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| **Store selection:** If the user names a store (a store is a standalone OpenSpec repo registered on this machine) or the work lives in one, run `openspec store list --json` to discover registered store ids, then pass `--store <id>` on the commands that read or write specs and changes (`new change`, `status`, `instructions`, `list`, `show`, `validate`, `archive`, `doctor`, `context`). Other commands do not take the flag. Hints printed by commands already carry the flag; keep it on follow-ups. Without a store, commands act on the nearest local `openspec/` root. | |
| **Input**: Optionally specify a change name. If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes. | |
| **Steps** | |
| 1. **If no change name provided, prompt for selection** | |
| Run `openspec list --json` to get available changes. Use the **AskUserQuestion tool** to let the user select. | |
| Show changes that have delta specs (under `specs/` directory). | |
| **IMPORTANT**: Do NOT guess or auto-select a change. Always let the user choose. | |
| 2. **Resolve change context** | |
| Run: | |
| ```bash | |
| openspec status --change "<name>" --json | |
| ``` | |
| **Store selection:** If the user names a store (a store is a standalone OpenSpec repo registered on this machine) or the work lives in one, run `openspec store list --json` to discover registered store ids, then pass `--store <id>` on the commands that read or write specs and changes (`new change`, `status`, `instructions`, `list`, `show`, `validate`, `archive`, `doctor`, `context`). Other commands do not take the flag. Hints printed by commands already carry the flag; keep it on follow-ups. Without a store, commands act on the nearest local `openspec/` root. | |
| **Input**: Optionally specify a change name. If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes. | |
| **Steps** | |
| 1. **If no change name provided, prompt for selection** | |
| Run `openspec list --json --store "<id>"` to get available changes. Use the **AskUserQuestion tool** to let the user select. | |
| Show changes that have delta specs (under `specs/` directory). | |
| **IMPORTANT**: Do NOT guess or auto-select a change. Always let the user choose. | |
| 2. **Resolve change context** | |
| Run: |
🧰 Tools
🪛 LanguageTool
[style] ~16-~16: It’s more common nowadays to write this noun as one word.
Context: ...uirement). Store selection: If the user names a store (a store is a standalone OpenSp...
(RECOMMENDED_COMPOUNDS)
🪛 markdownlint-cli2 (0.22.1)
[warning] 33-33: Fenced code blocks should be surrounded by blank lines
(MD031, blanks-around-fences)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In @.claude/skills/openspec-sync-specs/SKILL.md around lines 16 - 35, The
openspec sync workflow is missing --store on the discovery and status steps, so
it can target the nearest local root instead of the selected store. Update the
instructions in SKILL.md so the `openspec list --json` and `openspec status
--change "<name>" --json` steps also use the same store id resolved earlier, and
keep the flag consistent with the store-selection guidance for commands like
`list` and `status`.
| ``` | ||
| Co-Authored-By: Claude <noreply@anthropic.com> | ||
| ``` |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Підправте fenced-блок у шаблоні коміту.
Тут бракує мови fence, і markdownlint ще може сваритися на відступи навколо блоку. Для такого прикладу краще явно ставити text.
Пропонований diff
- ```
+ ```text
Co-Authored-By: Claude <noreply@anthropic.com>
- ```
+ ```📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| ``` | |
| Co-Authored-By: Claude <noreply@anthropic.com> | |
| ``` |
🧰 Tools
🪛 markdownlint-cli2 (0.22.1)
[warning] 60-60: Fenced code blocks should be surrounded by blank lines
(MD031, blanks-around-fences)
[warning] 60-60: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
[warning] 62-62: Fenced code blocks should be surrounded by blank lines
(MD031, blanks-around-fences)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@LOOP.md` around lines 60 - 62, The fenced block in the commit template needs
an explicit language tag and cleaner surrounding markdown spacing. Update the
fenced section in LOOP.md around the Co-Authored-By example to use the same
block but with a text fence, and ensure the indentation/blank lines around the
block match markdownlint expectations so the template passes lint.
Source: Linters/SAST tools
Автор
Anton Petrenko
Проєкт
mytv — самохостований пульт для Samsung TV, який працює на локальній
мережі: бек-енд на Fastify 5 + TypeScript автоматично знаходить телевізори
через UPnP, оголошує себе як
mytv.localчерез mDNS і керує ТВ черезSamsung Smart View WebSocket API (порт 8001); фронт-енд — React 19 + Vite
SPA (двоекранний: список пристроїв + пульт), обслуговується самим бек-ендом
з того самого origin. Розгортається на Orange Pi одним скриптом. Без хмари,
без авторизації, без інтернету на runtime.
Відео-демо (1–2 хв)
Video: https://youtu.be/zzvwq1cn6cA
Які практики Agentic Engineering застосовано
Контекст-інженерія (статичний vs динамічний):
AGENTS.mdу корені репо — house rules, які завантажуються на початкукожної сесії: security-правила (пейрінг-токен ніколи не покидає бек-енд),
правила фронт-енду (тільки Orbit DS через
@ds/, ніяких hard-codedпалітр), single-origin у проді, cross-cutting rules по кожному пакету.
CLAUDE.md,DESIGN.md,docs/product-brief.md,docs/requirements.md,docs/capabilities.md,LOOP.md— разомскладають статичний контекст, який агент читає перед будь-якою
роботою.
docs/current-state.md— session log, кудиагент префіксує запис на початку кожної нової сесії (правило в
AGENTS.md→ "Session log"). Він транспортує стан між незалежнимисесіями агента.
frontend-design-checkтільки при редагуванні
front-end/**,samsung-ip-control-protocolтільки для TV-I/O коду,
orbit-tv-remote-designтільки коли требагенерувати UI. Це тримає контекстне вікно чистим.
Loop engineering (цикли замість покрокового промптингу):
LOOP.mdописує чіткий per-cycle алгоритм: select change → openspecstatus → openspec instructions apply → implement → verify → review →
archive → auto-commit → stop and wait for human. Один слеш-виклик
/next-change= один повний цикл, не більше./next-change(моя власна slash-команда,
.claude/commands/next-change.md). Цеhuman-in-the-loop checkpoint, який зберігає такий послідовний потік
на кожній capability:
openspec-propose(або сам пишу) і створююproposal.md+design.md+spec.md+tasks.mdдля наступноїcapability — чітко фіксую why, what, how і критерії
приймання.
передумати про scope без коштовних rollback-ів.
/next-change <capability-name>, і далі loop ужевиконує алгоритм з
LOOP.mdавтоматично: імплементує таски поtasks.md, крутить build+test gate, спавнить свіжийcode-reviewerсуб-агент, на
CHANGES_REQUESTEDповертається до fix + re-verify +re-review, і зупиняється тільки коли всі критерії з
LOOP.mdвиконані (green build, green tests,
PASSreview, archive +synced specs, clean commit).
чи запускати наступну capability.
/next-change= один повний cycle. Автоматичнонаступну capability воно не бере; це дає мені точку контролю між
ітераціями.
того, щоб я вручну керував кожним кроком — але з тим, що я свідомо
контролюю куди йде наступний cycle. C11 і C12 обидва ловилися
reviewer-ом на першій ітерації і виправлялися в межах того ж
/next-changeвиклику без мого втручання.Maker ≠ checker / суб-агенти:
code-reviewerсуб-агент зі свіжим контекстом; йому передається тільки назва
зміни — жодного резюме моєї роботи, щоб він не був anchored до
моїх раціоналізацій. Reviewer read-only (тільки
Read/Grep/Glob/Bash), тому review і fix — окремі кроки.PASSз першої спроби.CHANGES_REQUESTED(unit виставлявXDG_CONFIG_HOME, щодесинхронізувало реальний шлях
tokens.jsonз тим, що казалиdesign/spec/README; плюс невідома CSS-зміна їхала разом зі змінами).
Виправлено → повторний review →
PASS.CHANGES_REQUESTED(build.sh при tsc/vite failure не називавочікуваний артефакт; dry-run banner не згадував split). Виправлено
→ повторний review →
PASS.Верифікація:
npm run back:build(tsc),npm run front:build(Vite),npm run back:test(node--test, 107 тестів),npm run front:test(vitest, 44 тести). Всі мають бути зелені перед архівацією.
AGENTS.md: task marked[x]без passing test run — цене done. Якщо в
tasks.mdє "add test", тест повинен існувати іпройти.
bash scripts/build.shна dev-машині, ручний деплойна реальний Orange Pi (Armbian), де на живому діагностували
203/EXEC→200/CHDIR→ masked service →network-online.targetдедлок. Кожна проблема породжувала конкретне рішення (setcap, world-
traversable home, unmask + enable).
batsне був встановлений, всіassertions програвалися вручну в plain bash проти sourced-функцій
— і це чесно позначено в
tasks.md.Специфікації наперед (SDD):
proposal.md(why +what) →
design.md(how, decisions з альтернативами і відхиленимиваріантами, non-goals, risks, open questions) →
specs/**/spec.md(requirements з
SHALL+#### Scenario:з WHEN/THEN) →tasks.md(чекбокси, які тікаються по ходу impl).
## MODIFIED Requirementsі## ADDED Requirements(використано дляC11 при доставці C12).
FR-*,NFR-*,BC-*) уdocs/requirements.mdтрасуються з кожного proposal — жодна імплементація без свого FR.
Інструменти та MCP:
openspec-propose,openspec-apply-change,openspec-archive-change,openspec-sync-specs.frontend-design-check,orbit-tv-remote-design,samsung-ip-control-protocol,next-change,code-review,verify.code-reviewer(fresh-context review),Explore(read-only search),general-purpose.Vite, vitest, node
--test, systemd, bats-core.Що вирішував я, а що — агент:
Orange Pi), архітектурні межі (single-origin у проді, no-cloud,
no-auth), поділ на capabilities C1–C12 і їх залежності, дизайн-система
(neumorphism, Orbit DS), non-goals ("no
.debpackage", "no cross-archnative modules"), точку "де закінчується моє ручне рев'ю і починається
автономний loop" — тобто момент виклику
/next-changeпісля того, як яперевірив proposal/design/spec/tasks вручну, live-decisions при
деплої на Pi ("split build з install", "окремий user на Pi",
"npm ci --omit=dev а не bundled node_modules").
декомпозицію на tasks у межах змін, вибір шаблонів помилок, назви
функцій, структуру bats-тестів, формулювання spec-сценаріїв, точну
послідовність кроків у скриптах, тексти session-log записів. Плюс —
всередині одного
/next-changeциклу сам вирішував, коли фікс наCHANGES_REQUESTEDдостатній, щоб пройти повторне рев'ю. Деколипіднімав питання назад через
AskUserQuestion(наприклад, проruntime-deps на Pi: npm ci vs. bundled node_modules) — це моменти,
де він свідомо повертав контроль мені всередині циклу.
(Опційно) Посилання на код
https://github.com/AntoniPetrenko/2026-fwdays-agentic-greenfield-task-AP(бранч
anton-petrenko-solution)Чекліст
Summary by CodeRabbit