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 { Hono } from "hono";
|
||||||
import { serveStatic } from "hono/deno";
|
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();
|
const app = new Hono();
|
||||||
|
|
||||||
// Connect to Postgres using env vars (fallback for dev)
|
// Initialize DB schema on startup
|
||||||
const sql = postgres({
|
await initDb();
|
||||||
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",
|
|
||||||
});
|
|
||||||
|
|
||||||
// API Routes
|
// API Routes
|
||||||
app.get("/api/status", async (c) => {
|
app.get("/api/status", async (c) => {
|
||||||
try {
|
try {
|
||||||
// Ping the database
|
|
||||||
const result = await sql`SELECT version()`;
|
const result = await sql`SELECT version()`;
|
||||||
return c.json({
|
return c.json({
|
||||||
status: "online",
|
status: "online",
|
||||||
@ -25,18 +19,15 @@ app.get("/api/status", async (c) => {
|
|||||||
timestamp: new Date().toISOString()
|
timestamp: new Date().toISOString()
|
||||||
});
|
});
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
return c.json({
|
return c.json({ status: "online", database: "disconnected", error: error.message }, 500);
|
||||||
status: "online",
|
|
||||||
database: "disconnected",
|
|
||||||
error: error.message
|
|
||||||
}, 500);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Mount modular sub-routes
|
||||||
|
app.route("/api/qa", qaRoutes);
|
||||||
|
|
||||||
// Serve the 3D Viewer frontend
|
// Serve the 3D Viewer frontend
|
||||||
app.use("/*", serveStatic({ root: "./" }));
|
app.use("/*", serveStatic({ root: "./" }));
|
||||||
|
|
||||||
// Handle root redirect to viewer
|
|
||||||
app.get("/", (c) => c.redirect("/viewer/index.html"));
|
app.get("/", (c) => c.redirect("/viewer/index.html"));
|
||||||
|
|
||||||
// Start the server
|
// 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>
|
<span id="qa-progress-label" style="color:var(--accent-emerald); font-weight:700;">0% Complete</span>
|
||||||
</div>
|
</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 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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -926,7 +922,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Master Interactive JavaScript Application -->
|
<!-- Master Interactive JavaScript Application -->
|
||||||
<script>
|
<script type="module">
|
||||||
let scene, camera, renderer, controls;
|
let scene, camera, renderer, controls;
|
||||||
let groupSkeleton, groupSheets, groupFacade, groupDrives, groupFans, groupPcba, groupIsolatedPart;
|
let groupSkeleton, groupSheets, groupFacade, groupDrives, groupFans, groupPcba, groupIsolatedPart;
|
||||||
let raycaster, mouse;
|
let raycaster, mouse;
|
||||||
@ -2223,7 +2219,10 @@
|
|||||||
renderer.render(scene, camera);
|
renderer.render(scene, camera);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
import { loadQaChecklist } from './qa.js';
|
||||||
|
|
||||||
window.onload = function() {
|
window.onload = function() {
|
||||||
|
loadQaChecklist();
|
||||||
init3D();
|
init3D();
|
||||||
updatePrintFarmStats();
|
updatePrintFarmStats();
|
||||||
updateMaterialPhysics();
|
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