Scaffold weather app: reception forecast display + shared internal API
- Backend: Fastify service polling Met Office DataHub (Global Spot: hourly, three-hourly, daily endpoints) with Open-Meteo fallback, hourly refresh. Reuses settings' hotel_location/met_office integrations (same pattern as hvac's lib/weather.js) rather than its own credential copy. - Three display tiers stored in one forecast_points table: today (hourly), next 2 days (3-hourly), days 4-7 (plain daily) - 7 days total. - Public /api/public/forecast (no auth) feeds the reception kiosk display; /api/internal/forecast (WEATHER_SECRET bearer) lets other apps read the stored forecast instead of polling providers themselves; /api/admin/* (normal JWT + settings cap) is the staff status/refresh page. - Frontend: root / is the public full-screen Display (no AuthGate at all - first genuinely public app in the stack); /admin is the authed status page. apps.base_path is /weather/admin so the portal card opens the admin view directly, precedented by forecasting's history sub-route. - LXC 131 / 10.10.10.131, registered in auth/src/db.js, push-to-forgejo.sh, and stack-init/install-stack.sh (deploy_weather + --only dispatch + full install chain). - Typechecks and builds clean; not yet deployed. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
commit
f977a5778f
36 changed files with 8125 additions and 0 deletions
185
frontend/src/index.css
Normal file
185
frontend/src/index.css
Normal file
|
|
@ -0,0 +1,185 @@
|
|||
/* Stack design system tokens — include verbatim in every app */
|
||||
:root {
|
||||
--navy: #1a1a2e;
|
||||
--navy-dark: #0f0f20;
|
||||
--gold: #c9a84c;
|
||||
--gold-light: #e8c96d;
|
||||
--surface: rgba(255,255,255,0.07);
|
||||
--surface-2: rgba(255,255,255,0.08);
|
||||
--text: rgba(255,255,255,0.88);
|
||||
--text-muted: rgba(255,255,255,0.48);
|
||||
--body-bg: #f4f5f7;
|
||||
--card-bg: #ffffff;
|
||||
--card-border: #e4e8ee;
|
||||
--text-dark: #1e293b;
|
||||
--text-mid: #64748b;
|
||||
--shadow-sm: 0 1px 3px rgba(0,0,0,0.07), 0 1px 2px rgba(0,0,0,0.04);
|
||||
--shadow-md: 0 4px 12px rgba(0,0,0,0.08);
|
||||
--danger: #dc2626;
|
||||
--radius: 10px;
|
||||
--font: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
}
|
||||
body { background: var(--body-bg); color: var(--text-dark); font-family: var(--font); }
|
||||
|
||||
/* App theme */
|
||||
:root {
|
||||
--app-primary: #0ea5e9;
|
||||
--app-primary-light: #38bdf8;
|
||||
}
|
||||
|
||||
*, *::before, *::after { box-sizing: border-box; }
|
||||
html, body, #root { height: 100%; margin: 0; font-size: 14px; }
|
||||
|
||||
::-webkit-scrollbar { width: 4px; height: 4px; }
|
||||
::-webkit-scrollbar-track { background: transparent; }
|
||||
::-webkit-scrollbar-thumb { background: var(--card-border); border-radius: 2px; }
|
||||
|
||||
/* ── Reception display (full-screen, public, no chrome) ─────────────────── */
|
||||
.wx-display {
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
overflow: hidden;
|
||||
background: linear-gradient(160deg, var(--navy-dark) 0%, var(--navy) 60%, #14304a 100%);
|
||||
color: var(--text);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: clamp(16px, 3vh, 40px) clamp(20px, 4vw, 56px);
|
||||
font-size: clamp(14px, 1.1vw, 18px);
|
||||
}
|
||||
|
||||
.wx-display-header {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
margin-bottom: clamp(12px, 2vh, 28px);
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
}
|
||||
.wx-clock { font-size: clamp(2.2rem, 5vw, 4rem); font-weight: 700; line-height: 1; letter-spacing: -0.02em; }
|
||||
.wx-date { color: var(--text-muted); font-size: clamp(1rem, 1.6vw, 1.4rem); margin-top: 4px; }
|
||||
.wx-hotel-name { color: var(--gold); font-size: clamp(1.1rem, 1.8vw, 1.6rem); font-weight: 600; letter-spacing: .03em; text-transform: uppercase; }
|
||||
|
||||
.wx-now {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: clamp(12px, 2vw, 28px);
|
||||
margin-bottom: clamp(16px, 3vh, 32px);
|
||||
}
|
||||
.wx-now-temp { font-size: clamp(3.2rem, 7vw, 6rem); font-weight: 700; line-height: 1; }
|
||||
.wx-now-meta { display: flex; flex-direction: column; gap: 4px; color: var(--text-muted); font-size: clamp(0.9rem, 1.2vw, 1.15rem); }
|
||||
|
||||
.wx-section-label {
|
||||
font-size: clamp(0.75rem, 1vw, 0.95rem);
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: .08em;
|
||||
color: var(--text-muted);
|
||||
margin-bottom: clamp(8px, 1.2vh, 14px);
|
||||
}
|
||||
|
||||
/* Today's hourly course */
|
||||
.wx-hourly-row {
|
||||
display: flex;
|
||||
gap: clamp(6px, 1vw, 14px);
|
||||
overflow-x: auto;
|
||||
padding-bottom: 4px;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
.wx-hour-card {
|
||||
flex: 1 0 64px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: clamp(8px, 1vh, 14px) 4px;
|
||||
border-radius: var(--radius);
|
||||
background: var(--surface);
|
||||
}
|
||||
.wx-hour-card.now { background: rgba(201,168,76,0.16); border: 1px solid rgba(201,168,76,0.4); }
|
||||
.wx-hour-time { font-size: clamp(0.75rem, 0.9vw, 0.95rem); color: var(--text-muted); }
|
||||
.wx-hour-temp { font-size: clamp(0.95rem, 1.1vw, 1.2rem); font-weight: 600; }
|
||||
.wx-hour-precip { font-size: clamp(0.65rem, 0.8vw, 0.8rem); color: #7dd3fc; }
|
||||
|
||||
/* Next 2 days — 3-hourly */
|
||||
.wx-next-days {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: clamp(12px, 1.6vw, 24px);
|
||||
margin: clamp(16px, 2.5vh, 28px) 0;
|
||||
}
|
||||
.wx-next-day-card {
|
||||
background: var(--surface);
|
||||
border-radius: var(--radius);
|
||||
padding: clamp(10px, 1.4vh, 18px);
|
||||
}
|
||||
.wx-next-day-title { font-weight: 700; font-size: clamp(0.95rem, 1.2vw, 1.2rem); margin-bottom: 8px; }
|
||||
.wx-next-day-points { display: flex; gap: clamp(4px, 0.8vw, 10px); overflow-x: auto; }
|
||||
.wx-point { display: flex; flex-direction: column; align-items: center; gap: 4px; flex: 1 0 52px; }
|
||||
.wx-point-time { font-size: clamp(0.65rem, 0.8vw, 0.8rem); color: var(--text-muted); }
|
||||
.wx-point-temp { font-size: clamp(0.85rem, 1vw, 1.05rem); font-weight: 600; }
|
||||
|
||||
/* Days 4-7 — plain daily row */
|
||||
.wx-daily-row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: clamp(10px, 1.4vw, 20px);
|
||||
margin-top: auto;
|
||||
}
|
||||
.wx-day-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: clamp(10px, 1.4vh, 16px) 6px;
|
||||
border-radius: var(--radius);
|
||||
background: var(--surface);
|
||||
}
|
||||
.wx-day-name { font-weight: 700; font-size: clamp(0.85rem, 1vw, 1.05rem); }
|
||||
.wx-day-temps { display: flex; gap: 8px; font-size: clamp(0.8rem, 0.95vw, 1rem); }
|
||||
.wx-day-max { font-weight: 600; }
|
||||
.wx-day-min { color: var(--text-muted); }
|
||||
|
||||
.wx-updated { text-align: right; color: var(--text-muted); font-size: clamp(0.7rem, 0.8vw, 0.85rem); margin-top: 10px; }
|
||||
|
||||
.wx-fallback-screen {
|
||||
height: 100vh; display: flex; flex-direction: column; align-items: center; justify-content: center;
|
||||
gap: 12px; background: var(--navy-dark); color: var(--text-muted);
|
||||
}
|
||||
|
||||
/* ── Admin (authed) page ──────────────────────────────────────────────── */
|
||||
.wx-admin-shell { min-height: 100vh; background: var(--body-bg); display: flex; justify-content: center; padding: 32px 16px; }
|
||||
.wx-admin-panel { width: 100%; max-width: 560px; }
|
||||
.wx-admin-panel h1 { font-size: 20px; margin: 0 0 4px; color: var(--text-dark); }
|
||||
.wx-admin-sub { color: var(--text-mid); font-size: 13px; margin-bottom: 20px; }
|
||||
.wx-status-row { display: flex; justify-content: space-between; align-items: center; padding: 10px 0; border-bottom: 1px solid var(--card-border); font-size: 13.5px; }
|
||||
.wx-status-row:last-child { border-bottom: none; }
|
||||
.wx-status-label { color: var(--text-mid); }
|
||||
.wx-status-value { font-weight: 600; color: var(--text-dark); }
|
||||
.wx-status-value.ok { color: #16a34a; }
|
||||
.wx-status-value.err { color: var(--danger); }
|
||||
.wx-admin-actions { display: flex; gap: 10px; margin-top: 16px; }
|
||||
.wx-admin-link { color: var(--gold); text-decoration: none; font-weight: 500; }
|
||||
.wx-admin-link:hover { text-decoration: underline; }
|
||||
|
||||
/* buttons — same convention as every other app */
|
||||
.btn {
|
||||
display: inline-flex; align-items: center; gap: 6px;
|
||||
border: 1px solid var(--card-border); background: var(--card-bg); color: var(--text-dark);
|
||||
border-radius: var(--radius); padding: 7px 14px; font-size: 13px; cursor: pointer;
|
||||
font-family: var(--font); transition: background .12s, border-color .12s;
|
||||
}
|
||||
.btn:hover { border-color: var(--text-mid); }
|
||||
.btn:disabled { opacity: .5; cursor: default; }
|
||||
.btn-primary { background: var(--gold); border-color: var(--gold); color: var(--navy); font-weight: 600; }
|
||||
.btn-primary:hover { background: var(--gold-light); border-color: var(--gold-light); }
|
||||
|
||||
.card {
|
||||
background: var(--card-bg); border: 1px solid var(--card-border);
|
||||
border-radius: var(--radius); box-shadow: var(--shadow-sm); padding: 14px 16px; margin-bottom: 10px;
|
||||
}
|
||||
.error-banner {
|
||||
background: #fef2f2; border: 1px solid var(--danger); color: var(--danger);
|
||||
border-radius: var(--radius); padding: 10px 14px; margin-bottom: 12px; font-size: 13px;
|
||||
}
|
||||
@keyframes spin { to { transform: rotate(360deg); } }
|
||||
.spin { animation: spin .75s linear infinite; }
|
||||
Loading…
Add table
Add a link
Reference in a new issue