Initial commit: boarding pass to AirTrail pipeline

Photograph/screenshot a boarding pass, decode its BCBP barcode (PDF417/
Aztec/QR/DataMatrix), fetch the historical flight track from FlightAware
the day after the flight, stage it for review, and write it into AirTrail
via its REST API on approval. ntfy notifications carry signed
Approve/Deny/Investigate actions.
This commit is contained in:
2026-07-04 22:21:02 +10:00
commit 408fde440d
34 changed files with 11657 additions and 0 deletions
+258
View File
@@ -0,0 +1,258 @@
/* Design tokens - kept as CSS custom properties under these exact names so a
real apointless.css can drop in later without touching any template. */
:root {
--bg: #0b0d11;
--surface-1: #12151b;
--surface-2: #1a1e28;
--surface-3: #232838;
--border: #2a3040;
--text-primary: #e6e9ef;
--text-secondary: #9aa3b2;
--accent: #3b82f6;
--accent-hover: #5b95f7;
--success: #22c55e;
--danger: #ef4444;
--warning: #f59e0b;
--font-body: "DM Sans", system-ui, -apple-system, sans-serif;
--font-mono: "JetBrains Mono", ui-monospace, SFMono-Regular, monospace;
--radius: 10px;
}
html.light {
--bg: #f5f6f8;
--surface-1: #ffffff;
--surface-2: #f0f1f4;
--surface-3: #e5e7eb;
--border: #d7dae0;
--text-primary: #14161a;
--text-secondary: #565d6b;
--accent: #2563eb;
--accent-hover: #1d4ed8;
}
* { box-sizing: border-box; }
html, body {
margin: 0;
padding: 0;
background: var(--bg);
color: var(--text-primary);
font-family: var(--font-body);
min-height: 100%;
}
a { color: var(--accent); text-decoration: none; }
a:hover { color: var(--accent-hover); }
.muted { color: var(--text-secondary); }
.topbar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.9rem 1.5rem;
background: var(--surface-1);
border-bottom: 1px solid var(--border);
}
.topbar-nav { display: flex; align-items: center; gap: 1.25rem; }
.topbar-nav a { color: var(--text-secondary); font-weight: 500; }
.topbar-nav a:hover { color: var(--text-primary); }
.brand { color: var(--text-primary) !important; font-weight: 700; }
.icon-btn {
background: var(--surface-2);
border: 1px solid var(--border);
color: var(--text-primary);
border-radius: 50%;
width: 2.2rem;
height: 2.2rem;
cursor: pointer;
font-size: 1rem;
}
.icon-btn:hover { background: var(--surface-3); }
.page {
max-width: 900px;
margin: 0 auto;
padding: 2rem 1.5rem 4rem;
}
h1 { margin-top: 0; }
.card {
background: var(--surface-1);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 1.25rem 1.5rem;
margin-bottom: 1.25rem;
}
.alert {
padding: 0.8rem 1rem;
border-radius: var(--radius);
margin-bottom: 1.25rem;
border: 1px solid transparent;
}
.alert-danger { background: rgba(239, 68, 68, 0.12); border-color: var(--danger); color: var(--danger); }
.alert-warning { background: rgba(245, 158, 11, 0.12); border-color: var(--warning); color: var(--warning); }
.alert-success { background: rgba(34, 197, 94, 0.12); border-color: var(--success); color: var(--success); }
.alert ul { margin: 0.4rem 0 0; padding-left: 1.2rem; }
.btn {
display: inline-block;
padding: 0.6rem 1.1rem;
border-radius: var(--radius);
border: 1px solid var(--border);
background: var(--surface-2);
color: var(--text-primary);
font-family: inherit;
font-size: 0.95rem;
cursor: pointer;
}
.btn:hover { background: var(--surface-3); }
.btn:disabled { opacity: 0.5; cursor: not-allowed; }
.btn-primary { background: var(--accent); border-color: var(--accent); color: #fff; }
.btn-primary:hover { background: var(--accent-hover); }
.btn-success { background: var(--success); border-color: var(--success); color: #06210f; }
.btn-danger { background: var(--danger); border-color: var(--danger); color: #2a0a0a; }
.btn-secondary { background: var(--surface-2); }
.upload-form { display: flex; flex-direction: column; gap: 1rem; align-items: flex-start; }
.upload-drop {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
min-height: 10rem;
border: 2px dashed var(--border);
border-radius: var(--radius);
color: var(--text-secondary);
cursor: pointer;
text-align: center;
padding: 1rem;
}
.upload-drop:hover { border-color: var(--accent); color: var(--text-primary); }
.upload-drop input { display: none; }
.tabs {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
margin-bottom: 1.25rem;
border-bottom: 1px solid var(--border);
padding-bottom: 0.75rem;
}
.tab {
padding: 0.4rem 0.9rem;
border-radius: 999px;
color: var(--text-secondary);
font-size: 0.9rem;
}
.tab:hover { background: var(--surface-2); color: var(--text-primary); }
.tab-active { background: var(--accent); color: #fff; }
.tab-active:hover { background: var(--accent-hover); color: #fff; }
.flight-list { display: flex; flex-direction: column; gap: 0.6rem; }
.flight-row {
display: grid;
grid-template-columns: auto auto auto 1fr auto;
align-items: center;
gap: 1rem;
color: var(--text-primary);
}
.flight-row:hover { border-color: var(--accent); }
.flight-route { font-family: var(--font-mono); }
.flight-number { font-family: var(--font-mono); color: var(--text-secondary); }
.badge {
display: inline-block;
padding: 0.2rem 0.6rem;
border-radius: 999px;
font-size: 0.75rem;
font-weight: 500;
text-transform: capitalize;
background: var(--surface-3);
color: var(--text-secondary);
}
.badge-pending_review { background: rgba(59, 130, 246, 0.15); color: var(--accent); }
.badge-track_unavailable { background: rgba(245, 158, 11, 0.15); color: var(--warning); }
.badge-decode_failed { background: rgba(239, 68, 68, 0.15); color: var(--danger); }
.badge-approved { background: rgba(34, 197, 94, 0.15); color: var(--success); }
.badge-rejected { background: rgba(239, 68, 68, 0.15); color: var(--danger); }
.badge-queued { background: rgba(154, 163, 178, 0.15); color: var(--text-secondary); }
.back-link { display: inline-block; margin-bottom: 1rem; color: var(--text-secondary); }
.detail-header h1 { display: flex; align-items: center; gap: 0.75rem; font-size: 1.5rem; }
.detail-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1.25rem;
}
.map-card { grid-column: 1 / -1; }
@media (max-width: 720px) {
.detail-grid { grid-template-columns: 1fr; }
.flight-row { grid-template-columns: auto 1fr; grid-template-rows: auto auto; }
}
.edit-form { display: flex; flex-direction: column; gap: 0.85rem; }
.edit-form label {
display: flex;
flex-direction: column;
gap: 0.3rem;
font-size: 0.85rem;
color: var(--text-secondary);
}
.field-row { display: flex; gap: 1rem; }
.field-row label { flex: 1; }
input {
font-family: var(--font-mono);
font-size: 0.95rem;
background: var(--surface-2);
border: 1px solid var(--border);
border-radius: 6px;
padding: 0.5rem 0.6rem;
color: var(--text-primary);
}
input:focus { outline: 2px solid var(--accent); outline-offset: 1px; }
.meta-list {
margin: 1rem 0 0;
display: grid;
grid-template-columns: auto 1fr;
gap: 0.35rem 1rem;
font-size: 0.9rem;
}
.meta-list dt { color: var(--text-secondary); }
.meta-list dd { margin: 0; font-family: var(--font-mono); overflow-wrap: anywhere; }
.bp-photo { width: 100%; border-radius: var(--radius); display: block; }
.raw-bcbp { margin-top: 1rem; }
.raw-bcbp pre {
font-family: var(--font-mono);
font-size: 0.8rem;
background: var(--surface-2);
padding: 0.75rem;
border-radius: 6px;
overflow-x: auto;
white-space: pre-wrap;
word-break: break-all;
}
#track-map { height: 360px; border-radius: var(--radius); overflow: hidden; }
.action-bar { display: flex; gap: 0.75rem; flex-wrap: wrap; }
.settings-form { display: flex; flex-direction: column; gap: 1rem; max-width: 480px; }
.settings-form label {
display: flex;
flex-direction: column;
gap: 0.3rem;
font-size: 0.9rem;
color: var(--text-secondary);
}
.settings-actions { display: flex; gap: 0.75rem; }