feat: Implement Minimum Viable Calm (MVC) feature and initial tests

This commit is contained in:
AlexanderWhitestone
2026-03-02 11:46:40 -05:00
parent 62ef1120a4
commit d080e67faf
20 changed files with 1389 additions and 30 deletions

0
src/__init__.py Normal file
View File

View File

@@ -21,35 +21,36 @@ from fastapi.responses import HTMLResponse
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
from config import settings
from dashboard.routes.agents import router as agents_router
from dashboard.routes.health import router as health_router
from dashboard.routes.swarm import router as swarm_router
from dashboard.routes.marketplace import router as marketplace_router
from dashboard.routes.voice import router as voice_router
from dashboard.routes.mobile import router as mobile_router
from dashboard.routes.briefing import router as briefing_router
from dashboard.routes.telegram import router as telegram_router
from dashboard.routes.tools import router as tools_router
from dashboard.routes.spark import router as spark_router
from dashboard.routes.creative import router as creative_router
from dashboard.routes.discord import router as discord_router
from dashboard.routes.events import router as events_router
from dashboard.routes.ledger import router as ledger_router
from dashboard.routes.memory import router as memory_router
from dashboard.routes.router import router as router_status_router
from dashboard.routes.upgrades import router as upgrades_router
from dashboard.routes.tasks import router as tasks_router
from dashboard.routes.scripture import router as scripture_router
from dashboard.routes.self_coding import router as self_coding_router
from dashboard.routes.self_coding import self_modify_router
from dashboard.routes.hands import router as hands_router
from dashboard.routes.grok import router as grok_router
from dashboard.routes.models import router as models_router
from dashboard.routes.models import api_router as models_api_router
from dashboard.routes.chat_api import router as chat_api_router
from dashboard.routes.thinking import router as thinking_router
from dashboard.routes.bugs import router as bugs_router
from src.config import settings
from src.dashboard.routes.agents import router as agents_router
from src.dashboard.routes.health import router as health_router
from src.dashboard.routes.swarm import router as swarm_router
from src.dashboard.routes.marketplace import router as marketplace_router
from src.dashboard.routes.voice import router as voice_router
from src.dashboard.routes.mobile import router as mobile_router
from src.dashboard.routes.briefing import router as briefing_router
from src.dashboard.routes.telegram import router as telegram_router
from src.dashboard.routes.tools import router as tools_router
from src.dashboard.routes.spark import router as spark_router
from src.dashboard.routes.creative import router as creative_router
from src.dashboard.routes.discord import router as discord_router
from src.dashboard.routes.events import router as events_router
from src.dashboard.routes.ledger import router as ledger_router
from src.dashboard.routes.memory import router as memory_router
from src.dashboard.routes.router import router as router_status_router
from src.dashboard.routes.upgrades import router as upgrades_router
from src.dashboard.routes.tasks import router as tasks_router
from src.dashboard.routes.scripture import router as scripture_router
from src.dashboard.routes.self_coding import router as self_coding_router
from src.dashboard.routes.self_coding import self_modify_router
from src.dashboard.routes.hands import router as hands_router
from src.dashboard.routes.grok import router as grok_router
from src.dashboard.routes.models import router as models_router
from src.dashboard.routes.models import api_router as models_api_router
from src.dashboard.routes.chat_api import router as chat_api_router
from src.dashboard.routes.thinking import router as thinking_router
from src.dashboard.routes.bugs import router as bugs_router
from src.dashboard.routes.calm import router as calm_router
from infrastructure.router.api import router as cascade_router
@@ -682,6 +683,7 @@ app.include_router(models_api_router)
app.include_router(chat_api_router)
app.include_router(thinking_router)
app.include_router(bugs_router)
app.include_router(calm_router)
app.include_router(cascade_router)

View File

@@ -0,0 +1,60 @@
from datetime import datetime, date
from enum import Enum as PyEnum
from sqlalchemy import (
Column, Integer, String, DateTime, Boolean, Enum as SQLEnum,
Date, ForeignKey, Index, JSON
)
from sqlalchemy.orm import relationship
from .database import Base # Assuming a shared Base in models/database.py
class TaskState(str, PyEnum):
LATER = "LATER"
NEXT = "NEXT"
NOW = "NOW"
DONE = "DONE"
DEFERRED = "DEFERRED" # Task pushed to tomorrow
class TaskCertainty(str, PyEnum):
FUZZY = "FUZZY" # An intention without a time
SOFT = "SOFT" # A flexible task with a time
HARD = "HARD" # A fixed meeting/appointment
class Task(Base):
__tablename__ = "tasks"
id = Column(Integer, primary_key=True, index=True)
title = Column(String(255), nullable=False)
description = Column(String(1000), nullable=True)
state = Column(SQLEnum(TaskState), default=TaskState.LATER, nullable=False, index=True)
certainty = Column(SQLEnum(TaskCertainty), default=TaskCertainty.SOFT, nullable=False)
is_mit = Column(Boolean, default=False, nullable=False) # 1-3 per day
sort_order = Column(Integer, default=0, nullable=False)
# Time tracking
started_at = Column(DateTime, nullable=True)
completed_at = Column(DateTime, nullable=True)
deferred_at = Column(DateTime, nullable=True)
# Timestamps
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
__table_args__ = (Index('ix_task_state_order', 'state', 'sort_order'),)
class JournalEntry(Base):
__tablename__ = "journal_entries"
id = Column(Integer, primary_key=True, index=True)
entry_date = Column(Date, unique=True, nullable=False, index=True, default=date.today)
# Relationships to the 1-3 MITs for the day
mit_task_ids = Column(JSON, nullable=True)
evening_reflection = Column(String(2000), nullable=True)
gratitude = Column(String(500), nullable=True)
energy_level = Column(Integer, nullable=True) # User-reported, 1-10
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)

View File

@@ -0,0 +1,20 @@
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker, Session
SQLALCHEMY_DATABASE_URL = "sqlite:///./data/timmy_calm.db"
engine = create_engine(
SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False}
)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()

View File

@@ -7,7 +7,7 @@ from fastapi import APIRouter, Form, Request
from fastapi.responses import HTMLResponse
from fastapi.templating import Jinja2Templates
from timmy.session import chat as timmy_chat
from src.timmy.session import chat as timmy_chat
from dashboard.store import message_log
logger = logging.getLogger(__name__)

View File

@@ -0,0 +1,380 @@
import logging
from datetime import date, datetime
from typing import List, Optional
from fastapi import APIRouter, Depends, Form, HTTPException, Request
from fastapi.responses import HTMLResponse
from fastapi.templating import Jinja2Templates
from sqlalchemy.orm import Session
from src.dashboard.models.calm import JournalEntry, Task, TaskCertainty, TaskState
from src.dashboard.models.database import SessionLocal, engine, get_db
# Create database tables (if not already created by Alembic)
# This is typically handled by Alembic migrations in a production environment
# from src.dashboard.models.database import Base
# Base.metadata.create_all(bind=engine)
logger = logging.getLogger(__name__)
router = APIRouter(tags=["calm"])
templates = Jinja2Templates(directory="src/dashboard/templates")
# Helper functions for state machine logic
def get_now_task(db: Session) -> Optional[Task]:
return db.query(Task).filter(Task.state == TaskState.NOW).first()
def get_next_task(db: Session) -> Optional[Task]:
return db.query(Task).filter(Task.state == TaskState.NEXT).first()
def get_later_tasks(db: Session) -> List[Task]:
return db.query(Task).filter(Task.state == TaskState.LATER).order_by(Task.is_mit.desc(), Task.sort_order).all()
def promote_tasks(db: Session):
# Ensure only one NOW task exists. If multiple, demote extras to NEXT.
now_tasks = db.query(Task).filter(Task.state == TaskState.NOW).all()
if len(now_tasks) > 1:
# Keep the one with highest priority/sort_order, demote others to NEXT
now_tasks.sort(key=lambda t: (t.is_mit, t.sort_order), reverse=True)
for task_to_demote in now_tasks[1:]:
task_to_demote.state = TaskState.NEXT
db.add(task_to_demote)
db.flush() # Make changes visible
# If no NOW task, promote NEXT to NOW
current_now = db.query(Task).filter(Task.state == TaskState.NOW).first()
if not current_now:
next_task = db.query(Task).filter(Task.state == TaskState.NEXT).first()
if next_task:
next_task.state = TaskState.NOW
db.add(next_task)
db.flush() # Make changes visible
# If no NEXT task, promote highest priority LATER to NEXT
current_next = db.query(Task).filter(Task.state == TaskState.NEXT).first()
if not current_next:
later_tasks = db.query(Task).filter(Task.state == TaskState.LATER).order_by(Task.is_mit.desc(), Task.sort_order).all()
if later_tasks:
later_tasks[0].state = TaskState.NEXT
db.add(later_tasks[0])
db.commit()
# Endpoints
@router.get("/calm", response_class=HTMLResponse)
async def get_calm_view(request: Request, db: Session = Depends(get_db)):
now_task = get_now_task(db)
next_task = get_next_task(db)
later_tasks_count = len(get_later_tasks(db))
return templates.TemplateResponse(
"calm/calm_view.html",
{
"request": request,
"now_task": now_task,
"next_task": next_task,
"later_tasks_count": later_tasks_count,
},
)
@router.get("/calm/ritual/morning", response_class=HTMLResponse)
async def get_morning_ritual_form(request: Request):
return templates.TemplateResponse(
"calm/morning_ritual_form.html", {"request": request}
)
@router.post("/calm/ritual/morning", response_class=HTMLResponse)
async def post_morning_ritual(
request: Request,
db: Session = Depends(get_db),
mit1_title: str = Form(None),
mit2_title: str = Form(None),
mit3_title: str = Form(None),
other_tasks: str = Form(""),
):
# Create Journal Entry
mit_task_ids = []
journal_entry = JournalEntry(entry_date=date.today())
db.add(journal_entry)
db.commit()
db.refresh(journal_entry)
# Create MIT tasks
for mit_title in [mit1_title, mit2_title, mit3_title]:
if mit_title:
task = Task(
title=mit_title,
is_mit=True,
state=TaskState.LATER, # Initially LATER, will be promoted
certainty=TaskCertainty.SOFT,
)
db.add(task)
db.commit()
db.refresh(task)
mit_task_ids.append(task.id)
journal_entry.mit_task_ids = mit_task_ids
db.add(journal_entry)
# Create other tasks
for task_title in other_tasks.split('\n'):
task_title = task_title.strip()
if task_title:
task = Task(
title=task_title,
state=TaskState.LATER,
certainty=TaskCertainty.FUZZY,
)
db.add(task)
db.commit()
# Set initial NOW/NEXT states
# Set initial NOW/NEXT states after all tasks are created
if not get_now_task(db) and not get_next_task(db):
later_tasks = db.query(Task).filter(Task.state == TaskState.LATER).order_by(Task.is_mit.desc(), Task.sort_order).all()
if later_tasks:
# Set the highest priority LATER task to NOW
later_tasks[0].state = TaskState.NOW
db.add(later_tasks[0])
db.flush() # Flush to make the change visible for the next query
# Set the next highest priority LATER task to NEXT
if len(later_tasks) > 1:
later_tasks[1].state = TaskState.NEXT
db.add(later_tasks[1])
db.commit() # Commit changes after initial NOW/NEXT setup
return templates.TemplateResponse(
"calm/calm_view.html",
{
"request": request,
"now_task": get_now_task(db),
"next_task": get_next_task(db),
"later_tasks_count": len(get_later_tasks(db)),
},
)
@router.get("/calm/ritual/evening", response_class=HTMLResponse)
async def get_evening_ritual_form(request: Request, db: Session = Depends(get_db)):
journal_entry = db.query(JournalEntry).filter(JournalEntry.entry_date == date.today()).first()
if not journal_entry:
raise HTTPException(status_code=404, detail="No journal entry for today")
return templates.TemplateResponse(
"calm/evening_ritual_form.html", {"request": request, "journal_entry": journal_entry}
)
@router.post("/calm/ritual/evening", response_class=HTMLResponse)
async def post_evening_ritual(
request: Request,
db: Session = Depends(get_db),
evening_reflection: str = Form(None),
gratitude: str = Form(None),
energy_level: int = Form(None),
):
journal_entry = db.query(JournalEntry).filter(JournalEntry.entry_date == date.today()).first()
if not journal_entry:
raise HTTPException(status_code=404, detail="No journal entry for today")
journal_entry.evening_reflection = evening_reflection
journal_entry.gratitude = gratitude
journal_entry.energy_level = energy_level
db.add(journal_entry)
# Archive any remaining active tasks
active_tasks = db.query(Task).filter(Task.state.in_([TaskState.NOW, TaskState.NEXT, TaskState.LATER])).all()
for task in active_tasks:
task.state = TaskState.DEFERRED # Or DONE, depending on desired archiving logic
task.deferred_at = datetime.utcnow()
db.add(task)
db.commit()
return templates.TemplateResponse(
"calm/evening_ritual_complete.html", {"request": request}
)
@router.post("/calm/tasks", response_class=HTMLResponse)
async def create_new_task(
request: Request,
db: Session = Depends(get_db),
title: str = Form(...),
description: Optional[str] = Form(None),
is_mit: bool = Form(False),
certainty: TaskCertainty = Form(TaskCertainty.SOFT),
):
task = Task(
title=title,
description=description,
is_mit=is_mit,
certainty=certainty,
state=TaskState.LATER,
)
db.add(task)
db.commit()
db.refresh(task)
# After creating a new task, we might need to re-evaluate NOW/NEXT/LATER, but for simplicity
# and given the spec, new tasks go to LATER. Promotion happens on completion/deferral.
return templates.TemplateResponse(
"calm/partials/later_count.html",
{"request": request, "later_tasks_count": len(get_later_tasks(db))},
)
@router.post("/calm/tasks/{task_id}/start", response_class=HTMLResponse)
async def start_task(
request: Request,
task_id: int,
db: Session = Depends(get_db),
):
current_now_task = get_now_task(db)
if current_now_task and current_now_task.id != task_id:
current_now_task.state = TaskState.NEXT # Demote current NOW to NEXT
db.add(current_now_task)
task = db.query(Task).filter(Task.id == task_id).first()
if not task:
raise HTTPException(status_code=404, detail="Task not found")
task.state = TaskState.NOW
task.started_at = datetime.utcnow()
db.add(task)
db.commit()
# Re-evaluate NEXT from LATER if needed
promote_tasks(db)
return templates.TemplateResponse(
"calm/partials/now_next_later.html",
{
"request": request,
"now_task": get_now_task(db),
"next_task": get_next_task(db),
"later_tasks_count": len(get_later_tasks(db)),
},
)
@router.post("/calm/tasks/{task_id}/complete", response_class=HTMLResponse)
async def complete_task(
request: Request,
task_id: int,
db: Session = Depends(get_db),
):
task = db.query(Task).filter(Task.id == task_id).first()
if not task:
raise HTTPException(status_code=404, detail="Task not found")
task.state = TaskState.DONE
task.completed_at = datetime.utcnow()
db.add(task)
db.commit()
promote_tasks(db)
return templates.TemplateResponse(
"calm/partials/now_next_later.html",
{
"request": request,
"now_task": get_now_task(db),
"next_task": get_next_task(db),
"later_tasks_count": len(get_later_tasks(db)),
},
)
@router.post("/calm/tasks/{task_id}/defer", response_class=HTMLResponse)
async def defer_task(
request: Request,
task_id: int,
db: Session = Depends(get_db),
):
task = db.query(Task).filter(Task.id == task_id).first()
if not task:
raise HTTPException(status_code=404, detail="Task not found")
task.state = TaskState.DEFERRED
task.deferred_at = datetime.utcnow()
db.add(task)
db.commit()
promote_tasks(db)
return templates.TemplateResponse(
"calm/partials/now_next_later.html",
{
"request": request,
"now_task": get_now_task(db),
"next_task": get_next_task(db),
"later_tasks_count": len(get_later_tasks(db)),
},
)
@router.get("/calm/partials/later_tasks_list", response_class=HTMLResponse)
async def get_later_tasks_list(request: Request, db: Session = Depends(get_db)):
later_tasks = get_later_tasks(db)
return templates.TemplateResponse(
"calm/partials/later_tasks_list.html",
{"request": request, "later_tasks": later_tasks},
)
@router.post("/calm/tasks/reorder", response_class=HTMLResponse)
async def reorder_tasks(
request: Request,
db: Session = Depends(get_db),
# Expecting a comma-separated string of task IDs in new order
later_task_ids: str = Form(""),
next_task_id: Optional[int] = Form(None),
):
# Reorder LATER tasks
if later_task_ids:
ids_in_order = [int(x.strip()) for x in later_task_ids.split(',') if x.strip()]
for index, task_id in enumerate(ids_in_order):
task = db.query(Task).filter(Task.id == task_id).first()
if task and task.state == TaskState.LATER:
task.sort_order = index
db.add(task)
# Handle NEXT task if it's part of the reorder (e.g., moved from LATER to NEXT explicitly)
if next_task_id:
task = db.query(Task).filter(Task.id == next_task_id).first()
if task and task.state == TaskState.LATER: # Only if it was a LATER task being promoted manually
# Demote current NEXT to LATER
current_next = get_next_task(db)
if current_next:
current_next.state = TaskState.LATER
current_next.sort_order = len(get_later_tasks(db)) # Add to end of later
db.add(current_next)
task.state = TaskState.NEXT
task.sort_order = 0 # NEXT tasks don't really need sort_order, but for consistency
db.add(task)
db.commit()
# Re-render the relevant parts of the UI
return templates.TemplateResponse(
"calm/partials/now_next_later.html",
{
"request": request,
"now_task": get_now_task(db),
"next_task": get_next_task(db),
"later_tasks_count": len(get_later_tasks(db)),
},
)
# Include this router in the main FastAPI app
# In src/dashboard/app.py, add:
# from dashboard.routes.calm import router as calm_router
# app.include_router(calm_router)

View File

@@ -27,6 +27,7 @@
<!-- Desktop nav -->
<div class="mc-header-right mc-desktop-nav">
<a href="/calm" class="mc-test-link">CALM</a>
<a href="/tasks" class="mc-test-link">TASKS</a>
<a href="/briefing" class="mc-test-link">BRIEFING</a>
<a href="/thinking" class="mc-test-link mc-link-thinking">THINKING</a>
@@ -73,6 +74,7 @@
</div>
<a href="/" class="mc-mobile-link">HOME</a>
<div class="mc-mobile-section-label">CORE</div>
<a href="/calm" class="mc-mobile-link">CALM</a>
<a href="/tasks" class="mc-mobile-link">TASKS</a>
<a href="/briefing" class="mc-mobile-link">BRIEFING</a>
<a href="/thinking" class="mc-mobile-link">THINKING</a>

View File

@@ -0,0 +1,127 @@
{% extends "base.html" %}
{% block title %}Timmy Calm{% endblock %}
{% block extra_styles %}
<style>
.calm-container { max-width: 600px; margin: 0 auto; padding: 20px; }
.calm-header { text-align: center; margin-bottom: 30px; }
.calm-title { font-size: 2.5rem; font-weight: 700; color: var(--text-bright); letter-spacing: 0.05em; }
.calm-subtitle { font-size: 1.1rem; color: var(--text-dim); margin-top: 5px; }
.task-card {
background: var(--bg-secondary);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
padding: 25px;
margin-bottom: 20px;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
transition: all 0.3s ease;
}
.task-card:hover { transform: translateY(-3px); box-shadow: 0 6px 20px rgba(0, 0, 0, 0.3); }
.now-card {
background: linear-gradient(135deg, var(--bg-secondary) 0%, rgba(124, 58, 237, 0.1) 100%);
border-color: rgba(124, 58, 237, 0.4);
}
.now-card .task-title { font-size: 2.2rem; font-weight: 800; color: var(--green); margin-bottom: 15px; }
.now-card .task-description { font-size: 1.1rem; color: var(--text); line-height: 1.6; margin-bottom: 20px; }
.now-card .task-actions { display: flex; gap: 15px; justify-content: center; }
.now-card .task-btn { padding: 12px 25px; font-size: 1rem; font-weight: 700; border-radius: var(--radius-md); cursor: pointer; transition: all 0.2s ease; }
.now-card .task-btn-complete { background: var(--green); color: var(--bg-secondary); border: none; }
.now-card .task-btn-complete:hover { background: var(--green-dark); }
.now-card .task-btn-defer { background: var(--bg-tertiary); color: var(--text); border: 1px solid var(--border); }
.now-card .task-btn-defer:hover { background: var(--bg-tertiary-hover); }
.next-card {
background: var(--bg-tertiary);
border-color: var(--border);
padding: 15px 20px;
}
.next-card .task-title { font-size: 1.3rem; font-weight: 600; color: var(--info); margin-bottom: 5px; }
.next-card .task-description { font-size: 0.9rem; color: var(--text-dim); max-height: 40px; overflow: hidden; }
.later-section {
background: var(--bg-tertiary);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
margin-top: 20px;
}
.later-summary { padding: 15px 20px; cursor: pointer; display: flex; justify-content: space-between; align-items: center; font-size: 1.1rem; font-weight: 600; color: var(--text-bright); }
.later-summary:hover { background: var(--bg-tertiary-hover); border-radius: var(--radius-lg); }
.later-content { padding: 10px 20px 20px; border-top: 1px solid var(--border); }
.later-task-item { padding: 8px 0; border-bottom: 1px dashed var(--border); display: flex; justify-content: space-between; align-items: center; }
.later-task-item:last-child { border-bottom: none; }
.later-task-title { font-size: 0.95rem; color: var(--text); }
.later-task-actions .task-btn { font-size: 0.75rem; padding: 5px 10px; }
.empty-state { text-align: center; color: var(--text-dim); padding: 40px 20px; font-size: 1rem; }
.ritual-btn { display: block; width: fit-content; margin: 20px auto; padding: 10px 20px; background: var(--purple); color: white; border-radius: var(--radius-md); text-decoration: none; font-weight: 600; }
.ritual-btn:hover { opacity: 0.9; }
/* Inter font - assuming it's available or linked in base.html */
body { font-family: 'Inter', sans-serif; }
</style>
{% endblock %}
{% block content %}
<div class="calm-container py-3">
<div class="calm-header">
<h1 class="calm-title">Timmy Calm</h1>
<p class="calm-subtitle">Your focused attention stack</p>
</div>
<div id="now-next-later-stack" hx-trigger="load, taskPromoted from:body" hx-get="/calm/partials/now_next_later" hx-swap="outerHTML">
{% include "calm/partials/now_next_later.html" %}
</div>
{% if not now_task and not next_task and later_tasks_count == 0 %}
<div class="empty-state">
<p>It looks like you have no tasks. Start your day with a morning ritual!</p>
<a href="/calm/ritual/morning" class="ritual-btn">Start Morning Ritual</a>
</div>
{% endif %}
<div class="add-task-section" style="text-align: center; margin-top: 30px;">
<button class="task-btn task-btn-defer" onclick="openAddTaskModal()">+ Add New Task</button>
</div>
<!-- Add Task Modal (simple example, can be expanded) -->
<div id="add-task-modal" class="task-modal-overlay">
<div class="task-modal">
<h3>Add New Task</h3>
<form hx-post="/calm/tasks" hx-target="#later-count-container" hx-swap="outerHTML" hx-on::after-request="closeAddTaskModal()">
<label>Title</label>
<input type="text" name="title" required>
<label>Description (optional)</label>
<textarea name="description"></textarea>
<label>
<input type="checkbox" name="is_mit" value="true"> Is this a Most Important Task (MIT)?
</label>
<div class="task-modal-actions">
<button type="button" class="task-btn task-btn-cancel" onclick="closeAddTaskModal()">Cancel</button>
<button type="submit" class="task-btn task-btn-complete">Add Task</button>
</div>
</form>
</div>
</div>
</div>
<script>
function openAddTaskModal() {
document.getElementById('add-task-modal').classList.add('open');
}
function closeAddTaskModal() {
document.getElementById('add-task-modal').classList.remove('open');
}
document.getElementById('add-task-modal').addEventListener('click', function(e) {
if (e.target === this) closeAddTaskModal();
});
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') closeAddTaskModal();
});
</script>
{% endblock %}

View File

@@ -0,0 +1,24 @@
{% extends "base.html" %}
{% block title %}Evening Ritual Complete - Timmy Calm{% endblock %}
{% block extra_styles %}
<style>
.ritual-container { max-width: 700px; margin: 0 auto; padding: 30px; background: var(--bg-secondary); border-radius: var(--radius-lg); box-shadow: 0 5px 20px rgba(0,0,0,0.2); text-align: center; }
.ritual-title { font-size: 2rem; font-weight: 700; color: var(--green); margin-bottom: 20px; }
.ritual-message { font-size: 1.1rem; color: var(--text); line-height: 1.6; margin-bottom: 30px; }
.ritual-btn { display: inline-block; padding: 12px 25px; font-size: 1rem; font-weight: 700; border-radius: var(--radius-md); cursor: pointer; transition: all 0.2s ease; border: none; background: var(--purple); color: white; text-decoration: none; }
.ritual-btn:hover { opacity: 0.9; }
</style>
{% endblock %}
{% block content %}
<div class="ritual-container">
<h1 class="ritual-title">✓ Evening Ritual Complete</h1>
<p class="ritual-message">
You've reflected on your day and archived your tasks. Rest well — tomorrow is a fresh start.
</p>
<a href="/calm" class="ritual-btn">Return to Calm</a>
</div>
{% endblock %}

View File

@@ -0,0 +1,54 @@
{% extends "base.html" %}
{% block title %}Evening Ritual - Timmy Calm{% endblock %}
{% block extra_styles %}
<style>
.ritual-container { max-width: 700px; margin: 0 auto; padding: 30px; background: var(--bg-secondary); border-radius: var(--radius-lg); box-shadow: 0 5px 20px rgba(0,0,0,0.2); }
.ritual-header { text-align: center; margin-bottom: 30px; }
.ritual-title { font-size: 2rem; font-weight: 700; color: var(--text-bright); margin-bottom: 10px; }
.ritual-subtitle { font-size: 1rem; color: var(--text-dim); line-height: 1.5; }
.form-group { margin-bottom: 20px; }
.form-group label { display: block; font-size: 0.9rem; color: var(--text-dim); margin-bottom: 8px; font-weight: 600; }
.form-group input[type="text"], .form-group textarea, .form-group input[type="number"] { width: 100%; padding: 12px; border: 1px solid var(--border); border-radius: var(--radius-md); background: var(--bg-tertiary); color: var(--text); font-size: 1rem; }
.form-group textarea { min-height: 100px; resize: vertical; }
.form-group input[type="text"]:focus, .form-group textarea:focus, .form-group input[type="number"]:focus { border-color: var(--purple); outline: none; box-shadow: 0 0 0 2px rgba(124, 58, 237, 0.2); }
.form-actions { display: flex; justify-content: flex-end; margin-top: 30px; }
.form-actions button { padding: 12px 25px; font-size: 1rem; font-weight: 700; border-radius: var(--radius-md); cursor: pointer; transition: all 0.2s ease; border: none; }
.form-actions .btn-submit { background: var(--green); color: var(--bg-secondary); }
.form-actions .btn-submit:hover { background: var(--green-dark); }
</style>
{% endblock %}
{% block content %}
<div class="ritual-container">
<div class="ritual-header">
<h1 class="ritual-title">Good Evening, Timmy.</h1>
<p class="ritual-subtitle">Reflect on your day and prepare for tomorrow.</p>
</div>
<form hx-post="/calm/ritual/evening" hx-swap="outerHTML" hx-target="body">
<div class="form-group">
<label for="evening_reflection">Evening Reflection</label>
<textarea id="evening_reflection" name="evening_reflection" placeholder="What went well today? What could be improved?" rows="5">{{ journal_entry.evening_reflection if journal_entry else '' }}</textarea>
</div>
<div class="form-group">
<label for="gratitude">Gratitude</label>
<input type="text" id="gratitude" name="gratitude" placeholder="What are you grateful for today?" value="{{ journal_entry.gratitude if journal_entry else '' }}">
</div>
<div class="form-group">
<label for="energy_level">Energy Level (1-10)</label>
<input type="number" id="energy_level" name="energy_level" min="1" max="10" placeholder="e.g., 7" value="{{ journal_entry.energy_level if journal_entry else '' }}">
</div>
<div class="form-actions">
<button type="submit" class="btn-submit">Complete Evening Ritual</button>
</div>
</form>
</div>
{% endblock %}

View File

@@ -0,0 +1,66 @@
{% extends "base.html" %}
{% block title %}Morning Ritual - Timmy Calm{% endblock %}
{% block extra_styles %}
<style>
.ritual-container { max-width: 700px; margin: 0 auto; padding: 30px; background: var(--bg-secondary); border-radius: var(--radius-lg); box-shadow: 0 5px 20px rgba(0,0,0,0.2); }
.ritual-header { text-align: center; margin-bottom: 30px; }
.ritual-title { font-size: 2rem; font-weight: 700; color: var(--text-bright); margin-bottom: 10px; }
.ritual-subtitle { font-size: 1rem; color: var(--text-dim); line-height: 1.5; }
.form-group { margin-bottom: 20px; }
.form-group label { display: block; font-size: 0.9rem; color: var(--text-dim); margin-bottom: 8px; font-weight: 600; }
.form-group input[type="text"], .form-group textarea { width: 100%; padding: 12px; border: 1px solid var(--border); border-radius: var(--radius-md); background: var(--bg-tertiary); color: var(--text); font-size: 1rem; }
.form-group textarea { min-height: 100px; resize: vertical; }
.form-group input[type="text"]:focus, .form-group textarea:focus { border-color: var(--purple); outline: none; box-shadow: 0 0 0 2px rgba(124, 58, 237, 0.2); }
.mit-section { border: 1px dashed var(--border); padding: 20px; border-radius: var(--radius-md); margin-top: 25px; background: rgba(124, 58, 237, 0.05); }
.mit-section h4 { color: var(--purple); margin-top: 0; margin-bottom: 15px; font-size: 1.1rem; }
.form-actions { display: flex; justify-content: flex-end; margin-top: 30px; }
.form-actions button { padding: 12px 25px; font-size: 1rem; font-weight: 700; border-radius: var(--radius-md); cursor: pointer; transition: all 0.2s ease; border: none; }
.form-actions .btn-submit { background: var(--green); color: var(--bg-secondary); }
.form-actions .btn-submit:hover { background: var(--green-dark); }
</style>
{% endblock %}
{% block content %}
<div class="ritual-container">
<div class="ritual-header">
<h1 class="ritual-title">Good Morning, Timmy.</h1>
<p class="ritual-subtitle">Let's set your intentions for a calm and focused day.</p>
</div>
<form hx-post="/calm/ritual/morning" hx-swap="outerHTML" hx-target="body">
<div class="mit-section">
<h4>Your 1-3 Most Important Tasks (MITs) for today:</h4>
<div class="form-group">
<label for="mit1_title">MIT 1</label>
<input type="text" id="mit1_title" name="mit1_title" placeholder="e.g., Finish report draft">
</div>
<div class="form-group">
<label for="mit2_title">MIT 2 (optional)</label>
<input type="text" id="mit2_title" name="mit2_title" placeholder="e.g., Prepare for client meeting">
</div>
<div class="form-group">
<label for="mit3_title">MIT 3 (optional)</label>
<input type="text" id="mit3_title" name="mit3_title" placeholder="e.g., Review team's code">
</div>
</div>
<div class="form-group" style="margin-top: 30px;">
<label for="other_tasks">Other tasks or intentions (one per line)</label>
<textarea id="other_tasks" name="other_tasks" placeholder="e.g.,
Reply to emails
Schedule dentist appointment
Research new framework"></textarea>
</div>
<div class="form-actions">
<button type="submit" class="btn-submit">Start My Day</button>
</div>
</form>
</div>
{% endblock %}

View File

@@ -0,0 +1 @@
<span id="later-count-container">{{ later_tasks_count }}</span>

View File

@@ -0,0 +1,19 @@
{% if later_tasks %}
<form hx-post="/calm/tasks/reorder" hx-target="#now-next-later-stack" hx-swap="outerHTML">
<ul class="later-task-list">
{% for task in later_tasks %}
<li class="later-task-item">
<span class="later-task-title">{{ task.title }}</span>
<div class="later-task-actions">
<button type="submit" name="next_task_id" value="{{ task.id }}" class="task-btn task-btn-approve">Make Next</button>
</div>
</li>
{% endfor %}
</ul>
<!-- Hidden input to send all later task IDs for reordering, if drag-and-drop were implemented -->
<input type="hidden" name="later_task_ids" value="{% for task in later_tasks %}{{ task.id }}{% if not loop.last %},{% endif %}{% endfor %}">
</form>
{% else %}
<div class="empty-state">No tasks in Later.</div>
{% endif %}

View File

@@ -0,0 +1,50 @@
<div id="now-next-later-stack">
{% if now_task %}
<div class="task-card now-card">
<h2 class="task-title">{{ now_task.title }}</h2>
{% if now_task.description %}
<p class="task-description">{{ now_task.description }}</p>
{% endif %}
<div class="task-actions">
<button class="task-btn task-btn-complete"
hx-post="/calm/tasks/{{ now_task.id }}/complete"
hx-target="#now-next-later-stack"
hx-swap="outerHTML">
Complete
</button>
<button class="task-btn task-btn-defer"
hx-post="/calm/tasks/{{ now_task.id }}/defer"
hx-target="#now-next-later-stack"
hx-swap="outerHTML">
Defer
</button>
</div>
</div>
{% else %}
<div class="empty-state">
<p>No task is NOW. Time to pick one or start your morning ritual!</p>
</div>
{% endif %}
{% if next_task %}
<div class="task-card next-card">
<h3 class="task-title">Next: {{ next_task.title }}</h3>
{% if next_task.description %}
<p class="task-description">{{ next_task.description }}</p>
{% endif %}
</div>
{% endif %}
<div class="later-section">
<details hx-get="/calm/partials/later_tasks_list" hx-trigger="toggle" hx-target="#later-content" hx-swap="innerHTML">
<summary class="later-summary">
Later ({{ later_tasks_count }} items)
<span id="later-count-container"></span>
</summary>
<div id="later-content" class="later-content">
<!-- Later tasks will be loaded here via HTMX -->
</div>
</details>
</div>
</div>