From 82851eb287fb20297ddfa7c7afbd3e2ebd1c7043 Mon Sep 17 00:00:00 2001 From: Richie Cahill Date: Sun, 8 Mar 2026 16:06:09 -0400 Subject: [PATCH] added van inventory serves --- python/van_inventory/main.py | 5 + python/van_inventory/routers/frontend.py | 8 +- python/van_inventory/static/style.css | 212 +++++++++++++++++++++++ python/van_inventory/templates/base.html | 28 +-- systems/brain/services/postgress.nix | 17 +- systems/brain/services/van_inventory.nix | 46 +++++ 6 files changed, 281 insertions(+), 35 deletions(-) create mode 100644 python/van_inventory/static/style.css create mode 100644 systems/brain/services/van_inventory.nix diff --git a/python/van_inventory/main.py b/python/van_inventory/main.py index 594f018..9b19fc1 100644 --- a/python/van_inventory/main.py +++ b/python/van_inventory/main.py @@ -4,16 +4,20 @@ from __future__ import annotations import logging from contextlib import asynccontextmanager +from pathlib import Path from typing import TYPE_CHECKING, Annotated import typer import uvicorn from fastapi import FastAPI +from fastapi.staticfiles import StaticFiles from python.common import configure_logger from python.orm.common import get_postgres_engine from python.van_inventory.routers import api_router, frontend_router +STATIC_DIR = Path(__file__).resolve().parent / "static" + if TYPE_CHECKING: from collections.abc import AsyncIterator @@ -30,6 +34,7 @@ def create_app() -> FastAPI: app.state.engine.dispose() app = FastAPI(title="Van Inventory", lifespan=lifespan) + app.mount("/static", StaticFiles(directory=STATIC_DIR), name="static") app.include_router(api_router) app.include_router(frontend_router) return app diff --git a/python/van_inventory/routers/frontend.py b/python/van_inventory/routers/frontend.py index e38c074..c51c0e7 100644 --- a/python/van_inventory/routers/frontend.py +++ b/python/van_inventory/routers/frontend.py @@ -3,7 +3,7 @@ from __future__ import annotations from pathlib import Path -from typing import TYPE_CHECKING, Annotated +from typing import Annotated from fastapi import APIRouter, Form, Request from fastapi.responses import HTMLResponse @@ -12,10 +12,10 @@ from sqlalchemy import select from sqlalchemy.orm import selectinload from python.orm.van_inventory.models import Item, Meal, MealIngredient -from python.van_inventory.routers.api import _check_meal -if TYPE_CHECKING: - from python.van_inventory.dependencies import DbSession +# FastAPI needs DbSession at runtime to resolve the Depends() annotation +from python.van_inventory.dependencies import DbSession # noqa: TC001 +from python.van_inventory.routers.api import _check_meal TEMPLATE_DIR = Path(__file__).resolve().parent.parent / "templates" templates = Jinja2Templates(directory=TEMPLATE_DIR) diff --git a/python/van_inventory/static/style.css b/python/van_inventory/static/style.css new file mode 100644 index 0000000..40ef037 --- /dev/null +++ b/python/van_inventory/static/style.css @@ -0,0 +1,212 @@ +:root { + --neon-pink: #ff2a6d; + --neon-cyan: #05d9e8; + --neon-yellow: #f9f002; + --neon-purple: #d300c5; + --bg-dark: #0a0a0f; + --bg-panel: #0d0d1a; + --bg-input: #111128; + --border: #1a1a3e; + --text: #c0c0d0; + --text-dim: #666680; +} + +* { box-sizing: border-box; margin: 0; padding: 0; } + +body { + font-family: 'Share Tech Mono', monospace; + max-width: 900px; + margin: 0 auto; + padding: 1rem; + background: var(--bg-dark); + color: var(--text); + position: relative; +} + +/* Scanline overlay */ +body::before { + content: ''; + position: fixed; + top: 0; left: 0; right: 0; bottom: 0; + background: repeating-linear-gradient( + 0deg, + transparent, + transparent 2px, + rgba(0, 0, 0, 0.08) 2px, + rgba(0, 0, 0, 0.08) 4px + ); + pointer-events: none; + z-index: 9999; +} + +h1, h2, h3 { + font-family: 'Orbitron', sans-serif; + margin-bottom: 0.5rem; + color: var(--neon-cyan); + text-shadow: 0 0 10px rgba(5, 217, 232, 0.5), 0 0 40px rgba(5, 217, 232, 0.2); + text-transform: uppercase; + letter-spacing: 2px; +} + +a { color: var(--neon-pink); text-decoration: none; transition: all 0.2s; } +a:hover { + text-shadow: 0 0 8px rgba(255, 42, 109, 0.8), 0 0 20px rgba(255, 42, 109, 0.4); +} + +nav { + display: flex; + gap: 1.5rem; + padding: 1rem 0; + border-bottom: 1px solid var(--border); + margin-bottom: 1.5rem; + position: relative; +} +nav::after { + content: ''; + position: absolute; + bottom: -1px; + left: 0; + right: 0; + height: 1px; + background: linear-gradient(90deg, var(--neon-pink), var(--neon-cyan), var(--neon-purple)); + opacity: 0.6; +} +nav a { + font-family: 'Orbitron', sans-serif; + font-weight: 700; + font-size: 0.85rem; + letter-spacing: 1px; + text-transform: uppercase; + padding: 0.3rem 0; + border-bottom: 2px solid transparent; + transition: all 0.2s; +} +nav a:hover { + border-bottom-color: var(--neon-pink); + text-shadow: 0 0 8px rgba(255, 42, 109, 0.8); +} + +table { + width: 100%; + border-collapse: collapse; + margin: 1rem 0; + border: 1px solid var(--border); +} +th, td { + text-align: left; + padding: 0.6rem 0.75rem; + border-bottom: 1px solid var(--border); +} +th { + font-family: 'Orbitron', sans-serif; + color: var(--neon-cyan); + font-size: 0.7rem; + text-transform: uppercase; + letter-spacing: 2px; + background: var(--bg-panel); + border-bottom: 1px solid var(--neon-cyan); + text-shadow: 0 0 6px rgba(5, 217, 232, 0.3); +} +tr:hover td { + background: rgba(5, 217, 232, 0.03); +} + +form { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; + align-items: end; + margin: 1rem 0; + padding: 1rem; + border: 1px solid var(--border); + background: var(--bg-panel); +} + +input, select { + padding: 0.5rem 0.6rem; + border: 1px solid var(--border); + border-radius: 2px; + background: var(--bg-input); + color: var(--neon-cyan); + font-family: 'Share Tech Mono', monospace; + transition: all 0.2s; +} +input:focus, select:focus { + outline: none; + border-color: var(--neon-cyan); + box-shadow: 0 0 8px rgba(5, 217, 232, 0.3), inset 0 0 8px rgba(5, 217, 232, 0.05); +} + +button { + padding: 0.5rem 1.2rem; + border: 1px solid var(--neon-pink); + border-radius: 2px; + background: transparent; + color: var(--neon-pink); + cursor: pointer; + font-family: 'Orbitron', sans-serif; + font-weight: 700; + font-size: 0.7rem; + letter-spacing: 1px; + text-transform: uppercase; + transition: all 0.2s; +} +button:hover { + background: var(--neon-pink); + color: var(--bg-dark); + box-shadow: 0 0 15px rgba(255, 42, 109, 0.5), 0 0 30px rgba(255, 42, 109, 0.2); +} +button.danger { + border-color: var(--text-dim); + color: var(--text-dim); +} +button.danger:hover { + border-color: var(--neon-pink); + background: var(--neon-pink); + color: var(--bg-dark); + box-shadow: 0 0 15px rgba(255, 42, 109, 0.5); +} + +.badge { + display: inline-block; + padding: 0.2rem 0.6rem; + border-radius: 2px; + font-family: 'Orbitron', sans-serif; + font-size: 0.65rem; + font-weight: 700; + letter-spacing: 1px; + text-transform: uppercase; +} +.badge.yes { + background: rgba(5, 217, 232, 0.1); + color: var(--neon-cyan); + border: 1px solid var(--neon-cyan); + text-shadow: 0 0 6px rgba(5, 217, 232, 0.5); +} +.badge.no { + background: rgba(255, 42, 109, 0.1); + color: var(--neon-pink); + border: 1px solid var(--neon-pink); + text-shadow: 0 0 6px rgba(255, 42, 109, 0.5); +} + +.missing-list { font-size: 0.85rem; color: var(--text-dim); } + +label { + font-size: 0.75rem; + color: var(--text-dim); + display: flex; + flex-direction: column; + gap: 0.2rem; + text-transform: uppercase; + letter-spacing: 1px; +} + +.flash { + padding: 0.5rem 1rem; + margin: 0.5rem 0; + border-radius: 2px; + background: rgba(5, 217, 232, 0.1); + color: var(--neon-cyan); + border: 1px solid var(--neon-cyan); +} diff --git a/python/van_inventory/templates/base.html b/python/van_inventory/templates/base.html index 54ecf59..d4610f7 100644 --- a/python/van_inventory/templates/base.html +++ b/python/van_inventory/templates/base.html @@ -5,31 +5,9 @@ {% block title %}Van Inventory{% endblock %} - + + +