feat: Connect QA checklist UI to modular Deno/Postgres API

This commit is contained in:
Tyler G 2026-08-22 22:35:51 -07:00
parent 90cecc3ddc
commit 45946b797e
6 changed files with 156 additions and 24 deletions

25
main.ts
View File

@ -1,22 +1,16 @@
import { Hono } from "hono";
import { serveStatic } from "hono/deno";
import postgres from "postgres";
import { sql, initDb } from "./src/db/connection.ts";
import { qaRoutes } from "./src/routes/qa.ts";
const app = new Hono();
// Connect to Postgres using env vars (fallback for dev)
const sql = postgres({
host: Deno.env.get("POSTGRES_HOST") || "db",
port: parseInt(Deno.env.get("POSTGRES_PORT") || "5432"),
database: Deno.env.get("POSTGRES_DB") || "nas_builder",
username: Deno.env.get("POSTGRES_USER") || "nasadmin",
password: Deno.env.get("POSTGRES_PASSWORD") || "your_secure_database_password_here",
});
// Initialize DB schema on startup
await initDb();
// API Routes
app.get("/api/status", async (c) => {
try {
// Ping the database
const result = await sql`SELECT version()`;
return c.json({
status: "online",
@ -25,18 +19,15 @@ app.get("/api/status", async (c) => {
timestamp: new Date().toISOString()
});
} catch (error: any) {
return c.json({
status: "online",
database: "disconnected",
error: error.message
}, 500);
return c.json({ status: "online", database: "disconnected", error: error.message }, 500);
}
});
// Mount modular sub-routes
app.route("/api/qa", qaRoutes);
// Serve the 3D Viewer frontend
app.use("/*", serveStatic({ root: "./" }));
// Handle root redirect to viewer
app.get("/", (c) => c.redirect("/viewer/index.html"));
// Start the server

33
src/db/connection.ts Normal file
View File

@ -0,0 +1,33 @@
import postgres from "postgres";
export const sql = postgres({
host: Deno.env.get("POSTGRES_HOST") || "db",
port: parseInt(Deno.env.get("POSTGRES_PORT") || "5432"),
database: Deno.env.get("POSTGRES_DB") || "nas_builder",
username: Deno.env.get("POSTGRES_USER") || "nasadmin",
password: Deno.env.get("POSTGRES_PASSWORD") || "your_secure_database_password_here",
});
export async function initDb() {
await sql`
CREATE TABLE IF NOT EXISTS qa_checklists (
id VARCHAR(50) PRIMARY KEY,
label TEXT NOT NULL,
is_completed BOOLEAN DEFAULT false,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
`;
// Seed some initial data if empty
const count = await sql`SELECT count(*) FROM qa_checklists`;
if (count[0].count === "0") {
await sql`
INSERT INTO qa_checklists (id, label, is_completed) VALUES
('qa-cap', 'Supercap mounted & isolated', false),
('qa-sas', 'Broadcom SAS seated in PCIe', false),
('qa-cool', 'Waterblock pressure tested', false),
('qa-zfs', 'ZFS pool scrub baseline passed', false)
`;
}
console.log("✅ Database schema initialized");
}

22
src/db/qa.ts Normal file
View File

@ -0,0 +1,22 @@
import { sql } from "./connection.ts";
export interface QaTask {
id: string;
label: string;
is_completed: boolean;
}
export const getQaTasks = async (): Promise<QaTask[]> => {
const tasks = await sql<QaTask[]>`SELECT id, label, is_completed FROM qa_checklists ORDER BY id ASC`;
return tasks;
};
export const toggleQaTask = async (id: string, is_completed: boolean): Promise<QaTask[]> => {
const result = await sql<QaTask[]>`
UPDATE qa_checklists
SET is_completed = ${is_completed}, updated_at = CURRENT_TIMESTAMP
WHERE id = ${id}
RETURNING id, label, is_completed
`;
return result;
};

28
src/routes/qa.ts Normal file
View File

@ -0,0 +1,28 @@
import { Hono } from "hono";
import { getQaTasks, toggleQaTask } from "../db/qa.ts";
export const qaRoutes = new Hono();
qaRoutes.get("/", async (c) => {
try {
const tasks = await getQaTasks();
return c.json(tasks);
} catch (error: any) {
return c.json({ error: error.message }, 500);
}
});
qaRoutes.put("/:id", async (c) => {
const id = c.req.param("id");
try {
const body = await c.req.json();
if (typeof body.is_completed !== "boolean") {
return c.json({ error: "Invalid payload: is_completed must be a boolean" }, 400);
}
const updated = await toggleQaTask(id, body.is_completed);
if (updated.length === 0) return c.json({ error: "Task not found" }, 404);
return c.json(updated[0]);
} catch (error: any) {
return c.json({ error: error.message }, 500);
}
});

View File

@ -862,13 +862,9 @@
<span id="qa-progress-label" style="color:var(--accent-emerald); font-weight:700;">0% Complete</span>
</div>
<div class="mat-bar-bg" style="margin-bottom:0.75rem;"><div id="qa-progress-bar" class="mat-bar-fill" style="width:0%; background:var(--accent-emerald);"></div></div>
<div id="qa-checklist-container"><i class="fa-solid fa-spinner fa-spin"></i> Fetching live tasks from Postgres...</div>
<div class="check-item"><input type="checkbox" onchange="updateQAProgress()"><label><strong>Stage 1:</strong> Thermal insertion of 56x M3/M4 brass threaded inserts into 3D facade & fan wall.</label></div>
<div class="check-item"><input type="checkbox" onchange="updateQAProgress()"><label><strong>Stage 2:</strong> 2020 Aluminum frame assembly and squaring (Torque M5 corner bolts to 4.5 N·m).</label></div>
<div class="check-item"><input type="checkbox" onchange="updateQAProgress()"><label><strong>Stage 3:</strong> 3x 120mm PWM fan wall installation with EPDM pneumatic perimeter gasket.</label></div>
<div class="check-item"><input type="checkbox" onchange="updateQAProgress()"><label><strong>Stage 4:</strong> Baseboard PCBA mounting and MCIO 8i twinaxial cable routing.</label></div>
<div class="check-item"><input type="checkbox" onchange="updateQAProgress()"><label><strong>Stage 5:</strong> SendCutSend 5052 aluminum skins fastening & 6U rack ear installation.</label></div>
<div class="check-item"><input type="checkbox" onchange="updateQAProgress()"><label><strong>Stage 6:</strong> Supercapacitor PLP holdup validation & Rust firmware flashing.</label></div>
</div>
</div>
@ -926,7 +922,7 @@
</div>
<!-- Master Interactive JavaScript Application -->
<script>
<script type="module">
let scene, camera, renderer, controls;
let groupSkeleton, groupSheets, groupFacade, groupDrives, groupFans, groupPcba, groupIsolatedPart;
let raycaster, mouse;
@ -2223,7 +2219,10 @@
renderer.render(scene, camera);
}
import { loadQaChecklist } from './qa.js';
window.onload = function() {
loadQaChecklist();
init3D();
updatePrintFarmStats();
updateMaterialPhysics();

59
viewer/qa.js Normal file
View File

@ -0,0 +1,59 @@
// QA Checklist API Client
export async function loadQaChecklist() {
try {
const res = await fetch('/api/qa');
if (!res.ok) throw new Error("Failed to load QA data");
const tasks = await res.json();
const container = document.getElementById('qa-checklist-container');
if (!container) return;
container.innerHTML = ''; // Clear hardcoded items
tasks.forEach(task => {
const div = document.createElement('div');
div.className = 'check-item';
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.checked = task.is_completed;
checkbox.dataset.id = task.id;
checkbox.addEventListener('change', async (e) => {
await toggleQaTask(task.id, e.target.checked);
updateQAProgress();
});
const label = document.createElement('label');
label.innerHTML = `<strong>${task.id}:</strong> ${task.label}`;
div.appendChild(checkbox);
div.appendChild(label);
container.appendChild(div);
});
// Expose function to global scope since HTML uses inline onchange="updateQAProgress()" for some reason
window.updateQAProgress = () => {
const total = document.querySelectorAll('.check-item input').length;
const checked = document.querySelectorAll('.check-item input:checked').length;
const pct = Math.round((checked / total) * 100) || 0;
document.getElementById('qa-progress-label').innerText = `${pct}% Complete`;
document.getElementById('qa-progress-bar').style.width = `${pct}%`;
};
window.updateQAProgress();
} catch (err) {
console.error("QA Loading Error:", err);
}
}
export async function toggleQaTask(id, is_completed) {
try {
await fetch(`/api/qa/${id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ is_completed })
});
} catch (err) {
console.error("Failed to update QA task", err);
}
}