"""
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)