Release Device Tools v0.1.0

This commit is contained in:
2026-09-01 14:22:42 +02:00
commit 06f697e180
57 changed files with 9775 additions and 0 deletions
+27
View File
@@ -0,0 +1,27 @@
import { Component, type ErrorInfo, type ReactNode } from "react";
export class ErrorBoundary extends Component<
{ children: ReactNode },
{ error?: Error }
> {
state: { error?: Error } = {};
static getDerivedStateFromError(error: Error) {
return { error };
}
componentDidCatch(error: Error, info: ErrorInfo) {
console.error("Application failure", error, info);
}
render() {
if (this.state.error)
return (
<main className="fatal">
<h1>Device Tools could not continue</h1>
<p>{this.state.error.message}</p>
<button type="button" onClick={() => location.reload()}>
Reload
</button>
</main>
);
return this.props.children;
}
}
+51
View File
@@ -0,0 +1,51 @@
import { useEffect, useRef } from "react";
export function HelpDialog({
open,
onClose,
}: {
open: boolean;
onClose: () => void;
}) {
const dialog = useRef<HTMLDialogElement>(null);
useEffect(() => {
const node = dialog.current;
if (!node) return;
if (open && !node.open) node.showModal();
if (!open && node.open) node.close();
}, [open]);
return (
<dialog
ref={dialog}
className="help-dialog"
onClose={onClose}
onCancel={onClose}
aria-labelledby="help-title"
>
<div className="dialog-heading">
<div>
<p className="eyebrow">Local-first help</p>
<h2 id="help-title">About Device Tools</h2>
</div>
<button type="button" onClick={onClose} aria-label="Close help">
×
</button>
</div>
<p>
The passive inventory checks browser API presence, viewport and input
hints, declared codec support and media constraints without requesting
permissions or enumerating hardware.
</p>
<p>
Optional probes run only from their individual buttons. Camera,
microphone, screen and persistence checks can prompt; returned media
tracks are stopped immediately and no content is recorded.
</p>
<p>
Reports contain no timestamp, user agent, locale, identifier, hash or
uniqueness score. Exact high-entropy measurements are bucketed before
JSON or CSV export. Nothing is uploaded.
</p>
</dialog>
);
}
+289
View File
@@ -0,0 +1,289 @@
import { useMemo, useState } from "react";
import { triggerBlobDownload } from "@add-ideas/toolbox-helpers";
import {
collectPassiveCapabilities,
createRedactedReport,
exportReportCsv,
exportReportJson,
PROBES,
runProbe,
type Capability,
type ProbeId,
type ProbeResult,
} from "../core/device";
export function Workbench() {
const [capabilities, setCapabilities] = useState<Capability[]>(() =>
collectPassiveCapabilities(),
);
const [results, setResults] = useState<Partial<Record<ProbeId, ProbeResult>>>(
{},
);
const [running, setRunning] = useState<ProbeId | null>(null);
const [filter, setFilter] = useState("");
const [status, setStatus] = useState(
"Passive API presence was checked locally. No permission was requested.",
);
const filtered = useMemo(() => {
const needle = filter.trim().toLowerCase();
return needle === ""
? capabilities
: capabilities.filter((item) =>
(item.label + " " + item.group + " " + item.value)
.toLowerCase()
.includes(needle),
);
}, [capabilities, filter]);
const groups = useMemo(
() =>
[...new Set(filtered.map((item) => item.group))].map((group) => ({
group,
items: filtered.filter((item) => item.group === group),
})),
[filtered],
);
const completed = PROBES.flatMap((probe) => {
const result = results[probe.id];
return result ? [result] : [];
});
const report = createRedactedReport(capabilities, completed);
const available = capabilities.filter(
(item) => item.state === "available",
).length;
async function performProbe(id: ProbeId): Promise<void> {
setRunning(id);
setStatus("Running the selected probe locally…");
const result = await runProbe(id);
setResults((current) => ({ ...current, [id]: result }));
setRunning(null);
setStatus(result.summary);
}
return (
<main className="workbench">
<section className="hero">
<div>
<p className="eyebrow">Browser capability and privacy lab</p>
<h1>Inspect capabilities, not identity.</h1>
<p>
See what this browser exposes, run sensitive probes only when you
choose, and export a deliberately redacted report without a device
fingerprint or score.
</p>
</div>
<span className="privacy-pill">No exfiltration</span>
</section>
<section className="principles" aria-labelledby="principles-title">
<div>
<p className="eyebrow">Privacy contract</p>
<h2 id="principles-title">Useful evidence with a hard boundary</h2>
</div>
<ul>
<li>
No user agent, locale, device name, stable identifier or hash.
</li>
<li>No canvas/audio fingerprint sample and no uniqueness score.</li>
<li>
No API request leaves this origin; media tracks stop immediately.
</li>
<li>Downloaded reports bucket exact high-entropy measurements.</li>
</ul>
</section>
<section className="panel" aria-labelledby="inventory-title">
<div className="panel-heading">
<div>
<p className="eyebrow">Passive inventory</p>
<h2 id="inventory-title">Browser and display capabilities</h2>
</div>
<span className="count-pill">
{available} of {capabilities.length} available
</span>
</div>
<p className="muted">
These checks read API presence, media queries, viewport values,
supported constraints and codec declarations. They do not request
permission or enumerate hardware.
</p>
<div className="inventory-toolbar">
<label>
Filter capabilities
<input
type="search"
value={filter}
onChange={(event) => setFilter(event.target.value)}
placeholder="display, media, storage…"
/>
</label>
<button
type="button"
onClick={() => {
setCapabilities(collectPassiveCapabilities());
setStatus(
"Passive inventory refreshed; no permission was requested.",
);
}}
>
Refresh passive inventory
</button>
</div>
<div className="capability-groups">
{groups.map(({ group, items }) => (
<section key={group} className="capability-group">
<h3>{group}</h3>
<div className="capability-table" tabIndex={0}>
<table>
<thead>
<tr>
<th>Capability</th>
<th>State</th>
<th>Local value</th>
<th>Privacy</th>
</tr>
</thead>
<tbody>
{items.map((item) => (
<tr key={item.id} title={item.explanation}>
<th scope="row">{item.label}</th>
<td>
<State state={item.state} />
</td>
<td>{item.value}</td>
<td>
<Risk value={item.privacy} />
</td>
</tr>
))}
</tbody>
</table>
</div>
</section>
))}
{groups.length === 0 && (
<p className="empty-state">No capability matches this filter.</p>
)}
</div>
</section>
<section className="panel" aria-labelledby="probes-title">
<div className="panel-heading">
<div>
<p className="eyebrow">Explicit user-gated checks</p>
<h2 id="probes-title">Optional probes</h2>
</div>
<span className="count-pill">{completed.length} run</span>
</div>
<p className="muted">
Nothing below runs automatically. Buttons marked may prompt can
trigger browser or operating-system UI.
</p>
<div className="probe-grid">
{PROBES.map((probe) => {
const result = results[probe.id];
return (
<article className="probe-card" key={probe.id}>
<div className="probe-heading">
<h3>{probe.title}</h3>
<Risk value={probe.privacy} />
</div>
<p>{probe.description}</p>
{probe.prompts && (
<strong className="prompt-note">May prompt</strong>
)}
<button
type="button"
disabled={running !== null}
onClick={() => void performProbe(probe.id)}
>
{running === probe.id ? "Running…" : probe.action}
</button>
{result && <ProbeOutcome result={result} />}
</article>
);
})}
</div>
<p className="status" role="status" aria-live="polite">
{status}
</p>
</section>
<section className="panel report-panel" aria-labelledby="report-title">
<div className="panel-heading">
<div>
<p className="eyebrow">Shareable output</p>
<h2 id="report-title">Redacted capability report</h2>
</div>
<span className="count-pill">No timestamp or identifier</span>
</div>
<p className="muted">
The preview is the exact downloaded payload. Screen/viewport/DPR,
storage, GPU limits, device counts, battery timing and network quality
are reduced to broad buckets.
</p>
<pre tabIndex={0} aria-label="Redacted JSON report">
{exportReportJson(report)}
</pre>
<div className="button-row">
<button
type="button"
className="primary-button"
onClick={() =>
download(
new Blob([exportReportJson(report)], {
type: "application/json",
}),
"device-capabilities-redacted.json",
)
}
>
Download redacted JSON
</button>
<button
type="button"
onClick={() =>
download(
new Blob([exportReportCsv(report)], { type: "text/csv" }),
"device-capabilities-redacted.csv",
)
}
>
Download redacted CSV
</button>
</div>
</section>
</main>
);
}
function ProbeOutcome({ result }: { result: ProbeResult }) {
return (
<div className={"probe-result " + result.state}>
<strong>{result.state}</strong>
<p>{result.summary}</p>
{result.details.length > 0 && (
<dl>
{result.details.map((detail) => (
<div key={detail.label}>
<dt>{detail.label}</dt>
<dd>{detail.value}</dd>
</div>
))}
</dl>
)}
</div>
);
}
function State({ state }: { state: Capability["state"] }) {
return <span className={"state-chip " + state}>{state}</span>;
}
function Risk({ value }: { value: Capability["privacy"] }) {
return <span className={"risk-chip " + value}>{value}</span>;
}
function download(blob: Blob, name: string): void {
triggerBlobDownload(blob, name);
}