home: add minerva dashboard scaffold
This commit is contained in:
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]
|
||||
Reference in New Issue
Block a user