Initial scaffold: ArchyHCL, a community hardware compatibility list for Archipelago
Build and validate / build (push) Successful in 1m38s
Build and validate / build (push) Successful in 1m38s
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>
This commit is contained in:
+130
@@ -0,0 +1,130 @@
|
||||
// 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;
|
||||
});
|
||||
Reference in New Issue
Block a user