Restructure Web App into server/ + web/ (GsmNode parity)
Reorganize the Web App to match the GsmNode project layout: a Go backend-for-frontend in server/ that embeds the built SPA and reverse-proxies /api/* to the API Server, with the Vue 3 + Vite frontend moved into web/. - Move all frontend files into web/ (history preserved via renames) - Point vite build output at ../server/dist for Go embedding - Add server/ Go BFF (main.go, go.mod, .env.example, Run-WebApp.ps1) - Drop Docker/nginx deploy (Dockerfile, docker-compose.yml, nginx.conf.template, .dockerignore) in favor of the BFF, matching GsmNode - Update .claude/launch.json to run the dev server from web/ - Rewrite README.md for the new layout Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
ba3f227361
commit
75a2ccc226
@@ -0,0 +1,94 @@
|
||||
// Formatting + maintenance-status helpers. The status logic follows the
|
||||
// spreadsheet idea: a service is "due" when its computed next-service date
|
||||
// (service date + interval) approaches/passes today, OR when the car's current
|
||||
// odometer approaches/passes the computed next-service km.
|
||||
|
||||
import { prefs } from "../prefs.js";
|
||||
|
||||
export function formatDate(value) {
|
||||
if (!value) return "—";
|
||||
const d = new Date(value);
|
||||
if (isNaN(d)) return "—";
|
||||
|
||||
const day = String(d.getDate()).padStart(2, "0");
|
||||
const month = String(d.getMonth() + 1).padStart(2, "0");
|
||||
const monthName = d.toLocaleDateString(prefs.locale || undefined, { month: "short" });
|
||||
const year = d.getFullYear();
|
||||
|
||||
switch (prefs.dateFormat) {
|
||||
case "DMY_NUM":
|
||||
return `${day}-${month}-${year}`;
|
||||
case "DMY":
|
||||
return `${day} ${monthName} ${year}`;
|
||||
case "MDY":
|
||||
return `${monthName} ${day}, ${year}`;
|
||||
case "YMD":
|
||||
default:
|
||||
return `${year}-${month}-${day}`;
|
||||
}
|
||||
}
|
||||
|
||||
export function formatKm(value) {
|
||||
if (value == null || value === "" || value === 0) return "—";
|
||||
return Number(value).toLocaleString() + " km";
|
||||
}
|
||||
|
||||
const DAY = 24 * 60 * 60 * 1000;
|
||||
const KM_SOON = 1000; // within 1000 km of due => "soon"
|
||||
|
||||
// daysUntil returns whole days from today to the given date (negative = past).
|
||||
export function daysUntil(value) {
|
||||
if (!value) return null;
|
||||
const target = new Date(value);
|
||||
if (isNaN(target)) return null;
|
||||
const today = new Date();
|
||||
today.setHours(0, 0, 0, 0);
|
||||
target.setHours(0, 0, 0, 0);
|
||||
return Math.round((target - today) / DAY);
|
||||
}
|
||||
|
||||
// Severity ranking so we can pick the worst of the date/km signals.
|
||||
const RANK = { unknown: 0, ok: 1, soon: 2, overdue: 3 };
|
||||
// DriverVault status language: On track (green) / Due soon (amber) / Action
|
||||
// needed (red). Uses the shared badge recipes from style.css so tints flip in
|
||||
// dark mode automatically.
|
||||
const STYLE = {
|
||||
unknown: "dh-badge dh-badge-neutral",
|
||||
ok: "dh-badge dh-badge-success",
|
||||
soon: "dh-badge dh-badge-warning",
|
||||
overdue: "dh-badge dh-badge-danger",
|
||||
};
|
||||
|
||||
// dateSignal classifies the next-due date relative to today.
|
||||
function dateSignal(nextServiceDate) {
|
||||
const days = daysUntil(nextServiceDate);
|
||||
if (days == null) return { key: "unknown", label: "No data" };
|
||||
if (days < 0) return { key: "overdue", label: `Service Overdue ${Math.abs(days)}d` };
|
||||
if (days <= 30) return { key: "soon", label: `Due in ${days}d` };
|
||||
return { key: "ok", label: `OK · ${days}d` };
|
||||
}
|
||||
|
||||
// kmSignal classifies the current odometer against the next-due km.
|
||||
function kmSignal(currentKm, nextServiceKm) {
|
||||
if (!currentKm || !nextServiceKm) return { key: "unknown", label: "No km" };
|
||||
const remaining = nextServiceKm - currentKm;
|
||||
if (remaining < 0) return { key: "overdue", label: `Service Overdue ${Math.abs(remaining).toLocaleString()} km` };
|
||||
if (remaining <= KM_SOON) return { key: "soon", label: `In ${remaining.toLocaleString()} km` };
|
||||
return { key: "ok", label: `${remaining.toLocaleString()} km left` };
|
||||
}
|
||||
|
||||
// serviceStatus combines the date- and km-based signals, returning the worse of
|
||||
// the two for the badge. `latest` is the most recent service record (with
|
||||
// nextServiceDate/nextServiceKm); `car` carries the current odometer.
|
||||
export function serviceStatus(latest, car = null) {
|
||||
const date = dateSignal(latest?.nextServiceDate);
|
||||
const km = kmSignal(car?.currentKm, latest?.nextServiceKm);
|
||||
|
||||
const worse = RANK[km.key] > RANK[date.key] ? km : date;
|
||||
// If only one signal has data, use that one's label.
|
||||
let label = worse.label;
|
||||
if (date.key === "unknown" && km.key !== "unknown") label = km.label;
|
||||
else if (km.key === "unknown" && date.key !== "unknown") label = date.label;
|
||||
|
||||
return { key: worse.key, label, classes: STYLE[worse.key], date, km };
|
||||
}
|
||||
Reference in New Issue
Block a user