home: add minerva dashboard scaffold
This commit is contained in:
0
home/minerva-dashboard/app/__init__.py
Normal file
0
home/minerva-dashboard/app/__init__.py
Normal file
11
home/minerva-dashboard/app/config.py
Normal file
11
home/minerva-dashboard/app/config.py
Normal file
@@ -0,0 +1,11 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class Settings:
|
||||
app_name: str = 'Minerva Dashboard'
|
||||
default_state_dir: Path = Path(os.environ.get('MINERVA_STATE_DIR', '/tmp/minerva-dashboard-state'))
|
||||
27
home/minerva-dashboard/app/main.py
Normal file
27
home/minerva-dashboard/app/main.py
Normal file
@@ -0,0 +1,27 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi import FastAPI
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
|
||||
from app.config import Settings
|
||||
from app.routes.api import router as api_router
|
||||
from app.routes.ui import router as ui_router
|
||||
from app.services.store import JSONStore
|
||||
|
||||
BASE_DIR = Path(__file__).resolve().parent
|
||||
|
||||
|
||||
def create_app(state_dir: Path | str | None = None) -> FastAPI:
|
||||
settings = Settings()
|
||||
app = FastAPI(title=settings.app_name)
|
||||
resolved_state_dir = Path(state_dir) if state_dir is not None else settings.default_state_dir
|
||||
app.state.store = JSONStore(resolved_state_dir)
|
||||
app.mount('/static', StaticFiles(directory=BASE_DIR / 'static'), name='static')
|
||||
app.include_router(ui_router)
|
||||
app.include_router(api_router)
|
||||
return app
|
||||
|
||||
|
||||
app = create_app()
|
||||
26
home/minerva-dashboard/app/models.py
Normal file
26
home/minerva-dashboard/app/models.py
Normal file
@@ -0,0 +1,26 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class QuickAddNotePayload(BaseModel):
|
||||
title: str = Field(min_length=1, max_length=120)
|
||||
body: str = Field(min_length=1, max_length=1000)
|
||||
category: str = Field(default='note', min_length=1, max_length=40)
|
||||
source: str = Field(default='quick-add', min_length=1, max_length=40)
|
||||
pinned: bool = False
|
||||
|
||||
|
||||
class DashboardSummary(BaseModel):
|
||||
profile: dict[str, Any]
|
||||
counts: dict[str, int]
|
||||
agenda: list[dict[str, Any]]
|
||||
reminders: list[dict[str, Any]]
|
||||
lists: list[dict[str, Any]]
|
||||
notes: list[dict[str, Any]]
|
||||
links: list[dict[str, Any]]
|
||||
meal_ideas: list[dict[str, Any]]
|
||||
shortcuts: list[dict[str, Any]]
|
||||
quick_actions: list[dict[str, Any]]
|
||||
22
home/minerva-dashboard/app/routes/api.py
Normal file
22
home/minerva-dashboard/app/routes/api.py
Normal file
@@ -0,0 +1,22 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from fastapi import APIRouter, Request, status
|
||||
|
||||
from app.models import DashboardSummary, QuickAddNotePayload
|
||||
|
||||
router = APIRouter(prefix='/api')
|
||||
|
||||
|
||||
@router.get('/health')
|
||||
def health() -> dict[str, str]:
|
||||
return {'status': 'ok'}
|
||||
|
||||
|
||||
@router.get('/dashboard/summary', response_model=DashboardSummary)
|
||||
def dashboard_summary(request: Request) -> dict:
|
||||
return request.app.state.store.dashboard_summary()
|
||||
|
||||
|
||||
@router.post('/notes/quick-add', status_code=status.HTTP_201_CREATED)
|
||||
def quick_add_note(payload: QuickAddNotePayload, request: Request) -> dict:
|
||||
return request.app.state.store.add_quick_note(payload.model_dump())
|
||||
42
home/minerva-dashboard/app/routes/ui.py
Normal file
42
home/minerva-dashboard/app/routes/ui.py
Normal file
@@ -0,0 +1,42 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi import APIRouter, Form, Request, status
|
||||
from fastapi.responses import HTMLResponse, RedirectResponse
|
||||
from fastapi.templating import Jinja2Templates
|
||||
|
||||
BASE_DIR = Path(__file__).resolve().parents[1]
|
||||
templates = Jinja2Templates(directory=str(BASE_DIR / 'templates'))
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get('/', response_class=HTMLResponse)
|
||||
def dashboard(request: Request) -> HTMLResponse:
|
||||
summary = request.app.state.store.dashboard_summary()
|
||||
return templates.TemplateResponse(
|
||||
request,
|
||||
'dashboard.html',
|
||||
{
|
||||
'summary': summary,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@router.post('/quick-actions/notes')
|
||||
def quick_add_note_form(
|
||||
request: Request,
|
||||
title: str = Form(...),
|
||||
body: str = Form(...),
|
||||
category: str = Form('note'),
|
||||
) -> RedirectResponse:
|
||||
request.app.state.store.add_quick_note(
|
||||
{
|
||||
'title': title,
|
||||
'body': body,
|
||||
'category': category,
|
||||
'source': 'dashboard-form',
|
||||
'pinned': False,
|
||||
}
|
||||
)
|
||||
return RedirectResponse(url='/', status_code=status.HTTP_303_SEE_OTHER)
|
||||
172
home/minerva-dashboard/app/services/store.py
Normal file
172
home/minerva-dashboard/app/services/store.py
Normal file
@@ -0,0 +1,172 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from copy import deepcopy
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
from uuid import uuid4
|
||||
|
||||
DEFAULT_STATE = {
|
||||
'profile': {
|
||||
'id': 'manndra',
|
||||
'display_name': 'Manndra',
|
||||
'role': 'Primary household user',
|
||||
'persona': 'Minerva',
|
||||
'house_style': 'Ravenclaw common room',
|
||||
'tone': 'Elegant, magical, useful',
|
||||
'primary_color_story': 'Midnight blue, parchment, and warm bronze',
|
||||
'notes': 'Seeded as the primary user for the Minerva household dashboard scaffold.',
|
||||
},
|
||||
'agenda': [
|
||||
{
|
||||
'id': 'agenda-1',
|
||||
'title': 'Morning check-in',
|
||||
'time_label': '8:30 AM',
|
||||
'detail': 'Review the day, any errands, and which household lane needs attention first.',
|
||||
},
|
||||
{
|
||||
'id': 'agenda-2',
|
||||
'title': 'After-lunch reset',
|
||||
'time_label': '1:30 PM',
|
||||
'detail': 'Tidy loose notes, review reminders, and confirm dinner direction.',
|
||||
},
|
||||
{
|
||||
'id': 'agenda-3',
|
||||
'title': 'Evening wind-down',
|
||||
'time_label': '8:00 PM',
|
||||
'detail': 'Wrap tomorrow notes, shortlist meals, and leave one clean next action.',
|
||||
},
|
||||
],
|
||||
'reminders': [
|
||||
{'id': 'reminder-1', 'title': 'Check school and household messages', 'urgency': 'today'},
|
||||
{'id': 'reminder-2', 'title': 'Restock one comfort-snack shelf item', 'urgency': 'this week'},
|
||||
{'id': 'reminder-3', 'title': 'Set aside one graceful task for future Minerva automation', 'urgency': 'someday'},
|
||||
],
|
||||
'lists': [
|
||||
{
|
||||
'id': 'list-1',
|
||||
'title': 'Errands',
|
||||
'items': [
|
||||
{'label': 'Pick up household basics', 'done': False},
|
||||
{'label': 'Confirm any school supply gaps', 'done': False},
|
||||
{'label': 'Check gift / occasion queue', 'done': False},
|
||||
],
|
||||
},
|
||||
{
|
||||
'id': 'list-2',
|
||||
'title': 'House reset',
|
||||
'items': [
|
||||
{'label': 'Clear one surface', 'done': True},
|
||||
{'label': 'Run a fast room sweep', 'done': False},
|
||||
{'label': 'Stage tomorrow launch pad', 'done': False},
|
||||
],
|
||||
},
|
||||
],
|
||||
'notes': [
|
||||
{
|
||||
'id': 'note-1',
|
||||
'title': 'Minerva welcome',
|
||||
'body': 'This front page should feel calm, capable, and immediately useful from a phone.',
|
||||
'category': 'welcome',
|
||||
'source': 'seed',
|
||||
'pinned': True,
|
||||
'created_at': '2026-05-23T09:00:00Z',
|
||||
},
|
||||
{
|
||||
'id': 'note-2',
|
||||
'title': 'Household rhythm',
|
||||
'body': 'Keep the wording practical: what matters now, what can wait, and what would make the day easier.',
|
||||
'category': 'guidance',
|
||||
'source': 'seed',
|
||||
'pinned': False,
|
||||
'created_at': '2026-05-23T10:15:00Z',
|
||||
},
|
||||
],
|
||||
'links': [
|
||||
{'id': 'link-1', 'label': 'Calendar', 'url': '#', 'note': 'Placeholder until live integration is chosen.'},
|
||||
{'id': 'link-2', 'label': 'School hub', 'url': '#', 'note': 'Quick route for classes, due dates, and messages.'},
|
||||
{'id': 'link-3', 'label': 'Household notes', 'url': '#', 'note': 'Pin common references in one predictable place.'},
|
||||
],
|
||||
'meal_ideas': [
|
||||
{'id': 'meal-1', 'title': 'Easy comfort dinner', 'detail': 'One low-friction weeknight fallback with leftovers.'},
|
||||
{'id': 'meal-2', 'title': 'Snack board night', 'detail': 'Small-prep option for a busier day.'},
|
||||
{'id': 'meal-3', 'title': 'Tomorrow prep idea', 'detail': 'Something that makes the next day easier.'},
|
||||
],
|
||||
'shortcuts': [
|
||||
{'id': 'shortcut-1', 'label': 'Quick note', 'hint': 'Capture the thing before it evaporates.'},
|
||||
{'id': 'shortcut-2', 'label': 'Meal spark', 'hint': 'Write one dinner idea without overthinking it.'},
|
||||
{'id': 'shortcut-3', 'label': 'Errand add', 'hint': 'Drop a list item while already on the move.'},
|
||||
],
|
||||
'quick_actions': [
|
||||
{'id': 'action-1', 'label': 'Capture note', 'detail': 'Fast jot for reminders, ideas, and school fragments.'},
|
||||
{'id': 'action-2', 'label': 'Review day', 'detail': 'One-tap habit: scan agenda, reminders, and lists.'},
|
||||
{'id': 'action-3', 'label': 'Choose dinner lane', 'detail': 'Decide between comfort, fast, or leftovers.'},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
class JSONStore:
|
||||
def __init__(self, state_dir: Path) -> None:
|
||||
self.state_dir = Path(state_dir)
|
||||
self.state_path = self.state_dir / 'state.json'
|
||||
self.state_dir.mkdir(parents=True, exist_ok=True)
|
||||
if not self.state_path.exists():
|
||||
self._write(deepcopy(DEFAULT_STATE))
|
||||
|
||||
def _read(self) -> dict[str, Any]:
|
||||
return json.loads(self.state_path.read_text())
|
||||
|
||||
def _write(self, payload: dict[str, Any]) -> None:
|
||||
self.state_path.write_text(json.dumps(payload, indent=2, sort_keys=True))
|
||||
|
||||
def dashboard_summary(self) -> dict[str, Any]:
|
||||
state = self._read()
|
||||
notes = self._sorted_notes(state['notes'])
|
||||
return {
|
||||
'profile': state['profile'],
|
||||
'counts': {
|
||||
'agenda': len(state['agenda']),
|
||||
'reminders': len(state['reminders']),
|
||||
'lists': len(state['lists']),
|
||||
'notes': len(notes),
|
||||
'links': len(state['links']),
|
||||
'meal_ideas': len(state['meal_ideas']),
|
||||
'shortcuts': len(state['shortcuts']),
|
||||
'quick_actions': len(state['quick_actions']),
|
||||
},
|
||||
'agenda': state['agenda'],
|
||||
'reminders': state['reminders'],
|
||||
'lists': state['lists'],
|
||||
'notes': notes,
|
||||
'links': state['links'],
|
||||
'meal_ideas': state['meal_ideas'],
|
||||
'shortcuts': state['shortcuts'],
|
||||
'quick_actions': state['quick_actions'],
|
||||
}
|
||||
|
||||
def add_quick_note(self, payload: dict[str, Any]) -> dict[str, Any]:
|
||||
state = self._read()
|
||||
saved = {
|
||||
'id': uuid4().hex,
|
||||
'title': payload['title'].strip(),
|
||||
'body': payload['body'].strip(),
|
||||
'category': payload.get('category', 'note').strip() or 'note',
|
||||
'source': payload.get('source', 'quick-add').strip() or 'quick-add',
|
||||
'pinned': bool(payload.get('pinned', False)),
|
||||
'created_at': datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace('+00:00', 'Z'),
|
||||
}
|
||||
state['notes'].append(saved)
|
||||
self._write(state)
|
||||
return saved
|
||||
|
||||
@staticmethod
|
||||
def _sorted_notes(notes: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
||||
return sorted(
|
||||
notes,
|
||||
key=lambda item: (
|
||||
not item.get('pinned', False),
|
||||
item.get('created_at', ''),
|
||||
),
|
||||
reverse=False,
|
||||
)[::-1]
|
||||
343
home/minerva-dashboard/app/static/app.css
Normal file
343
home/minerva-dashboard/app/static/app.css
Normal file
@@ -0,0 +1,343 @@
|
||||
:root {
|
||||
--bg: #0f1728;
|
||||
--bg-elevated: #18233b;
|
||||
--card: rgba(19, 31, 54, 0.86);
|
||||
--card-strong: rgba(17, 28, 49, 0.94);
|
||||
--text: #e7edf9;
|
||||
--muted: #b7c3db;
|
||||
--line: rgba(198, 169, 102, 0.18);
|
||||
--accent: #8bb8ff;
|
||||
--bronze: #c6a966;
|
||||
--shadow: 0 18px 40px rgba(3, 8, 20, 0.35);
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
html, body { margin: 0; padding: 0; }
|
||||
body {
|
||||
font-family: Inter, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
background:
|
||||
radial-gradient(circle at top, rgba(93, 129, 194, 0.16), transparent 32%),
|
||||
linear-gradient(180deg, #0d1524 0%, var(--bg) 100%);
|
||||
color: var(--text);
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
a { color: var(--accent); text-decoration: none; }
|
||||
a:hover { text-decoration: underline; }
|
||||
|
||||
.page-shell {
|
||||
width: min(1120px, calc(100vw - 1.5rem));
|
||||
margin: 0 auto;
|
||||
padding: 1rem 0 2.5rem;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: var(--card);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 20px;
|
||||
box-shadow: var(--shadow);
|
||||
padding: 1rem;
|
||||
backdrop-filter: blur(8px);
|
||||
}
|
||||
|
||||
.glow-card {
|
||||
background: linear-gradient(135deg, rgba(23, 37, 65, 0.96), rgba(26, 40, 68, 0.9));
|
||||
}
|
||||
|
||||
.masthead {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1.15fr) minmax(260px, 0.85fr);
|
||||
gap: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.masthead::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background:
|
||||
linear-gradient(120deg, rgba(10, 18, 32, 0.9) 0%, rgba(10, 18, 32, 0.82) 44%, rgba(12, 20, 34, 0.58) 100%),
|
||||
radial-gradient(circle at top right, rgba(139, 184, 255, 0.18), transparent 32%);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.masthead > * {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.masthead-copy {
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
align-content: space-between;
|
||||
}
|
||||
|
||||
.masthead h1, .section-heading h2, .spotlight h2, .subcard h3, .note-item h3 {
|
||||
font-family: Georgia, 'Times New Roman', serif;
|
||||
letter-spacing: 0.01em;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.masthead-meta, .section-heading, .note-head {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.masthead-hero-frame,
|
||||
.visual-medallion,
|
||||
.inline-ornament {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.masthead-hero-frame {
|
||||
display: grid;
|
||||
gap: 0.5rem;
|
||||
align-content: start;
|
||||
}
|
||||
|
||||
.masthead-hero-image {
|
||||
width: 100%;
|
||||
aspect-ratio: 16 / 10;
|
||||
object-fit: cover;
|
||||
border-radius: 18px;
|
||||
border: 1px solid rgba(198, 169, 102, 0.22);
|
||||
box-shadow: 0 14px 34px rgba(3, 8, 20, 0.32);
|
||||
}
|
||||
|
||||
.masthead-hero-frame figcaption,
|
||||
.visual-medallion figcaption,
|
||||
.inline-ornament figcaption {
|
||||
color: var(--muted);
|
||||
font-size: 0.78rem;
|
||||
}
|
||||
|
||||
.eyebrow {
|
||||
margin: 0 0 0.35rem;
|
||||
color: var(--bronze);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.12em;
|
||||
font-size: 0.72rem;
|
||||
}
|
||||
|
||||
.lede, .muted {
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.badge, .pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
border-radius: 999px;
|
||||
padding: 0.3rem 0.7rem;
|
||||
font-size: 0.78rem;
|
||||
border: 1px solid rgba(139, 184, 255, 0.26);
|
||||
background: rgba(139, 184, 255, 0.08);
|
||||
}
|
||||
|
||||
.badge.bronze, .pill.subtle {
|
||||
border-color: rgba(198, 169, 102, 0.28);
|
||||
background: rgba(198, 169, 102, 0.1);
|
||||
}
|
||||
|
||||
.badge.outline {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.hero-grid,
|
||||
.dashboard-grid {
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.hero-grid {
|
||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.dashboard-grid {
|
||||
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
|
||||
}
|
||||
|
||||
.spotlight {
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
.spotlight-visuals {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 0.85rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.visual-medallion {
|
||||
display: grid;
|
||||
gap: 0.45rem;
|
||||
background: rgba(13, 22, 38, 0.76);
|
||||
border: 1px solid rgba(198, 169, 102, 0.12);
|
||||
border-radius: 16px;
|
||||
padding: 0.65rem;
|
||||
}
|
||||
|
||||
.visual-medallion img,
|
||||
.inline-ornament img {
|
||||
width: 100%;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.visual-medallion img {
|
||||
border-radius: 14px;
|
||||
aspect-ratio: 1 / 1;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.framed-medallion {
|
||||
background:
|
||||
linear-gradient(180deg, rgba(27, 42, 70, 0.96), rgba(18, 29, 50, 0.92)),
|
||||
url('/static/images/art-nouveau-frame.svg') center / cover no-repeat;
|
||||
}
|
||||
|
||||
.room-medallion img {
|
||||
object-position: center 22%;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 0.75rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.stats-grid div,
|
||||
.subcard,
|
||||
.note-item {
|
||||
background: var(--card-strong);
|
||||
border: 1px solid rgba(198, 169, 102, 0.12);
|
||||
border-radius: 16px;
|
||||
padding: 0.85rem;
|
||||
}
|
||||
|
||||
.stats-grid span,
|
||||
.tiny {
|
||||
color: var(--muted);
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.stats-grid strong {
|
||||
display: block;
|
||||
font-size: 1.25rem;
|
||||
margin-top: 0.2rem;
|
||||
}
|
||||
|
||||
.stack-form {
|
||||
display: grid;
|
||||
gap: 0.85rem;
|
||||
}
|
||||
|
||||
label {
|
||||
display: grid;
|
||||
gap: 0.4rem;
|
||||
font-size: 0.92rem;
|
||||
}
|
||||
|
||||
input,
|
||||
textarea,
|
||||
button {
|
||||
width: 100%;
|
||||
border-radius: 14px;
|
||||
border: 1px solid rgba(139, 184, 255, 0.16);
|
||||
padding: 0.85rem 0.95rem;
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
input,
|
||||
textarea {
|
||||
background: rgba(10, 18, 33, 0.88);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
button {
|
||||
background: linear-gradient(135deg, #5c78b7, #7ea6ef);
|
||||
color: #09111e;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
filter: brightness(1.04);
|
||||
}
|
||||
|
||||
.timeline-list,
|
||||
.plain-list,
|
||||
.check-list {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: grid;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.timeline-list li,
|
||||
.plain-list li,
|
||||
.check-list li {
|
||||
background: rgba(15, 24, 42, 0.72);
|
||||
border: 1px solid rgba(198, 169, 102, 0.1);
|
||||
border-radius: 14px;
|
||||
padding: 0.8rem;
|
||||
}
|
||||
|
||||
.check-list li.done {
|
||||
text-decoration: line-through;
|
||||
opacity: 0.72;
|
||||
}
|
||||
|
||||
.notes-card {
|
||||
grid-column: span 2;
|
||||
}
|
||||
|
||||
.note-item.pinned {
|
||||
border-color: rgba(198, 169, 102, 0.34);
|
||||
}
|
||||
|
||||
.quick-add-card {
|
||||
align-self: start;
|
||||
}
|
||||
|
||||
.inline-ornament {
|
||||
margin-top: 1rem;
|
||||
display: grid;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
|
||||
.inline-ornament img {
|
||||
max-height: 96px;
|
||||
object-fit: cover;
|
||||
border-radius: 12px;
|
||||
border: 1px solid rgba(198, 169, 102, 0.16);
|
||||
background: linear-gradient(135deg, rgba(198, 169, 102, 0.18), rgba(139, 184, 255, 0.12));
|
||||
}
|
||||
|
||||
@media (max-width: 800px) {
|
||||
.page-shell {
|
||||
width: min(100vw - 1rem, 100%);
|
||||
}
|
||||
|
||||
.masthead {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.masthead-hero-image {
|
||||
aspect-ratio: 16 / 9;
|
||||
}
|
||||
|
||||
.spotlight-visuals {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.notes-card {
|
||||
grid-column: span 1;
|
||||
}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 238 KiB |
BIN
home/minerva-dashboard/app/static/images/blue-interior.jpg
Normal file
BIN
home/minerva-dashboard/app/static/images/blue-interior.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 259 KiB |
BIN
home/minerva-dashboard/app/static/images/library-hero.jpg
Normal file
BIN
home/minerva-dashboard/app/static/images/library-hero.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 359 KiB |
BIN
home/minerva-dashboard/app/static/images/owl-engraving.jpg
Normal file
BIN
home/minerva-dashboard/app/static/images/owl-engraving.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 803 KiB |
38
home/minerva-dashboard/app/templates/base.html
Normal file
38
home/minerva-dashboard/app/templates/base.html
Normal file
@@ -0,0 +1,38 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{{ summary.profile.persona if summary is defined else 'Minerva' }} Dashboard</title>
|
||||
<link rel="stylesheet" href="/static/app.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="page-shell">
|
||||
<header class="masthead card glow-card">
|
||||
<div class="masthead-copy">
|
||||
<div>
|
||||
<p class="eyebrow">Ravenclaw common room</p>
|
||||
<h1>Minerva Dashboard</h1>
|
||||
<p class="lede">Elegant household focus for {{ summary.profile.display_name if summary is defined else 'Manndra' }}: agenda, lists, notes, and one calm place to decide what matters now.</p>
|
||||
</div>
|
||||
<div class="masthead-meta">
|
||||
<span class="badge">Private-first</span>
|
||||
<span class="badge bronze">Phone friendly</span>
|
||||
<span class="badge outline">{{ summary.profile.tone if summary is defined else 'Elegant, magical, useful' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<figure class="masthead-hero-frame">
|
||||
<img
|
||||
class="masthead-hero-image"
|
||||
src="/static/images/library-hero.jpg"
|
||||
alt="Grand library interior with high shelves and warm lamplight"
|
||||
>
|
||||
<figcaption>Library study-hall artwork grounding the dashboard in a scholarly common-room mood.</figcaption>
|
||||
</figure>
|
||||
</header>
|
||||
<main>
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
160
home/minerva-dashboard/app/templates/dashboard.html
Normal file
160
home/minerva-dashboard/app/templates/dashboard.html
Normal file
@@ -0,0 +1,160 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block content %}
|
||||
<section class="hero-grid">
|
||||
<article class="card spotlight">
|
||||
<div class="section-heading">
|
||||
<div>
|
||||
<p class="eyebrow">Primary profile</p>
|
||||
<h2>{{ summary['profile']['display_name'] }} / {{ summary['profile']['persona'] }}</h2>
|
||||
</div>
|
||||
<span class="badge bronze">{{ summary['profile']['house_style'] }}</span>
|
||||
</div>
|
||||
<p class="muted">{{ summary['profile']['notes'] }}</p>
|
||||
<div class="stats-grid">
|
||||
<div><span>Agenda</span><strong>{{ summary['counts']['agenda'] }}</strong></div>
|
||||
<div><span>Reminders</span><strong>{{ summary['counts']['reminders'] }}</strong></div>
|
||||
<div><span>Lists</span><strong>{{ summary['counts']['lists'] }}</strong></div>
|
||||
<div><span>Notes</span><strong>{{ summary['counts']['notes'] }}</strong></div>
|
||||
</div>
|
||||
<div class="spotlight-visuals" aria-label="Ravenclaw common-room imagery">
|
||||
<figure class="visual-medallion framed-medallion">
|
||||
<img src="/static/images/owl-engraving.jpg" alt="Vintage owl engraving used as a scholarly crest substitute">
|
||||
<figcaption>Owl engraving crest</figcaption>
|
||||
</figure>
|
||||
<figure class="visual-medallion room-medallion">
|
||||
<img src="/static/images/blue-interior.jpg" alt="Blue-toned interior painting with elegant room details">
|
||||
<figcaption>Blue interior study corner</figcaption>
|
||||
</figure>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article class="card quick-add-card">
|
||||
<div class="section-heading">
|
||||
<div>
|
||||
<p class="eyebrow">Quick action</p>
|
||||
<h2>Capture a note before it disappears</h2>
|
||||
</div>
|
||||
<span class="badge">Minerva quick add</span>
|
||||
</div>
|
||||
<form method="post" action="/quick-actions/notes" class="stack-form">
|
||||
<label>Title
|
||||
<input type="text" name="title" placeholder="School follow-up, errand, reminder..." required>
|
||||
</label>
|
||||
<label>Note
|
||||
<textarea name="body" rows="4" placeholder="Write the useful fragment and keep moving." required></textarea>
|
||||
</label>
|
||||
<label>Category
|
||||
<input type="text" name="category" value="note" placeholder="note, reminder, meal, school">
|
||||
</label>
|
||||
<button type="submit">Save quick note</button>
|
||||
</form>
|
||||
<figure class="inline-ornament">
|
||||
<img src="/static/images/art-nouveau-frame.svg" alt="Art Nouveau ornamental frame accent">
|
||||
<figcaption>Ornamental frame accent for the quick-capture panel.</figcaption>
|
||||
</figure>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<section class="dashboard-grid">
|
||||
<article class="card">
|
||||
<div class="section-heading"><div><p class="eyebrow">Agenda</p><h2>Today at a glance</h2></div></div>
|
||||
<ul class="timeline-list">
|
||||
{% for item in summary['agenda'] %}
|
||||
<li>
|
||||
<strong>{{ item.time_label }}</strong>
|
||||
<span>{{ item.title }}</span>
|
||||
<p class="muted">{{ item.detail }}</p>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</article>
|
||||
|
||||
<article class="card">
|
||||
<div class="section-heading"><div><p class="eyebrow">Reminders</p><h2>Loose ends</h2></div></div>
|
||||
<ul class="plain-list">
|
||||
{% for item in summary['reminders'] %}
|
||||
<li>
|
||||
<strong>{{ item.title }}</strong>
|
||||
<span class="pill subtle">{{ item.urgency }}</span>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</article>
|
||||
|
||||
<article class="card">
|
||||
<div class="section-heading"><div><p class="eyebrow">Lists</p><h2>Household lanes</h2></div></div>
|
||||
{% for block in summary['lists'] %}
|
||||
<div class="subcard">
|
||||
<h3>{{ block['title'] }}</h3>
|
||||
<ul class="check-list">
|
||||
{% for item in block['items'] %}
|
||||
<li class="{{ 'done' if item['done'] else '' }}">{{ item['label'] }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</article>
|
||||
|
||||
<article class="card notes-card">
|
||||
<div class="section-heading"><div><p class="eyebrow">Notes</p><h2>Recent captures</h2></div></div>
|
||||
{% for item in summary['notes'][:5] %}
|
||||
<div class="note-item {{ 'pinned' if item['pinned'] else '' }}">
|
||||
<div class="note-head">
|
||||
<h3>{{ item['title'] }}</h3>
|
||||
<span class="pill subtle">{{ item['category'] }}</span>
|
||||
</div>
|
||||
<p>{{ item['body'] }}</p>
|
||||
<p class="muted tiny">{{ item['source'] }}{% if item['created_at'] %} · {{ item['created_at'] }}{% endif %}</p>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</article>
|
||||
|
||||
<article class="card">
|
||||
<div class="section-heading"><div><p class="eyebrow">Useful links</p><h2>Launch points</h2></div></div>
|
||||
<ul class="plain-list links-list">
|
||||
{% for item in summary['links'] %}
|
||||
<li>
|
||||
<a href="{{ item.url }}">{{ item.label }}</a>
|
||||
<p class="muted">{{ item.note }}</p>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</article>
|
||||
|
||||
<article class="card">
|
||||
<div class="section-heading"><div><p class="eyebrow">Meal ideas</p><h2>Dinner sparks</h2></div></div>
|
||||
<ul class="plain-list">
|
||||
{% for item in summary['meal_ideas'] %}
|
||||
<li>
|
||||
<strong>{{ item.title }}</strong>
|
||||
<p class="muted">{{ item.detail }}</p>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</article>
|
||||
|
||||
<article class="card">
|
||||
<div class="section-heading"><div><p class="eyebrow">Shortcuts</p><h2>Fast household moves</h2></div></div>
|
||||
<ul class="plain-list">
|
||||
{% for item in summary['shortcuts'] %}
|
||||
<li>
|
||||
<strong>{{ item.label }}</strong>
|
||||
<p class="muted">{{ item.hint }}</p>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</article>
|
||||
|
||||
<article class="card">
|
||||
<div class="section-heading"><div><p class="eyebrow">Quick actions</p><h2>Minerva prompts</h2></div></div>
|
||||
<ul class="plain-list">
|
||||
{% for item in summary['quick_actions'] %}
|
||||
<li>
|
||||
<strong>{{ item.label }}</strong>
|
||||
<p class="muted">{{ item.detail }}</p>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</article>
|
||||
</section>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user