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:
@@ -0,0 +1,27 @@
|
|||||||
|
name: Build and validate
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 5
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 1
|
||||||
|
|
||||||
|
- name: Set up Python
|
||||||
|
uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: "3.12"
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: pip install pyyaml jsonschema
|
||||||
|
|
||||||
|
- name: Build and validate every report
|
||||||
|
run: python3 scripts/build.py
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
name: Hardware report
|
||||||
|
description: Report a device you've installed Archipelago on, working or not.
|
||||||
|
title: "hw: <device model>"
|
||||||
|
labels: ["hardware-report"]
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
Thanks for reporting! A maintainer turns this into a data file under
|
||||||
|
`data/reports/` and it shows up on the site. If you're comfortable
|
||||||
|
with git, opening a PR directly is faster — see CONTRIBUTING.md.
|
||||||
|
- type: input
|
||||||
|
id: device_model
|
||||||
|
attributes:
|
||||||
|
label: Device model
|
||||||
|
placeholder: "Lenovo ThinkPad T430"
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: dropdown
|
||||||
|
id: form_factor
|
||||||
|
attributes:
|
||||||
|
label: Form factor
|
||||||
|
options:
|
||||||
|
- laptop
|
||||||
|
- desktop
|
||||||
|
- mini-pc
|
||||||
|
- sbc
|
||||||
|
- server
|
||||||
|
- other
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: input
|
||||||
|
id: cpu
|
||||||
|
attributes:
|
||||||
|
label: CPU
|
||||||
|
placeholder: "Intel Core i5-3320M"
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: input
|
||||||
|
id: ram_gb
|
||||||
|
attributes:
|
||||||
|
label: RAM (GB)
|
||||||
|
placeholder: "8"
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: input
|
||||||
|
id: storage
|
||||||
|
attributes:
|
||||||
|
label: Storage (type and size)
|
||||||
|
placeholder: "256GB SSD"
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: input
|
||||||
|
id: wifi_chip
|
||||||
|
attributes:
|
||||||
|
label: WiFi chip
|
||||||
|
description: Exact chipset if you know it. Put "none" if wired-only.
|
||||||
|
placeholder: "Intel Centrino Advanced-N 6205"
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: input
|
||||||
|
id: ethernet
|
||||||
|
attributes:
|
||||||
|
label: Ethernet chip (optional)
|
||||||
|
placeholder: "Intel 82579LM"
|
||||||
|
- type: input
|
||||||
|
id: archy_version
|
||||||
|
attributes:
|
||||||
|
label: Archipelago version tested
|
||||||
|
placeholder: "1.8.4-alpha"
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: dropdown
|
||||||
|
id: install_method
|
||||||
|
attributes:
|
||||||
|
label: Install method
|
||||||
|
options:
|
||||||
|
- usb-iso
|
||||||
|
- netboot
|
||||||
|
- existing-os-script
|
||||||
|
- other
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: dropdown
|
||||||
|
id: status
|
||||||
|
attributes:
|
||||||
|
label: Status
|
||||||
|
description: "working = no known issues. partial = runs with specific known issues (describe below). broken = does not install or run."
|
||||||
|
options:
|
||||||
|
- working
|
||||||
|
- partial
|
||||||
|
- broken
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: input
|
||||||
|
id: tested_date
|
||||||
|
attributes:
|
||||||
|
label: Date tested
|
||||||
|
placeholder: "2026-08-21"
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
id: issues
|
||||||
|
attributes:
|
||||||
|
label: Issues (required if status is partial or broken)
|
||||||
|
description: What broke, and any workaround you found.
|
||||||
|
- type: textarea
|
||||||
|
id: notes
|
||||||
|
attributes:
|
||||||
|
label: Other notes
|
||||||
|
description: BIOS/UEFI settings needed, quirks, links to a fuller writeup.
|
||||||
|
- type: input
|
||||||
|
id: submitted_by
|
||||||
|
attributes:
|
||||||
|
label: Attribution (optional)
|
||||||
|
description: Your npub, Gitea username, or leave blank to stay anonymous.
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
.DS_Store
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
# Contributing to ArchyHCL
|
||||||
|
|
||||||
|
Two ways to add a report, pick whichever's easier for you.
|
||||||
|
|
||||||
|
## Option A — open an issue (no git needed)
|
||||||
|
|
||||||
|
[Open a hardware report issue](https://source.archipelago-foundation.org/lfg2025/ArchyHCL/issues/new?template=hardware-report.yml)
|
||||||
|
and fill in the form. A maintainer will turn it into a data file and it'll
|
||||||
|
show up on the site.
|
||||||
|
|
||||||
|
## Option B — open a PR directly
|
||||||
|
|
||||||
|
1. Copy `data/reports/example-thinkpad-t430.yml` to a new file named
|
||||||
|
`<device-slug>-<short-id>.yml` (e.g. `thinkpad-t430-a1b2.yml` — the
|
||||||
|
suffix just needs to make the filename unique if the same model's been
|
||||||
|
reported before).
|
||||||
|
2. Fill in your report. Every field is described in
|
||||||
|
[`data/schema.json`](data/schema.json); `issues` is required if your
|
||||||
|
`status` is `partial` or `broken`.
|
||||||
|
3. Validate locally before opening the PR:
|
||||||
|
```bash
|
||||||
|
pip install pyyaml jsonschema # if you don't have them
|
||||||
|
python3 scripts/build.py
|
||||||
|
```
|
||||||
|
This fails loudly (and tells you exactly which field) if anything's
|
||||||
|
wrong — same idea as [archy](https://source.archipelago-foundation.org/lfg2025/archy)'s
|
||||||
|
own `scripts/validate-app-manifest.sh`.
|
||||||
|
4. Open the PR. Once merged, `scripts/build.py` regenerates `site/data.json`
|
||||||
|
and the site picks it up.
|
||||||
|
|
||||||
|
## Updating an existing report
|
||||||
|
|
||||||
|
Devices change over time (firmware updates fix WiFi issues, etc.) — if
|
||||||
|
you're re-testing a device that's already listed, open a PR editing the
|
||||||
|
existing file rather than adding a duplicate. Keep the old `tested_date`
|
||||||
|
context in mind: bump it to your test date so readers know how fresh the
|
||||||
|
report is.
|
||||||
|
|
||||||
|
## What makes a good report
|
||||||
|
|
||||||
|
- Be exact about the WiFi chip if you can (`iwconfig`/`lspci` on Linux,
|
||||||
|
Device Manager on Windows if you dual-booted to check). "Realtek" alone
|
||||||
|
isn't as useful as "Realtek RTL8821CE" — chip-specific driver issues are
|
||||||
|
the single most common thing this list exists to surface.
|
||||||
|
- If `status` is `partial` or `broken`, describe *what* broke and *how you
|
||||||
|
noticed* (crash on boot? WiFi drops under load? specific app won't
|
||||||
|
start?) — "doesn't work" isn't actionable for the next person.
|
||||||
|
- If you found a workaround, put it in `notes` even if the underlying issue
|
||||||
|
isn't fixed. A working-with-a-workaround report is more useful than no
|
||||||
|
report at all.
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2026 the Archipelago Project contributors
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
# ArchyHCL
|
||||||
|
|
||||||
|
A community hardware compatibility list for [Archipelago](https://source.archipelago-foundation.org/lfg2025/archy) —
|
||||||
|
what physical devices people have actually installed it on, what worked,
|
||||||
|
what didn't, and why. Model, CPU, RAM, storage, and WiFi chip for every
|
||||||
|
report, since WiFi chipsets are consistently the thing most likely to bite
|
||||||
|
someone doing an install on repurposed hardware.
|
||||||
|
|
||||||
|
**[Browse the list →](https://hcl.archipelago-foundation.org)** (once deployed —
|
||||||
|
see [Deployment](#deployment) below)
|
||||||
|
|
||||||
|
## Why this exists
|
||||||
|
|
||||||
|
Archipelago runs on whatever hardware people have lying around — old
|
||||||
|
laptops, mini PCs, SBCs — and that's a much wider, messier compatibility
|
||||||
|
surface than a project that only targets its own reference hardware. Before
|
||||||
|
building this, I looked at how a few existing communities solve the same
|
||||||
|
problem:
|
||||||
|
|
||||||
|
- **[OpenWrt's Table of Hardware](https://toh.openwrt.org/)** — a
|
||||||
|
searchable table of routers, generated nightly from structured wiki
|
||||||
|
pages into a `toh.json` file the site reads. Good model for the
|
||||||
|
browsing UX (sortable/filterable table of CPU/RAM/flash/WiFi chip), but
|
||||||
|
the underlying wiki pages are free-text and only loosely structured,
|
||||||
|
which the maintainers themselves have called out as a maintenance
|
||||||
|
problem over the years.
|
||||||
|
- **[postmarketOS's device list](https://wiki.postmarketos.org/wiki/Devices)** —
|
||||||
|
tiers devices into `main` / `community` / `testing` support levels, each
|
||||||
|
with a per-device page describing exactly what works and what doesn't.
|
||||||
|
Good model for the *status* field: a single working/broken flag loses
|
||||||
|
the "boots fine but WiFi is flaky" middle ground that's actually the
|
||||||
|
most common and most useful case to document.
|
||||||
|
- **[RaspiBlitz](https://github.com/raspiblitz/raspiblitz)** (closest
|
||||||
|
project to Archipelago in spirit — a Bitcoin/Lightning node OS for
|
||||||
|
repurposed hardware) — hardware feedback mostly lives scattered across
|
||||||
|
GitHub issues with no consistent fields, which makes it hard to search
|
||||||
|
or compare reports. Useful as a negative example: this is what we're
|
||||||
|
trying to avoid by using structured data files instead of free-form
|
||||||
|
issue threads.
|
||||||
|
|
||||||
|
**The model this repo uses**: one small YAML file per report
|
||||||
|
(`data/reports/*.yml`), validated against a JSON Schema, built into a
|
||||||
|
single `data.json` a plain static site reads — OpenWrt's browsable-table UX,
|
||||||
|
postmarketOS's tiered status field, and RaspiBlitz's actual subject matter
|
||||||
|
(node-OS-on-repurposed-hardware), on top of git-native, PR-reviewable data
|
||||||
|
instead of a wiki or a scattered issue tracker.
|
||||||
|
|
||||||
|
## Repo layout
|
||||||
|
|
||||||
|
```
|
||||||
|
data/
|
||||||
|
schema.json — the report schema (source of truth for what fields exist)
|
||||||
|
reports/*.yml — one file per hardware report
|
||||||
|
scripts/
|
||||||
|
build.py — validates every report and builds site/data.json
|
||||||
|
site/
|
||||||
|
index.html/.css/.js — the static site (no framework, no build step — just fetches data.json)
|
||||||
|
.gitea/issue_template/hardware-report.yml — structured issue form for reporting without git
|
||||||
|
```
|
||||||
|
|
||||||
|
## Contributing a report
|
||||||
|
|
||||||
|
See [CONTRIBUTING.md](CONTRIBUTING.md) — either open an issue with the
|
||||||
|
structured form, or add a YAML file directly and open a PR.
|
||||||
|
|
||||||
|
## Building the site locally
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install pyyaml jsonschema
|
||||||
|
python3 scripts/build.py # writes site/data.json
|
||||||
|
python3 -m http.server 8000 --directory site
|
||||||
|
```
|
||||||
|
|
||||||
|
## Deployment
|
||||||
|
|
||||||
|
Not yet deployed. This is a static site (`site/`) with no backend — any
|
||||||
|
static host works (the same nginx-in-front-of-a-container pattern archy's
|
||||||
|
other apps already use, GitHub/Gitea Pages, or a plain S3-style bucket).
|
||||||
|
`scripts/build.py` should run in CI on every push to `main` so `data.json`
|
||||||
|
never drifts from the source reports; wiring that up is the next step.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT — see [LICENSE](LICENSE).
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
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 — replace with a real one. Onboard WiFi drops the
|
||||||
|
connection under sustained heavy P2P traffic (Bitcoin IBD); a workaround
|
||||||
|
is to run over Ethernet instead. Everything else (Lightning, apps,
|
||||||
|
dashboard) works fine.
|
||||||
|
notes: |
|
||||||
|
NVMe via the official PCIe HAT. Boots noticeably faster than SD-card
|
||||||
|
installs.
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
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 — replace with a real one. Boots and installs cleanly from
|
||||||
|
a USB stick made with the standard ISO. No BIOS changes needed beyond
|
||||||
|
enabling AHCI mode (should already be default on most T430s).
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
{
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
|
"title": "ArchyHCL hardware report",
|
||||||
|
"description": "One reported install of Archipelago on a physical device. One file per report under data/reports/.",
|
||||||
|
"type": "object",
|
||||||
|
"required": ["device_model", "form_factor", "cpu", "ram_gb", "storage", "wifi_chip", "archy_version", "install_method", "status", "tested_date"],
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"device_model": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Manufacturer + model, e.g. \"Lenovo ThinkPad T430\" or \"Raspberry Pi 5 8GB\"."
|
||||||
|
},
|
||||||
|
"form_factor": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["laptop", "desktop", "mini-pc", "sbc", "server", "other"]
|
||||||
|
},
|
||||||
|
"cpu": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "e.g. \"Intel Core i5-3320M\", \"Broadcom BCM2712\"."
|
||||||
|
},
|
||||||
|
"ram_gb": {
|
||||||
|
"type": "number",
|
||||||
|
"exclusiveMinimum": 0
|
||||||
|
},
|
||||||
|
"storage": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["type", "size_gb"],
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"type": { "type": "string", "enum": ["hdd", "ssd", "nvme", "emmc", "sd"] },
|
||||||
|
"size_gb": { "type": "number", "exclusiveMinimum": 0 }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"wifi_chip": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Exact chipset if known, e.g. \"Intel Centrino Advanced-N 6205\". Use \"none\" for wired-only setups."
|
||||||
|
},
|
||||||
|
"ethernet": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Optional — matters for a node OS more than most HCLs. e.g. \"Intel 82579LM\"."
|
||||||
|
},
|
||||||
|
"archy_version": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Exact Archipelago version tested, e.g. \"1.8.4-alpha\"."
|
||||||
|
},
|
||||||
|
"install_method": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["usb-iso", "netboot", "existing-os-script", "other"]
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["working", "partial", "broken"],
|
||||||
|
"description": "working = installs and runs with no known issues. partial = installs and runs but with specific known issues (detail in `issues`). broken = does not install or does not run at all."
|
||||||
|
},
|
||||||
|
"tested_date": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^\\d{4}-\\d{2}-\\d{2}$"
|
||||||
|
},
|
||||||
|
"submitted_by": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Optional attribution — nostr npub, Gitea username, or leave unset for anonymous."
|
||||||
|
},
|
||||||
|
"issues": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Required detail when status is partial or broken. Free text: what broke, workarounds found."
|
||||||
|
},
|
||||||
|
"notes": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Anything else useful: BIOS/UEFI settings needed, quirks, links to a fuller writeup."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"if": {
|
||||||
|
"properties": { "status": { "enum": ["partial", "broken"] } }
|
||||||
|
},
|
||||||
|
"then": {
|
||||||
|
"required": ["issues"]
|
||||||
|
}
|
||||||
|
}
|
||||||
Executable
+59
@@ -0,0 +1,59 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Build site/data.json from data/reports/*.yml.
|
||||||
|
|
||||||
|
Every report is validated against data/schema.json before being included —
|
||||||
|
a malformed report fails the build loudly instead of shipping a broken row
|
||||||
|
to the site (same idea as archy's own scripts/validate-app-manifest.sh:
|
||||||
|
fail the build, don't ship the drift).
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import yaml
|
||||||
|
from jsonschema import Draft7Validator
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parent.parent
|
||||||
|
REPORTS_DIR = ROOT / "data" / "reports"
|
||||||
|
SCHEMA_PATH = ROOT / "data" / "schema.json"
|
||||||
|
OUT_PATH = ROOT / "site" / "data.json"
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
schema = json.loads(SCHEMA_PATH.read_text())
|
||||||
|
validator = Draft7Validator(schema)
|
||||||
|
|
||||||
|
reports = []
|
||||||
|
errors = []
|
||||||
|
|
||||||
|
for path in sorted(REPORTS_DIR.glob("*.yml")):
|
||||||
|
try:
|
||||||
|
data = yaml.safe_load(path.read_text())
|
||||||
|
except yaml.YAMLError as e:
|
||||||
|
errors.append(f"{path.name}: invalid YAML — {e}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
report_errors = sorted(validator.iter_errors(data), key=lambda e: e.path)
|
||||||
|
if report_errors:
|
||||||
|
for err in report_errors:
|
||||||
|
loc = ".".join(str(p) for p in err.path) or "(root)"
|
||||||
|
errors.append(f"{path.name}: {loc}: {err.message}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
data["_source_file"] = path.name
|
||||||
|
reports.append(data)
|
||||||
|
|
||||||
|
if errors:
|
||||||
|
print(f"Build failed — {len(errors)} error(s):", file=sys.stderr)
|
||||||
|
for e in errors:
|
||||||
|
print(f" {e}", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
reports.sort(key=lambda r: (r["device_model"], r["tested_date"]))
|
||||||
|
OUT_PATH.write_text(json.dumps(reports, indent=2) + "\n")
|
||||||
|
print(f"Wrote {len(reports)} report(s) to {OUT_PATH.relative_to(ROOT)}")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
+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