Initial commit: habit tracker PWA with Google OAuth, push notifications

FastAPI + SQLAlchemy/Alembic + MariaDB backend with Jinja2/htmx/Alpine
server-rendered frontend. Multi-user via Google OAuth, daily habit
tracking, monthly/weekly history views, Web Push reminders via
APScheduler, and PWA support (manifest, service worker, offline caching).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-16 18:06:17 +09:00
co-authored by Claude Sonnet 5
commit cee589bb3e
80 changed files with 4695 additions and 0 deletions
+92
View File
@@ -0,0 +1,92 @@
{% extends "base.html" %}
{% block title %}기록 · 습관 트래커{% endblock %}
{% block nav_history %}active{% endblock %}
{% block shell_class %} wide{% endblock %}
{% block content %}
<h1>기록</h1>
<div class="tabs">
<a href="/history?view=month" class="{{ 'active' if view == 'month' }}">월별</a>
<a href="/history?view=week" class="{{ 'active' if view == 'week' }}">주별</a>
</div>
{% if view == "month" %}
<div class="card">
<div style="display:flex; align-items:center; justify-content:space-between; margin-bottom: var(--space-2);">
<a href="/history?view=month&year={{ prev_year }}&month={{ prev_month }}" class="btn btn-secondary"></a>
<h2 style="margin:0;">{{ year }}년 {{ month }}월</h2>
<a href="/history?view=month&year={{ next_year }}&month={{ next_month }}" class="btn btn-secondary"></a>
</div>
<div style="text-align:center; margin-bottom: var(--space-2);">
<span class="badge">이번 달 완료율 {{ completion_rate }}%</span>
</div>
<div class="calendar-grid calendar-grid-header">
{% for wd in ["일", "월", "화", "수", "목", "금", "토"] %}
<div class="calendar-weekday">{{ wd }}</div>
{% endfor %}
</div>
{% for week in weeks %}
<div class="calendar-grid">
{% for d in week %}
{% set summary = summary_map.get(d) %}
<div
class="calendar-cell{{ '' if d.month == month else ' muted' }}"
style="background: rgba(var(--color-accent-rgb), {{ heatmap_opacity(summary.checked_count, summary.scheduled_count) if summary else 0 }});"
>
<span class="calendar-date">{{ d.day }}</span>
{% if summary and summary.scheduled_count %}
<span class="calendar-ratio">{{ summary.checked_count }}/{{ summary.scheduled_count }}</span>
{% endif %}
</div>
{% endfor %}
</div>
{% endfor %}
</div>
{% else %}
<div class="card" style="overflow-x:auto;">
<div style="display:flex; align-items:center; justify-content:space-between; margin-bottom: var(--space-2);">
<a href="/history?view=week&start={{ prev_week }}" class="btn btn-secondary"></a>
<h2 style="margin:0;">{{ week_start.strftime("%Y.%m.%d") }} - {{ week_end.strftime("%m.%d") }}</h2>
<a href="/history?view=week&start={{ next_week }}" class="btn btn-secondary"></a>
</div>
{% if rows %}
<table class="week-matrix">
<thead>
<tr>
<th>습관</th>
{% for wd in ["일", "월", "화", "수", "목", "금", "토"] %}
<th>{{ wd }}</th>
{% endfor %}
<th>완료율</th>
</tr>
</thead>
<tbody>
{% for row in rows %}
<tr>
<td>
{{ row.name }}
<span class="badge">{{ "형성" if row.habit_type == "build" else "중단" }}</span>
</td>
{% for iso_date, checked in row.checks.items() %}
<td class="week-matrix-cell">
{% if checked is none %}<span class="wm-dash"></span>
{% elif checked %}<span class="wm-check">&#10003;</span>
{% else %}<span class="wm-empty"></span>
{% endif %}
</td>
{% endfor %}
<td class="week-matrix-cell">{{ row.completion_rate }}%</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<div class="empty-state">진행 중인 습관이 없어요.</div>
{% endif %}
</div>
{% endif %}
{% endblock %}