feat: ship HTTP dashboard and harden daemon/API flows

Add the static HTTP dashboard example and wire in the recent daemon/API polish:
CORS-aware API routing, service-install behavior cleanup, safer systemd unit
ExecStart quoting, and friendly-name validation for path-safe targeting.

Also refresh README/API/roadmap docs, remove the temporary claude observations
file, and include the related tests for API/status and daemon validation.
This commit is contained in:
44r0n7
2026-04-18 16:45:12 -04:00
parent 795aa2f713
commit 000d97fdeb
16 changed files with 998 additions and 726 deletions
+135
View File
@@ -0,0 +1,135 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>tvctl HTTP API Dashboard</title>
<link rel="stylesheet" href="./styles.css">
</head>
<body>
<header>
<h1>tvctl HTTP API Dashboard</h1>
<p>Local manual test dashboard for every <code>/v1</code> endpoint.</p>
</header>
<main>
<section class="panel">
<h2>Connection</h2>
<label>
API Base URL
<input id="baseUrl" value="http://127.0.0.1:7272/v1" />
</label>
<div class="row">
<button id="pingBtn" type="button">GET /daemon/status</button>
<button id="listDevicesBtn" type="button">GET /devices</button>
<button id="discoverBtn" type="button">POST /devices/discover</button>
</div>
<p class="hint">Tip: if browser requests fail, enable CORS in tvctl config first.</p>
</section>
<section class="panel">
<h2>Device Target</h2>
<label>
Known Devices
<select id="deviceSelect">
<option value="">(none loaded)</option>
</select>
</label>
<label>
Manual device UUID or name override
<input id="targetOverride" placeholder="optional" />
</label>
<p class="hint">Device endpoint actions use override when set, otherwise selected device.</p>
</section>
<section class="panel">
<h2>Device Endpoints</h2>
<div class="row">
<button id="getDeviceBtn" type="button">GET /devices/{id}</button>
<button id="deleteDeviceBtn" class="danger" type="button">DELETE /devices/{id}</button>
<button id="getStateBtn" type="button">GET /devices/{id}/state</button>
</div>
</section>
<section class="panel">
<h2>Apps Endpoints</h2>
<label>
Launch app name or id
<input id="appLaunchValue" placeholder="jellyfin" />
</label>
<label>
Refresh clear cache first
<input id="refreshClear" type="checkbox" />
</label>
<div class="row">
<button id="listAppsBtn" type="button">GET /devices/{id}/apps</button>
<button id="launchAppBtn" class="warn" type="button">POST /devices/{id}/apps/launch</button>
<button id="stopAppBtn" class="warn" type="button">POST /devices/{id}/apps/stop</button>
<button id="refreshAppsBtn" type="button">POST /devices/{id}/apps/refresh</button>
</div>
</section>
<section class="panel">
<h2>Remote Endpoints</h2>
<label>
Single key
<input id="singleKey" placeholder="home" />
</label>
<label>
Sequence keys (comma separated)
<input id="sequenceKeys" placeholder="home,down,select" />
</label>
<label>
Sequence delay_ms
<input id="sequenceDelay" type="number" value="200" min="0" />
</label>
<div class="row">
<button id="sendKeyBtn" type="button">POST /devices/{id}/remote/key</button>
<button id="sendSequenceBtn" type="button">POST /devices/{id}/remote/sequence</button>
</div>
</section>
<section class="panel">
<h2>Dev Endpoints</h2>
<label>
Zip file for sideload
<input id="devZip" type="file" accept=".zip,application/zip" />
</label>
<div class="row">
<button id="devInstallBtn" class="warn" type="button">POST /devices/{id}/dev/install</button>
<button id="devReloadBtn" type="button">POST /devices/{id}/dev/reload</button>
<button id="devLogsBtn" type="button">GET /devices/{id}/dev/logs</button>
</div>
</section>
<section class="panel">
<h2>Config Endpoints</h2>
<label>
Config patch key
<input id="configKey" placeholder="daemon.http_port" />
</label>
<label>
Config patch value (JSON literal, e.g. 7272, true, "text")
<input id="configValue" placeholder="7272" />
</label>
<div class="row">
<button id="getConfigBtn" type="button">GET /config</button>
<button id="patchConfigBtn" class="danger" type="button">PATCH /config</button>
<button id="reloadConfigBtn" class="warn" type="button">POST /config/reload</button>
</div>
</section>
<section class="panel panel-wide">
<h2>Request Log</h2>
<textarea id="requestLog" readonly></textarea>
</section>
<section class="panel panel-wide">
<h2>Response</h2>
<textarea id="responseView" readonly></textarea>
</section>
</main>
<script src="./app.js"></script>
</body>
</html>