Files
ssmithxandClaude Sonnet 5 a22a9c9826
Build and validate / build (push) Successful in 1m38s
Initial scaffold: ArchyHCL, a community hardware compatibility list for Archipelago
Modeled on three researched precedents (see README): OpenWrt's Table of
Hardware for the browsable sortable/filterable table UX, postmarketOS's
working/community/testing tiers for the status field (collapsed to
working/partial/broken here), and RaspiBlitz's scattered GitHub-issues
approach as a negative example to avoid — hence structured YAML report
files validated against a JSON Schema instead of free-text issue threads.

- data/reports/*.yml + data/schema.json: one file per report, schema
  requires `issues` whenever status is partial/broken
- scripts/build.py: validates every report and builds site/data.json,
  fails loudly on bad data (same idea as archy's own
  validate-app-manifest.sh)
- site/: plain HTML/CSS/JS, no framework or build step, fetches data.json
  client-side — search, filter by status/form-factor, sortable columns,
  click a row for issues/notes detail
- .github/ISSUE_TEMPLATE/hardware-report.yml: structured submission path
  for contributors who don't want to touch git directly
- .gitea/workflows/ci.yml: runs the build/validate step on push and PRs

Not yet deployed anywhere — see README's Deployment section.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-01 11:32:58 +00:00

131 lines
4.0 KiB
JavaScript

// No framework, no build step — this is a plain JSON file (site/data.json,
// generated by scripts/build.py) rendered client-side. Keeps the "add a
// device" contribution path as simple as adding one YAML file.
let reports = [];
let sortKey = "device_model";
let sortDir = 1;
const rowsEl = document.getElementById("rows");
const searchEl = document.getElementById("search");
const statusEl = document.getElementById("status-filter");
const formFactorEl = document.getElementById("form-factor-filter");
const countEl = document.getElementById("count");
const emptyEl = document.getElementById("empty");
function storageLabel(storage) {
return `${storage.size_gb}GB ${storage.type.toUpperCase()}`;
}
function matchesFilters(r, query, status, formFactor) {
if (status && r.status !== status) return false;
if (formFactor && r.form_factor !== formFactor) return false;
if (!query) return true;
const haystack = [r.device_model, r.cpu, r.wifi_chip, r.ethernet]
.filter(Boolean)
.join(" ")
.toLowerCase();
return haystack.includes(query);
}
function sortValue(r, key) {
if (key === "storage") return r.storage.size_gb;
return r[key];
}
function render() {
const query = searchEl.value.trim().toLowerCase();
const status = statusEl.value;
const formFactor = formFactorEl.value;
const filtered = reports.filter((r) => matchesFilters(r, query, status, formFactor));
filtered.sort((a, b) => {
const av = sortValue(a, sortKey);
const bv = sortValue(b, sortKey);
if (av < bv) return -1 * sortDir;
if (av > bv) return 1 * sortDir;
return 0;
});
rowsEl.innerHTML = "";
for (const r of filtered) {
const tr = document.createElement("tr");
tr.className = "expandable";
tr.innerHTML = `
<td>${escapeHtml(r.device_model)}</td>
<td>${escapeHtml(r.form_factor)}</td>
<td>${escapeHtml(r.cpu)}</td>
<td>${r.ram_gb}GB</td>
<td>${storageLabel(r.storage)}</td>
<td>${escapeHtml(r.wifi_chip)}</td>
<td><span class="badge ${r.status}">${r.status}</span></td>
<td>${escapeHtml(r.archy_version)}</td>
<td>${escapeHtml(r.tested_date)}</td>
`;
tr.addEventListener("click", () => toggleDetail(tr, r));
rowsEl.appendChild(tr);
}
countEl.textContent = `${filtered.length} of ${reports.length} report${reports.length === 1 ? "" : "s"}`;
emptyEl.hidden = filtered.length !== 0;
}
function toggleDetail(tr, r) {
const next = tr.nextElementSibling;
if (next && next.classList.contains("detail-row")) {
next.remove();
return;
}
document.querySelectorAll(".detail-row").forEach((el) => el.remove());
const detail = document.createElement("tr");
detail.className = "detail-row";
const parts = [];
if (r.ethernet) parts.push(`<strong>Ethernet:</strong> ${escapeHtml(r.ethernet)}`);
parts.push(`<strong>Install method:</strong> ${escapeHtml(r.install_method)}`);
if (r.submitted_by) parts.push(`<strong>Submitted by:</strong> ${escapeHtml(r.submitted_by)}`);
if (r.issues) parts.push(`<strong>Issues:</strong>\n${escapeHtml(r.issues.trim())}`);
if (r.notes) parts.push(`<strong>Notes:</strong>\n${escapeHtml(r.notes.trim())}`);
const td = document.createElement("td");
td.colSpan = 9;
td.innerHTML = parts.join("\n\n");
detail.appendChild(td);
tr.after(detail);
}
function escapeHtml(s) {
const div = document.createElement("div");
div.textContent = s ?? "";
return div.innerHTML;
}
document.querySelectorAll("th[data-sort]").forEach((th) => {
th.addEventListener("click", () => {
const key = th.dataset.sort;
if (sortKey === key) {
sortDir *= -1;
} else {
sortKey = key;
sortDir = 1;
}
render();
});
});
searchEl.addEventListener("input", render);
statusEl.addEventListener("change", render);
formFactorEl.addEventListener("change", render);
fetch("data.json")
.then((res) => res.json())
.then((data) => {
reports = data;
render();
})
.catch((err) => {
emptyEl.hidden = false;
emptyEl.textContent = "Failed to load data.json — " + err;
});