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;
|
||||
});
|
||||
@@ -0,0 +1,41 @@
|
||||
[
|
||||
{
|
||||
"device_model": "Lenovo ThinkPad T430",
|
||||
"form_factor": "laptop",
|
||||
"cpu": "Intel Core i5-3320M",
|
||||
"ram_gb": 8,
|
||||
"storage": {
|
||||
"type": "ssd",
|
||||
"size_gb": 256
|
||||
},
|
||||
"wifi_chip": "Intel Centrino Advanced-N 6205",
|
||||
"ethernet": "Intel 82579LM",
|
||||
"archy_version": "1.8.4-alpha",
|
||||
"install_method": "usb-iso",
|
||||
"status": "working",
|
||||
"tested_date": "2026-08-15",
|
||||
"submitted_by": "example-contributor",
|
||||
"notes": "Example report \u2014 replace with a real one. Boots and installs cleanly from\na USB stick made with the standard ISO. No BIOS changes needed beyond\nenabling AHCI mode (should already be default on most T430s).\n",
|
||||
"_source_file": "example-thinkpad-t430.yml"
|
||||
},
|
||||
{
|
||||
"device_model": "Raspberry Pi 5 (8GB)",
|
||||
"form_factor": "sbc",
|
||||
"cpu": "Broadcom BCM2712 (4x Cortex-A76)",
|
||||
"ram_gb": 8,
|
||||
"storage": {
|
||||
"type": "nvme",
|
||||
"size_gb": 512
|
||||
},
|
||||
"wifi_chip": "Broadcom BCM43455 (onboard)",
|
||||
"ethernet": "Broadcom onboard Gigabit",
|
||||
"archy_version": "1.8.4-alpha",
|
||||
"install_method": "netboot",
|
||||
"status": "partial",
|
||||
"tested_date": "2026-08-10",
|
||||
"submitted_by": "example-contributor",
|
||||
"issues": "Example report \u2014 replace with a real one. Onboard WiFi drops the\nconnection under sustained heavy P2P traffic (Bitcoin IBD); a workaround\nis to run over Ethernet instead. Everything else (Lightning, apps,\ndashboard) works fine.\n",
|
||||
"notes": "NVMe via the official PCIe HAT. Boots noticeably faster than SD-card\ninstalls.\n",
|
||||
"_source_file": "example-raspberry-pi-5.yml"
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,66 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>ArchyHCL — Archipelago Hardware Compatibility List</title>
|
||||
<link rel="stylesheet" href="style.css">
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<h1>ArchyHCL</h1>
|
||||
<p>Community-reported hardware compatibility for <a href="https://source.archipelago-foundation.org/lfg2025/archy" target="_blank" rel="noopener">Archipelago</a>. Every row here is a real install someone tested and reported — see it on GitHub/Gitea as a plain data file, no login required to browse.</p>
|
||||
<p class="cta">
|
||||
Tested Archipelago on your own hardware? <a href="https://source.archipelago-foundation.org/lfg2025/ArchyHCL/issues/new?template=hardware-report.yml" target="_blank" rel="noopener">Report it</a> —
|
||||
takes two minutes and helps the next person know what to expect before they buy or repurpose a machine.
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<div class="controls">
|
||||
<input id="search" type="search" placeholder="Search model, CPU, WiFi chip…" autocomplete="off">
|
||||
<select id="status-filter">
|
||||
<option value="">All statuses</option>
|
||||
<option value="working">Working</option>
|
||||
<option value="partial">Partial</option>
|
||||
<option value="broken">Broken</option>
|
||||
</select>
|
||||
<select id="form-factor-filter">
|
||||
<option value="">All form factors</option>
|
||||
<option value="laptop">Laptop</option>
|
||||
<option value="desktop">Desktop</option>
|
||||
<option value="mini-pc">Mini PC</option>
|
||||
<option value="sbc">SBC</option>
|
||||
<option value="server">Server</option>
|
||||
<option value="other">Other</option>
|
||||
</select>
|
||||
<span id="count" class="count"></span>
|
||||
</div>
|
||||
|
||||
<table id="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th data-sort="device_model">Device</th>
|
||||
<th data-sort="form_factor">Type</th>
|
||||
<th data-sort="cpu">CPU</th>
|
||||
<th data-sort="ram_gb">RAM</th>
|
||||
<th data-sort="storage">Storage</th>
|
||||
<th data-sort="wifi_chip">WiFi chip</th>
|
||||
<th data-sort="status">Status</th>
|
||||
<th data-sort="archy_version">Archy ver.</th>
|
||||
<th data-sort="tested_date">Tested</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="rows"></tbody>
|
||||
</table>
|
||||
|
||||
<p id="empty" class="empty" hidden>No reports match your filters.</p>
|
||||
</main>
|
||||
|
||||
<footer>
|
||||
<p>Data lives in <code>data/reports/*.yml</code>, one file per report — browse or fork the raw data <a href="https://source.archipelago-foundation.org/lfg2025/ArchyHCL/src/branch/main/data/reports" target="_blank" rel="noopener">here</a>. See <a href="https://source.archipelago-foundation.org/lfg2025/ArchyHCL/src/branch/main/CONTRIBUTING.md" target="_blank" rel="noopener">CONTRIBUTING.md</a> for the two ways to add a report.</p>
|
||||
</footer>
|
||||
|
||||
<script src="app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
+140
@@ -0,0 +1,140 @@
|
||||
:root {
|
||||
--bg: #0a0e12;
|
||||
--panel: #10161c;
|
||||
--border: #223038;
|
||||
--text: #d8e4e8;
|
||||
--muted: #7f97a0;
|
||||
--accent: #00fff2;
|
||||
--accent-dim: #00b3a8;
|
||||
--working: #33d17a;
|
||||
--partial: #f5b23c;
|
||||
--broken: #f0506e;
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
font-family: ui-monospace, "SF Mono", Consolas, monospace;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
header, main, footer {
|
||||
max-width: 1100px;
|
||||
margin: 0 auto;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: var(--accent);
|
||||
letter-spacing: 0.08em;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
a { color: var(--accent-dim); }
|
||||
a:hover { color: var(--accent); }
|
||||
|
||||
header p { color: var(--muted); max-width: 70ch; }
|
||||
header .cta { color: var(--text); }
|
||||
|
||||
.controls {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
align-items: center;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
#search {
|
||||
flex: 1 1 260px;
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
input, select {
|
||||
background: var(--panel);
|
||||
border: 1px solid var(--border);
|
||||
color: var(--text);
|
||||
padding: 0.5rem 0.7rem;
|
||||
border-radius: 4px;
|
||||
font-family: inherit;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
input:focus, select:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent-dim);
|
||||
}
|
||||
|
||||
.count { color: var(--muted); font-size: 0.85rem; margin-left: auto; }
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
thead th {
|
||||
text-align: left;
|
||||
padding: 0.6rem 0.7rem;
|
||||
border-bottom: 2px solid var(--border);
|
||||
color: var(--muted);
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
thead th:hover { color: var(--accent); }
|
||||
|
||||
tbody tr {
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
tbody tr:hover { background: var(--panel); }
|
||||
|
||||
tbody tr.expandable { cursor: pointer; }
|
||||
|
||||
td {
|
||||
padding: 0.6rem 0.7rem;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.badge {
|
||||
display: inline-block;
|
||||
padding: 0.15rem 0.5rem;
|
||||
border-radius: 3px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.03em;
|
||||
}
|
||||
|
||||
.badge.working { background: rgba(51, 209, 122, 0.15); color: var(--working); }
|
||||
.badge.partial { background: rgba(245, 178, 60, 0.15); color: var(--partial); }
|
||||
.badge.broken { background: rgba(240, 80, 110, 0.15); color: var(--broken); }
|
||||
|
||||
.detail-row td {
|
||||
background: var(--panel);
|
||||
color: var(--muted);
|
||||
white-space: pre-wrap;
|
||||
font-size: 0.82rem;
|
||||
padding: 0.8rem 1.2rem;
|
||||
}
|
||||
|
||||
.detail-row strong { color: var(--text); }
|
||||
|
||||
.empty { color: var(--muted); text-align: center; padding: 2rem; }
|
||||
|
||||
footer {
|
||||
color: var(--muted);
|
||||
font-size: 0.8rem;
|
||||
border-top: 1px solid var(--border);
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
footer code {
|
||||
background: var(--panel);
|
||||
padding: 0.1rem 0.35rem;
|
||||
border-radius: 3px;
|
||||
}
|
||||
Reference in New Issue
Block a user