Phase 0: repo skeleton, image, compose stacks, doctor, migration runner, frontend shell
New repo for the Packet Station rebuild (Python 3.12, FastAPI, PostgreSQL 16, React, VisualKit, MFilesKit, Docker Compose on docker-1). Nothing is migrated from the M: install. - CLAUDE.md with the safety rules, README, CHANGELOG, docs (BRIEF, PLAN, SECURITY rewritten for the server with the requirement ID table, DEPLOY, stubs for FEED, POLICY, SCHEMA, GO-LIVE, PAPER-FALLBACK). - packetstation CLI: app, worker (heartbeat stub), migrate, doctor, version, healthcheck. Settings from the environment with secret files. Stack guard (test never points at VISUAL production). Security headers and CSP. Trusted proxy resolved by name. JSON logs with redaction. - Migration runner and 0001_base: settings, append-only audit_events with hash chain, ps_notify, grants for ps_app. - Multi-stage Dockerfile (ODBC Driver 18, PESCO libraries from the Gitea index only), compose base, proxy overlay for npm_default, CI overlay, postgres initdb roles, env examples, NPM proxy host notes, Gitea Actions. - Frontend: Vite, React, TypeScript, the PESCO dark theme ported with self-hosted fonts, placeholder sign-in and home. - Verified on docker-1: image builds, CI stack comes up healthy, doctor database checks pass, 77 server tests pass against PostgreSQL. Co-Authored-By: Claude Fable 5.1 <[email protected]>
This commit is contained in:
@@ -0,0 +1,241 @@
|
||||
/*
|
||||
PESCO dark theme. Ported from the old build's src/app.src.html (PacketStation repo).
|
||||
Values are copied as they were so trained users see the same look.
|
||||
Each block notes the old selectors it came from.
|
||||
Fonts: the old page loaded Barlow Condensed, IBM Plex Sans and IBM Plex Mono from Google Fonts.
|
||||
Here the same families are self-hosted from the @fontsource npm packages (see src/theme/fonts.ts).
|
||||
The fallback stacks are the old ones, so the page still reads right if a font file fails.
|
||||
*/
|
||||
|
||||
/* From :root. */
|
||||
:root {
|
||||
--bg: #0d161b;
|
||||
--panel: #1a252b;
|
||||
--panel2: #162931;
|
||||
--line: #26363e;
|
||||
--line2: #33474f;
|
||||
--cyan: #49c0de;
|
||||
--pink: #e5589f;
|
||||
--amber: #e7b33c;
|
||||
--green: #5fc98a;
|
||||
--red: #ef6b6b;
|
||||
--white: #ffffff;
|
||||
--near: #eff5f8;
|
||||
--body: #a9bac3;
|
||||
--muted: #77878f;
|
||||
--warnbg: #221b28;
|
||||
/* Shades the old page wrote inline. Named here so components never hard-code them. */
|
||||
--on-cyan: #06222b;
|
||||
--on-pink: #2a0717;
|
||||
--on-green: #06210f;
|
||||
--field: #0f1b20;
|
||||
--col: #111d23;
|
||||
--sel: #123643;
|
||||
--sheet: #13222a;
|
||||
--top-from: #0f2129;
|
||||
--scrim: rgba(3, 8, 10, 0.66);
|
||||
--f-disp: "Barlow Condensed", "Arial Narrow", Arial, sans-serif;
|
||||
--f-body: "IBM Plex Sans", Helvetica, Arial, sans-serif;
|
||||
--f-mono: "IBM Plex Mono", ui-monospace, Consolas, monospace;
|
||||
--toph: 64px;
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
/* From *, [hidden], html, body, form controls, :focus-visible, reduced motion, .wrap. */
|
||||
*, *::before, *::after { box-sizing: border-box; }
|
||||
[hidden] { display: none !important; }
|
||||
html, body { background: var(--bg); }
|
||||
body { color: var(--body); font-family: var(--f-body); font-size: 15px; line-height: 1.45; margin: 0; }
|
||||
button, input, select, textarea { font: inherit; color: inherit; }
|
||||
:focus-visible { outline: 2px solid var(--cyan); outline-offset: 2px; }
|
||||
@media (prefers-reduced-motion: reduce) { * { transition: none !important; animation: none !important; } }
|
||||
.wrap { padding-inline: 16px; padding-block: 14px 48px; max-width: 1500px; margin: 0 auto; }
|
||||
|
||||
/* Top bar. From .top, .top-in, .brand, .brand img, .brand .nm, .roles, .top .sp, .who, .proto. */
|
||||
.top { position: sticky; top: env(safe-area-inset-top, 0px); z-index: 30; background: linear-gradient(180deg, var(--top-from), var(--bg)); border-bottom: 1px solid var(--line); padding-inline: 16px; padding-block: 10px; }
|
||||
.top-in { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; max-width: 1500px; margin: 0 auto; }
|
||||
.brand { display: flex; align-items: center; gap: 12px; min-width: 0; color: inherit; text-decoration: none; }
|
||||
.brand img { height: 34px; width: auto; }
|
||||
.brand .nm { font-family: var(--f-disp); font-weight: 600; font-size: 22px; letter-spacing: .06em; color: var(--near); text-transform: uppercase; white-space: nowrap; }
|
||||
.brand .nm span { color: var(--cyan); }
|
||||
.roles { display: flex; gap: 4px; background: var(--panel); border: 1px solid var(--line); border-radius: 10px; padding: 4px; flex-wrap: wrap; }
|
||||
.roles button { border: 0; background: transparent; padding: 8px 14px; border-radius: 7px; font-weight: 600; font-size: 14px; color: var(--body); cursor: pointer; min-height: 40px; }
|
||||
.roles button[aria-pressed="true"] { background: var(--cyan); color: var(--on-cyan); }
|
||||
.top .sp { flex: 1; }
|
||||
.who { font-size: 13px; color: var(--muted); display: flex; align-items: center; gap: 8px; }
|
||||
.who select { background: var(--panel); border: 1px solid var(--line2); border-radius: 8px; padding: 8px 10px; color: var(--near); font-weight: 600; min-height: 40px; }
|
||||
.proto { font-family: var(--f-mono); font-size: 11px; letter-spacing: .08em; color: var(--amber); border: 1px solid #5a4720; border-radius: 6px; padding: 3px 7px; text-transform: uppercase; }
|
||||
|
||||
/* Type. From .eyebrow, h1, h2, h3, .mono, .muted, .small, .sr, .lnk. */
|
||||
.eyebrow { font-family: var(--f-mono); font-size: 11.5px; font-weight: 600; letter-spacing: .14em; text-transform: uppercase; color: var(--cyan); }
|
||||
h1, h2, h3 { color: var(--near); margin: 0; text-wrap: balance; }
|
||||
h1 { font-family: var(--f-disp); font-weight: 700; font-size: 34px; letter-spacing: .02em; line-height: 1.05; }
|
||||
h2 { font-family: var(--f-disp); font-weight: 600; font-size: 22px; letter-spacing: .04em; text-transform: uppercase; }
|
||||
h3 { font-size: 15px; font-weight: 600; }
|
||||
.mono { font-family: var(--f-mono); font-variant-numeric: tabular-nums; }
|
||||
.muted { color: var(--muted); }
|
||||
.small { font-size: 13px; }
|
||||
.sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
|
||||
.lnk { all: unset; cursor: pointer; color: var(--cyan); text-decoration: underline; text-underline-offset: 3px; font-weight: 600; }
|
||||
.lnk:focus-visible { outline: 2px solid var(--cyan); outline-offset: 2px; }
|
||||
|
||||
/* KPI tiles. From .kpis, .kpi, button.kpi, .kpi.on, .kpi .n, .kpi .l, .kpi.hot, .kpi.zero, .phead, .pacts. */
|
||||
.kpis { display: grid; grid-template-columns: repeat(auto-fit, minmax(170px, 1fr)); gap: 12px; margin-block: 12px 20px; }
|
||||
.kpi { background: var(--panel); border: 1px solid var(--line); border-top: 3px solid var(--cyan); border-radius: 10px; padding: 10px 16px 12px; text-align: left; color: inherit; font: inherit; display: block; width: 100%; }
|
||||
button.kpi { cursor: pointer; position: relative; transition: border-color .15s, background .15s; }
|
||||
button.kpi:hover { border-color: var(--cyan); border-top-color: var(--cyan); }
|
||||
button.kpi::after { content: "Tap to show these \2193"; position: absolute; top: 10px; right: 12px; font-size: 11px; color: var(--muted); opacity: 0; transition: opacity .15s; }
|
||||
button.kpi:hover::after, button.kpi:focus-visible::after { opacity: 1; }
|
||||
.kpi.on { background: var(--sel); border-color: var(--cyan); }
|
||||
.kpi.on::after { content: "Showing only these, tap to clear"; opacity: 1; color: var(--cyan); }
|
||||
.kpi .n { font-family: var(--f-disp); font-weight: 700; font-size: 44px; line-height: 1; color: var(--white); font-variant-numeric: tabular-nums; }
|
||||
.kpi .n small { font-size: 20px; color: var(--muted); font-weight: 600; }
|
||||
.kpi .l { font-size: 12.5px; font-weight: 700; letter-spacing: .06em; text-transform: uppercase; color: var(--cyan); margin-top: 6px; }
|
||||
.kpi.hot { border-top-color: var(--pink); }
|
||||
.kpi.hot .l { color: var(--pink); }
|
||||
.kpi.zero { border-top-color: var(--line2); }
|
||||
.kpi.zero .n, .kpi.zero .l { color: var(--muted); }
|
||||
.phead { display: flex; align-items: flex-end; justify-content: space-between; gap: 12px 20px; flex-wrap: wrap; margin-top: 4px; }
|
||||
.pacts { display: flex; gap: 8px; flex-wrap: wrap; }
|
||||
|
||||
/* Sections, boards, cards, bars. From .sec, .sec-h, .board, .col, .col-h, .empty, .card, .bar, .panel. */
|
||||
.sec { margin-block: 8px 26px; }
|
||||
.sec-h { display: flex; align-items: baseline; gap: 12px; margin-bottom: 10px; flex-wrap: wrap; }
|
||||
.sec-h .ct { font-family: var(--f-mono); font-size: 13px; color: var(--muted); }
|
||||
.board { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 14px; }
|
||||
.col { background: var(--col); border: 1px solid var(--line); border-radius: 12px; padding: 12px; min-width: 0; display: flex; flex-direction: column; gap: 10px; }
|
||||
.col-h { display: flex; justify-content: space-between; align-items: center; font-family: var(--f-disp); font-size: 17px; font-weight: 600; letter-spacing: .06em; text-transform: uppercase; color: var(--near); }
|
||||
.col-h .c { font-family: var(--f-mono); font-size: 13px; color: var(--muted); }
|
||||
.empty { border: 1px dashed var(--line2); border-radius: 10px; padding: 16px; color: var(--muted); font-size: 13.5px; text-align: center; }
|
||||
.empty.slim { padding: 12px 14px; }
|
||||
.card { background: var(--panel); border: 1px solid var(--line); border-radius: 10px; padding: 12px 14px; display: flex; flex-direction: column; gap: 6px; text-align: left; width: 100%; color: inherit; font: inherit; transition: border-color .15s; }
|
||||
button.card, a.card { cursor: pointer; text-decoration: none; }
|
||||
button.card:hover, a.card:hover { border-color: var(--cyan); }
|
||||
.card .r1 { display: flex; justify-content: space-between; gap: 6px 8px; align-items: center; flex-wrap: wrap; }
|
||||
.card .r1 .pill { margin-left: auto; max-width: 100%; overflow: hidden; text-overflow: ellipsis; }
|
||||
.card .job { font-family: var(--f-disp); font-weight: 700; font-size: 20px; color: var(--white); letter-spacing: .03em; }
|
||||
.card .tt { color: var(--near); font-weight: 500; }
|
||||
.card .meta { display: flex; gap: 6px 14px; flex-wrap: wrap; font-size: 12.5px; color: var(--muted); }
|
||||
.card .meta b { color: var(--body); font-weight: 600; }
|
||||
.bar { height: 6px; border-radius: 3px; background: var(--bg); overflow: hidden; }
|
||||
.bar i { display: block; height: 100%; background: var(--cyan); }
|
||||
.bar i.over { background: var(--pink); }
|
||||
.panel { background: var(--panel); border: 1px solid var(--line); border-radius: 10px; padding: 12px 14px; }
|
||||
.panel h3 { margin-bottom: 8px; }
|
||||
|
||||
/* Pills and status colours. From .pill, .pill::before, .st-*. */
|
||||
.pill { display: inline-flex; align-items: center; gap: 6px; font-size: 11.5px; font-weight: 700; letter-spacing: .07em; text-transform: uppercase; border-radius: 999px; padding: 3px 10px; white-space: nowrap; border: 1px solid currentColor; }
|
||||
.pill::before { content: ""; width: 7px; height: 7px; border-radius: 50%; background: currentColor; }
|
||||
.st-draft { color: #9fb1ba; }
|
||||
.st-warehouse { color: var(--amber); }
|
||||
.st-ready { color: var(--cyan); }
|
||||
.st-assigned { color: #8fa6ff; }
|
||||
.st-in_progress { color: var(--green); }
|
||||
.st-review { color: var(--pink); }
|
||||
.st-qc { color: #b99cff; }
|
||||
.st-complete { color: #c6d4da; }
|
||||
.st-filed { color: var(--muted); }
|
||||
/* New in the rebuild. Health and error states for the shell. */
|
||||
.st-ok { color: var(--green); }
|
||||
.st-bad { color: var(--red); }
|
||||
.st-wait { color: var(--muted); }
|
||||
|
||||
/* Buttons and fields. From .btn and its variants, label.f, input.in, select.in, textarea.in, .in.big, .frow, .ckl. */
|
||||
.btn { display: inline-flex; align-items: center; justify-content: center; gap: 8px; border-radius: 9px; padding: 10px 18px; font-weight: 700; font-size: 15px; border: 1px solid var(--line2); background: var(--panel); color: var(--near); cursor: pointer; min-height: 46px; letter-spacing: .02em; text-decoration: none; }
|
||||
.btn:hover { border-color: var(--cyan); }
|
||||
.btn.pri { background: var(--cyan); border-color: var(--cyan); color: var(--on-cyan); }
|
||||
.btn.pink { background: var(--pink); border-color: var(--pink); color: var(--on-pink); }
|
||||
.btn.ghost { background: transparent; }
|
||||
.btn.sm { min-height: 36px; padding: 6px 12px; font-size: 13.5px; }
|
||||
.btn.dim { opacity: .6; }
|
||||
.btn.rejb { border-color: var(--red); color: var(--red); background: transparent; }
|
||||
.btn.voidbtn { border-color: var(--pink); color: var(--pink); }
|
||||
.btn:disabled { opacity: .45; cursor: not-allowed; }
|
||||
.btn svg { vertical-align: -4px; }
|
||||
label.f { display: flex; flex-direction: column; gap: 4px; font-size: 12px; font-weight: 700; letter-spacing: .06em; text-transform: uppercase; color: var(--muted); min-width: 0; }
|
||||
input.in, select.in, textarea.in { background: var(--field); border: 1px solid var(--line2); border-radius: 8px; padding: 10px 12px; color: var(--near); font-size: 15px; min-height: 44px; width: 100%; text-transform: none; letter-spacing: 0; font-weight: 500; }
|
||||
input.in:focus, select.in:focus, textarea.in:focus { border-color: var(--cyan); outline: none; }
|
||||
input.in:disabled, select.in:disabled, textarea.in:disabled { opacity: .55; cursor: not-allowed; }
|
||||
textarea.in { min-height: 80px; resize: vertical; }
|
||||
.in.big { font-size: 22px; min-height: 56px; font-family: var(--f-mono); letter-spacing: .08em; }
|
||||
.frow { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 12px; }
|
||||
.ckl { display: flex; gap: 10px; align-items: center; font-weight: 600; color: var(--near); min-height: 46px; cursor: pointer; }
|
||||
.ckl input { width: 24px; height: 24px; accent-color: var(--green); }
|
||||
|
||||
/* Chips and segmented control. From .chips, .chip, .seg, .uchip, .rchip. */
|
||||
.chips { display: flex; gap: 8px; flex-wrap: wrap; }
|
||||
.chip { display: inline-flex; gap: 8px; align-items: center; border: 1px solid var(--line2); border-radius: 999px; padding: 8px 14px; cursor: pointer; color: var(--near); background: var(--panel); font-size: 14px; min-height: 42px; }
|
||||
.chip[aria-pressed="true"] { border-color: var(--cyan); background: var(--sel); }
|
||||
.seg { display: inline-flex; border: 1px solid var(--line2); border-radius: 10px; overflow: hidden; }
|
||||
.seg button { background: var(--panel); color: var(--near); border: 0; padding: 8px 14px; min-height: 44px; cursor: pointer; font-size: 13.5px; }
|
||||
.seg button + button { border-left: 1px solid var(--line2); }
|
||||
.seg button[aria-pressed="true"], .seg button[aria-selected="true"] { background: var(--sel); color: var(--cyan); font-weight: 700; }
|
||||
.uchip { display: inline-flex; gap: 8px; align-items: center; background: var(--panel); border: 1px solid var(--line); border-radius: 999px; padding: 6px 12px; color: var(--near); font-size: 13px; }
|
||||
.uchip .mono { color: var(--muted); }
|
||||
.rchip { display: inline-block; line-height: 1.5; font-size: 11.5px; font-weight: 700; letter-spacing: .04em; border-radius: 6px; padding: 2px 8px; background: #14262e; color: var(--cyan); border: 1px solid #23414c; }
|
||||
.rchip.supervisor { color: var(--pink); border-color: #5a2a44; background: #22131b; }
|
||||
.rchip.lead { color: var(--green); border-color: #24503a; background: #10221a; }
|
||||
.rchip.admin { color: var(--amber); border-color: #5a4720; background: #211c10; }
|
||||
|
||||
/* Callouts and notices. From .callout, .callout.warn, .callout b, .viewonly, .back. */
|
||||
.callout { border-left: 3px solid var(--cyan); background: var(--panel); border-radius: 0 10px 10px 0; padding: 10px 14px; font-size: 14px; margin-block: 10px; }
|
||||
.callout.warn { border-left-color: var(--pink); background: var(--warnbg); }
|
||||
.callout b { color: var(--white); }
|
||||
.viewonly { display: flex; gap: 10px; align-items: flex-start; border: 1px dashed var(--line2); border-radius: 10px; padding: 10px 14px; margin: 8px 0; font-size: 14px; color: var(--body); background: rgba(255, 255, 255, .02); }
|
||||
.viewonly b { color: var(--near); }
|
||||
.back { all: unset; cursor: pointer; color: var(--cyan); font-weight: 600; font-size: 14px; display: inline-flex; gap: 6px; align-items: center; padding-block: 6px; }
|
||||
|
||||
/* Tabs. From .tabs, .tabs button, .tabs .cnt, .pmtabs (static variant). */
|
||||
.tabs { display: flex; gap: 2px; border-bottom: 1px solid var(--line); overflow-x: auto; margin-bottom: 14px; position: sticky; top: var(--toph); z-index: 15; background: var(--bg); }
|
||||
.tabs button { border: 0; background: transparent; color: var(--muted); font-weight: 600; font-size: 15px; padding: 12px 16px; border-bottom: 3px solid transparent; cursor: pointer; white-space: nowrap; min-height: 48px; }
|
||||
.tabs button[aria-selected="true"] { color: var(--near); border-bottom-color: var(--cyan); }
|
||||
.tabs .cnt { font-family: var(--f-mono); font-size: 12px; color: var(--cyan); margin-left: 6px; }
|
||||
.tabs.static { position: static; }
|
||||
|
||||
/* Tables. From .tw, table, th, td, td.num, td .sub. Wrap a table in .tw for sideways scroll. */
|
||||
.tw { overflow-x: auto; border: 1px solid var(--line); border-radius: 10px; }
|
||||
table { border-collapse: collapse; width: 100%; font-size: 14px; }
|
||||
th { background: var(--panel2); text-align: left; font-size: 11.5px; letter-spacing: .08em; text-transform: uppercase; color: var(--muted); font-weight: 700; padding: 9px 10px; white-space: nowrap; }
|
||||
td { border-top: 1px solid var(--line); padding: 9px 10px; vertical-align: middle; color: var(--near); }
|
||||
td.num, th.num { text-align: right; font-family: var(--f-mono); font-variant-numeric: tabular-nums; }
|
||||
td .sub { color: var(--muted); font-size: 12.5px; }
|
||||
|
||||
/* Sheet, modal and toast. From .scrim, .sheet, .toast. */
|
||||
.scrim { position: fixed; inset: 0; background: var(--scrim); z-index: 50; display: flex; align-items: center; justify-content: center; padding: 16px; overscroll-behavior: contain; }
|
||||
.sheet { background: var(--sheet); border: 1px solid var(--line2); border-radius: 16px; width: min(760px, 100%); max-height: calc(100vh - 32px); max-height: calc(100dvh - 32px); overflow: auto; padding: 18px; display: flex; flex-direction: column; gap: 14px; box-shadow: 0 18px 60px rgba(0, 0, 0, .55); margin: auto; }
|
||||
.sheet-acts { display: flex; gap: 10px; justify-content: flex-end; flex-wrap: wrap; }
|
||||
.toast { position: fixed; left: 50%; bottom: calc(20px + env(safe-area-inset-bottom, 0px)); transform: translateX(-50%); background: var(--near); color: var(--bg); font-weight: 600; padding: 10px 18px; border-radius: 10px; z-index: 60; box-shadow: 0 6px 20px rgba(0, 0, 0, .4); max-width: calc(100% - 32px); }
|
||||
|
||||
/* Sign-in card and PIN keypad. From .auth, .auth-card, .auth-form, .keypad. */
|
||||
.auth { display: grid; place-items: center; padding-block: 5vh 40px; }
|
||||
.auth-card { width: min(440px, 100%); background: var(--panel); border: 1px solid var(--line); border-top: 3px solid var(--cyan); border-radius: 14px; padding: 24px; display: flex; flex-direction: column; gap: 14px; }
|
||||
.auth-form { display: flex; flex-direction: column; gap: 12px; }
|
||||
.keypad { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; }
|
||||
.keypad button { min-height: 60px; border-radius: 10px; border: 1px solid var(--line2); background: var(--field); color: var(--near); font-family: var(--f-mono); font-size: 22px; font-weight: 600; cursor: pointer; }
|
||||
.keypad button:active { background: var(--sel); }
|
||||
.keypad button:disabled { opacity: .45; cursor: not-allowed; }
|
||||
|
||||
/* Lock screen placeholder. From body.blur-hidden #main and #guard .scrim. The guard sits above every sheet. */
|
||||
.blur-hidden .wrap { filter: blur(18px); }
|
||||
.scrim.lock { z-index: 90; }
|
||||
.lock .auth-card { border-top-color: var(--amber); }
|
||||
|
||||
/* Motion, opt-in through a .motion class on body. From .motion .scrim, .motion .sheet, .motion .toast and their keyframes. */
|
||||
@keyframes mfade { from { opacity: 0; } to { opacity: 1; } }
|
||||
@keyframes msheet { from { opacity: 0; transform: translateY(18px) scale(.985); } to { opacity: 1; transform: none; } }
|
||||
@keyframes mtoast { from { opacity: 0; transform: translate(-50%, 14px); } to { opacity: 1; transform: translate(-50%, 0); } }
|
||||
.motion .scrim { animation: mfade .16s ease-out; }
|
||||
.motion .sheet { animation: msheet .24s cubic-bezier(.2, .7, .2, 1); }
|
||||
.motion .toast { animation: mtoast .22s cubic-bezier(.2, .7, .2, 1); }
|
||||
|
||||
/* Narrow screens. From the old @media (max-width:700px) and (max-width:640px) blocks. */
|
||||
@media (max-width: 700px) {
|
||||
.tw table { min-width: 620px; }
|
||||
.tabs { top: 0; position: static; }
|
||||
}
|
||||
@media (max-width: 640px) {
|
||||
h1 { font-size: 28px; }
|
||||
.kpi .n { font-size: 38px; }
|
||||
.brand .nm { font-size: 18px; }
|
||||
.phead h1 { font-size: 26px; }
|
||||
}
|
||||
Reference in New Issue
Block a user