📄 dev_server.py 8,351 bytes Apr 29, 2026 📋 Raw

"""
HoffDesk Dashboard — Dev Server
Serves the static dashboard with mock data for Sprint 1 development.
Run: python dev_server.py
"""
from fastapi import FastAPI
from fastapi.responses import HTMLResponse, FileResponse
from fastapi.staticfiles import StaticFiles
from pathlib import Path
from datetime import datetime, timezone, timedelta
import json, random

app = FastAPI()

BASE = Path(file).parent
CST = timezone(timedelta(hours=-5))

--- Mock Data Generator ---

def mock_today():
now = datetime.now(CST)
return {
"timestamp": now.isoformat(),
"calendar": {
"status": "ok",
"events_count": 3,
"events": [
{
"uid": "mock-001",
"summary": "Harper's Soccer Practice",
"start": now.replace(hour=16, minute=0, second=0).isoformat(),
"end": now.replace(hour=17, minute=0, second=0).isoformat(),
"is_all_day": False,
"location": "Green Bay Community Center",
"description": None,
"attendees": ["Harper"]
},
{
"uid": "mock-002",
"summary": "Dentist — Sullivan",
"start": now.replace(hour=10, minute=30, second=0).isoformat(),
"end": now.replace(hour=11, minute=15, second=0).isoformat(),
"is_all_day": False,
"location": "Dr. Patel, 123 Main St",
"description": "Regular checkup",
"attendees": None
},
{
"uid": "mock-003",
"summary": "Date Night 🍷",
"start": now.replace(hour=19, minute=0, second=0).isoformat(),
"end": now.replace(hour=21, minute=30, second=0).isoformat(),
"is_all_day": False,
"location": "Kōv — Ashwaubenon",
"description": None,
"attendees": ["Matt", "Aundrea"]
}
]
},
"weather": {
"status": "ok",
"location": {
"name": "Green Bay, WI",
"lat": 44.5133,
"lon": -88.0133
},
"current": {
"temperature_f": 52,
"feels_like_f": 49,
"condition": "Partly cloudy",
"humidity_pct": 62,
"wind_mph": 10,
"updated_at": now.isoformat()
},
"forecast": [
{
"hour": f"{(now.hour + i) % 24:02d}:00",
"temperature_f": 52 - i + random.randint(-2, 2),
"condition": ["Partly cloudy", "Cloudy", "Clear", "Overcast"][i % 4],
"precipitation_chance_pct": max(0, 15 - i * 3 + random.randint(0, 10))
}
for i in range(1, 7)
]
},
"health": {
"overall_status": "healthy",
"components": {
"openclaw_gateway": {"status": "ok", "last_check": now.isoformat(), "latency_ms": 8},
"radicale_caldav": {"status": "ok", "last_check": now.isoformat(), "latency_ms": 14},
"ollama_local": {"status": "ok", "last_check": now.isoformat(), "latency_ms": 45},
"cloudflare_tunnel": {"status": "ok", "last_check": now.isoformat(), "latency_ms": 22}
},
"disk_usage": {
"total_gb": 476.9,
"used_gb": 142.3,
"free_gb": 334.6,
"usage_pct": 29.8
}
},
"meta": {
"cache_ttl_seconds": 60,
"data_freshness": {
"calendar_seconds_ago": 12,
"weather_seconds_ago": 180
}
}
}

--- Routes ---

@app.get("/", response_class=HTMLResponse)
async def dashboard():
return (BASE / "templates" / "index.html").read_text()

@app.get("/api/today")
async def api_today():
return mock_today()

@app.get("/api/calendar/upcoming")
async def api_calendar(hours: int = 24):
data = mock_today()
return {"status": "ok", "events": data["calendar"]["events"]}

@app.get("/api/weather")
async def api_weather():
data = mock_today()
return {"status": "ok", "data": data["weather"]}

@app.get("/api/health")
async def api_health():
data = mock_today()
return {"status": "ok", "overall": data["health"]}

@app.get("/healthz")
async def healthz():
return "ok"

--- API Endpoints for Events ---

@app.get("/api/events-list", response_class=HTMLResponse)
async def api_events_list():
"""Return list view HTML fragment for events card."""
data = mock_today()
events = data["calendar"]["events"]

if not events:
    return '<p class="text-body-sm text-text-tertiary">No upcoming events</p>'

html = '<div class="event-list">'
for ev in events:
    start = datetime.fromisoformat(ev["start"])
    end = datetime.fromisoformat(ev["end"])
    time_str = f"{start.strftime('%-I:%M %p')} – {end.strftime('%-I:%M %p')}"

    html += f'''
    <div class="event-item">
      <div class="event-time">{time_str}</div>
      <div class="event-details">
        <div class="event-summary">{ev["summary"]}</div>
        {f'<div class="event-location">{ev["location"]}</div>' if ev.get("location") else ""}
      </div>
    </div>'''
html += '</div>'
return html

@app.get("/api/events-week", response_class=HTMLResponse)
async def api_events_week(offset: int = 0):
"""Return week view HTML fragment for events card."""
now = datetime.now(CST)

# Calculate week range based on offset (weeks)
start_of_week = now - timedelta(days=now.weekday()) + timedelta(weeks=offset)
days = []

# Mock events for the week
mock_week_events = {
    0: [{"title": "Soccer Practice", "time": "4:00 PM", "member": "harper", "all_day": False}],
    1: [{"title": "Dentist", "time": "10:30 AM", "member": "sullivan", "all_day": False}],
    2: [],
    3: [{"title": "School Play", "time": "6:00 PM", "member": "harper", "all_day": False}],
    4: [{"title": "Date Night", "time": "7:00 PM", "member": "matt", "all_day": False}],
    5: [{"title": "Grocery Shopping", "time": "9:00 AM", "member": "aundrea", "all_day": False}],
    6: [{"title": "Family Brunch", "time": "10:00 AM", "member": None, "all_day": False}],
}

day_names = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']

html = '''
<div class="week-view">
  <div class="week-nav">
    <button class="week-nav-btn" hx-get="/api/events-week?offset={}" hx-target="#events-content" hx-swap="innerHTML">←</button>
    <span class="week-range">This Week</span>
    <button class="week-nav-btn" hx-get="/api/events-week?offset={}" hx-target="#events-content" hx-swap="innerHTML">→</button>
  </div>
  <div class="week-grid">
'''.format(offset - 1, offset + 1)

for i in range(7):
    day_date = start_of_week + timedelta(days=i)
    day_num = day_date.day
    is_today = day_date.date() == now.date()
    events = mock_week_events.get(i, [])

    html += f'''
    <div class="day-column{' today' if is_today else ''}">
      <div class="day-header">
        <div class="day-name">{day_names[i]}</div>
        <div class="day-number">{day_num}</div>
      </div>
      <div class="day-events">
    '''

    for ev in events:
        member_class = ev.get("member") if ev.get("member") else ""
        html += f'''
        <div class="week-event {member_class}">
          <div class="event-title">{ev["title"]}</div>
          <div class="event-time">{ev["time"]}</div>
        </div>
        '''

    html += '</div></div>'

html += '''
  </div>
</div>
'''
return html

Static files

app.mount("/static", StaticFiles(directory=str(BASE / "static")), name="static")

if name == "main":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8080)