feat: Connect QA checklist UI to modular Deno/Postgres API
This commit is contained in:
parent
90cecc3ddc
commit
45946b797e
25
main.ts
25
main.ts
@ -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
33
src/db/connection.ts
Normal 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
22
src/db/qa.ts
Normal 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
28
src/routes/qa.ts
Normal 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);
|
||||
}
|
||||
});
|
||||
@ -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
59
viewer/qa.js
Normal 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);
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user