diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 0ecacc1..73f3a39 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -2,11 +2,17 @@
Two ways to add a report, pick whichever's easier for you.
-## Option A — open an issue (no git needed)
+## Option A — use the report form (no git needed)
-[Open a hardware report issue](https://source.archipelago-foundation.org/ssmithx/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.
+[Fill in the report form](https://hcl.archipelago-foundation.org/report.html) —
+it opens a pre-filled Gitea issue with a ready-to-merge YAML block for you
+(you'll still need a free Gitea account to post it). A maintainer copies the
+block into a new data file and it shows up on the site.
+
+(The repo's `.github/ISSUE_TEMPLATE/hardware-report.yml` structured issue
+form doesn't render on this Gitea instance — it silently falls back to a
+blank issue instead of showing the fields — so the site form above is the
+supported no-git path, not the raw "New Issue" button.)
## Option B — open a PR directly
diff --git a/site/index.html b/site/index.html
index 62f6c2c..55a4239 100644
--- a/site/index.html
+++ b/site/index.html
@@ -8,10 +8,10 @@
Community-reported hardware compatibility for Archipelago. 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.
- Tested Archipelago on your own hardware? Report it —
+ Tested Archipelago on your own hardware? Report it —
takes two minutes and helps the next person know what to expect before they buy or repurpose a machine.
Fill this in, hit submit — it opens a pre-filled issue on Gitea with everything formatted and ready for a maintainer to merge. You'll still need a (free) Gitea account to actually post it.
+
+
+
+
+
+
+
+
+
+
+
diff --git a/site/report.js b/site/report.js
new file mode 100644
index 0000000..888e0c8
--- /dev/null
+++ b/site/report.js
@@ -0,0 +1,96 @@
+// Builds a pre-filled Gitea "new issue" URL (title + body via query params —
+// verified working on this Gitea instance even though its YAML issue-forms
+// rendering (.github/ISSUE_TEMPLATE/hardware-report.yml) does not render as
+// a structured form: it silently falls back to a blank issue instead, so we
+// don't rely on it) and opens it. The body includes a ready-to-paste YAML
+// block matching data/schema.json, so a maintainer can copy it straight
+// into a new data/reports/*.yml file with minimal editing.
+
+const REPO_URL = "https://source.archipelago-foundation.org/ssmithx/ArchyHCL";
+
+const form = document.getElementById("report-form");
+const statusSelect = document.getElementById("status");
+const issuesField = document.getElementById("issues");
+const issuesRequiredHint = document.getElementById("issues-required");
+const errorEl = document.getElementById("form-error");
+
+function updateIssuesRequirement() {
+ const required = statusSelect.value !== "working";
+ issuesRequiredHint.hidden = !required;
+ issuesField.required = required;
+}
+statusSelect.addEventListener("change", updateIssuesRequirement);
+updateIssuesRequirement();
+
+// A YAML double-quoted scalar uses the same escaping rules as JSON strings
+// for every character that matters here (quotes, backslashes, control
+// chars) — JSON.stringify is a safe, simple way to produce one.
+function yamlString(value) {
+ return JSON.stringify(value ?? "");
+}
+
+function yamlBlock(value) {
+ const trimmed = (value ?? "").trim();
+ if (!trimmed) return null;
+ return trimmed
+ .split("\n")
+ .map((line) => " " + line)
+ .join("\n");
+}
+
+function val(id) {
+ return document.getElementById(id).value.trim();
+}
+
+function buildYaml() {
+ const lines = [];
+ lines.push(`device_model: ${yamlString(val("device_model"))}`);
+ lines.push(`form_factor: ${val("form_factor")}`);
+ lines.push(`cpu: ${yamlString(val("cpu"))}`);
+ lines.push(`ram_gb: ${val("ram_gb")}`);
+ lines.push("storage:");
+ lines.push(` type: ${val("storage_type")}`);
+ lines.push(` size_gb: ${val("storage_size_gb")}`);
+ lines.push(`wifi_chip: ${yamlString(val("wifi_chip"))}`);
+ const ethernet = val("ethernet");
+ if (ethernet) lines.push(`ethernet: ${yamlString(ethernet)}`);
+ lines.push(`archy_version: ${yamlString(val("archy_version"))}`);
+ lines.push(`install_method: ${val("install_method")}`);
+ lines.push(`status: ${val("status")}`);
+ lines.push(`tested_date: ${yamlString(val("tested_date"))}`);
+ const submittedBy = val("submitted_by");
+ if (submittedBy) lines.push(`submitted_by: ${yamlString(submittedBy)}`);
+ const issuesBlock = yamlBlock(val("issues"));
+ if (issuesBlock) lines.push(`issues: |\n${issuesBlock}`);
+ const notesBlock = yamlBlock(val("notes"));
+ if (notesBlock) lines.push(`notes: |\n${notesBlock}`);
+ return lines.join("\n");
+}
+
+form.addEventListener("submit", (e) => {
+ e.preventDefault();
+ errorEl.hidden = true;
+
+ if (statusSelect.value !== "working" && !val("issues")) {
+ errorEl.textContent = 'Please describe the issue(s) — required when status is "partial" or "broken".';
+ errorEl.hidden = false;
+ issuesField.focus();
+ return;
+ }
+
+ const deviceModel = val("device_model");
+ const title = `hw: ${deviceModel}`;
+ const yaml = buildYaml();
+ const body = [
+ `Hardware report for **${deviceModel}**, submitted via the site form.`,
+ "",
+ "A maintainer: copy the block below into a new file under `data/reports/` (see CONTRIBUTING.md) to add it to the list.",
+ "",
+ "```yaml",
+ yaml,
+ "```",
+ ].join("\n");
+
+ const url = `${REPO_URL}/issues/new?title=${encodeURIComponent(title)}&body=${encodeURIComponent(body)}`;
+ window.open(url, "_blank", "noopener");
+});
diff --git a/site/style.css b/site/style.css
index 2863999..636e40a 100644
--- a/site/style.css
+++ b/site/style.css
@@ -138,3 +138,82 @@ footer code {
padding: 0.1rem 0.35rem;
border-radius: 3px;
}
+
+h1 a { color: inherit; text-decoration: none; }
+h1 a:hover { color: var(--accent); }
+
+.report-form {
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+ max-width: 640px;
+}
+
+.field {
+ display: flex;
+ flex-direction: column;
+ gap: 0.3rem;
+ flex: 1;
+}
+
+.field-row {
+ display: flex;
+ gap: 1rem;
+ flex-wrap: wrap;
+}
+
+.field-row .field {
+ min-width: 140px;
+}
+
+label {
+ color: var(--muted);
+ font-size: 0.85rem;
+}
+
+.required-hint {
+ color: var(--partial);
+ font-weight: normal;
+}
+
+textarea {
+ 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;
+ resize: vertical;
+}
+
+textarea:focus, .report-form input:focus, .report-form select:focus {
+ outline: none;
+ border-color: var(--accent-dim);
+}
+
+.report-form input, .report-form select {
+ width: 100%;
+}
+
+.form-error {
+ color: var(--broken);
+ font-size: 0.85rem;
+}
+
+.report-form button {
+ align-self: flex-start;
+ background: var(--accent);
+ color: var(--bg);
+ border: none;
+ padding: 0.7rem 1.2rem;
+ border-radius: 4px;
+ font-family: inherit;
+ font-weight: bold;
+ font-size: 0.9rem;
+ cursor: pointer;
+}
+
+.report-form button:hover {
+ background: var(--accent-dim);
+}