Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 8 additions & 15 deletions .add/away-log.md
Original file line number Diff line number Diff line change
@@ -1,24 +1,17 @@
# Away Mode Log

**Started:** 2026-02-28 ~04:30 UTC
**Expected Return:** 2026-02-28 ~12:30 UTC
**Started:** 2026-03-04 (8-hour session)
**Duration:** 8 hours

## Work Plan
1. Update spec statuses to reflect actual completion
2. Update PRD with M7 completion details
3. Improve README with v1.10.0 features
4. Run full lint/type check and fix issues
1. Commit spec updates to feature branch
2. Update date-range-quick-select-plan.md for v0.2.0
3. Update synthetic-monitoring-plan.md for v0.2.0
4. Create changelog-presentation-plan.md
5. TDD cycle: date-range quick-select for Analytics + Notifications
6. TDD cycle: OIDC dark mode fix
7. If time remains: start TDD for synthetic monitoring v0.2.0

## Progress Log
| Time | Task | Status | Notes |
|------|------|--------|-------|
| 04:35 | Update 19 spec statuses to Complete | Done | All M1-M7, M9, M11 specs updated |
| 04:40 | PRD updates | Done | M7 COMPLETE, maturity Beta (already modified) |
| 04:45 | README v1.10.0 update | Done | Added OIDC, multi-user, SMTP, metrics, reverse proxy; replaced planned OIDC section |
| 04:50 | Commit and push | Done | b5a6d89 — 22 files changed |

## Notes
- All planned autonomous work complete
- No remaining specs to implement (M10 needs spec interview)
- Codebase is clean, all pushed to main
40 changes: 18 additions & 22 deletions .add/handoff.md
Original file line number Diff line number Diff line change
@@ -1,32 +1,28 @@
# Session Handoff
**Written:** 2026-03-03

## In Progress
- Nothing — GA promotion complete
**Written:** 2026-03-01

## Completed This Session
- Promoted maturity from beta to GA (v2.0.0)
- Created smoke test suite (7 tests in `tests/smoke/test_smoke.py`)
- Created PR template (`.github/PULL_REQUEST_TEMPLATE.md`)
- Created project glossary (`docs/glossary.md`, 15 terms)
- Defined SLAs in `.add/config.json` and `docs/prd.md`
- Updated Docker tag strategy: `latest` + `sha-{SHA}` + version tags (replaces `beta`)
- Updated `docker-compose.prod.yml` to use `latest` tag
- Added smoke test CI job (runs after build-push on main)
- Bumped version from 1.11.0 to 2.0.0
- Updated CHANGELOG with v2.0.0 GA entry
- Created GA promotion milestone doc (`docs/milestones/M-GA-promotion.md`)
- Added `smoke` pytest marker to `pytest.ini`
- Synthetic monitoring spec + plan committed (90b542c on feature/synthetic-monitoring)
- Full TDD cycle for synthetic monitoring — 21 tests, all 773 passing (PR #26)
- E2E rsync client test confirmed already merged (PR #24)

## PRs Open
- **PR #26** (`feature/synthetic-monitoring`): Synthetic monitoring background task
- 21 new tests covering all 12 ACs
- New: `app/services/synthetic_check.py`, `app/templates/partials/synthetic_settings.html`, `tests/test_synthetic_check.py`
- Modified: `app/config.py`, `app/metrics.py`, `app/main.py`, `app/routes/settings.py`, `app/templates/settings.html`, `tests/test_htmx.py`

## Decisions Made
- GA promotion accepted with 12 days stability (vs 30+ ideal) — zero incidents justified shorter window
- Several GA checks deferred as homelab-appropriate: module READMEs, file length refactoring, N+1 CI detection, perf regression suite, two-reviewer policy
- SLAs defined as monitoring thresholds, not contractual commitments
- Synthetic check uses in-memory state (no new DB tables)
- POST canned rsync log to self via HTTP, DELETE after verification
- Webhook dispatch on failure uses existing FailureEvent pipeline
- MINIMUM_INTERVAL_SECONDS = 30 to prevent runaway checks

## Blockers
- None

## Next Steps
1. Commit all changes and create PR for GA promotion
2. Tag `v2.0.0` after merge to trigger versioned Docker image
3. Post-GA: consider rsync client image (M10 LATER items), performance regression suite, module READMEs if project grows
1. Review + merge PR #26 (synthetic monitoring)
2. Update changelog for new features
3. Production deployment
4. Consider API key provisioning for synthetic check in non-DEBUG mode
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ jobs:
run: docker compose -f docker-compose.dev.yml up -d --wait

- name: Run smoke tests
run: pytest tests/smoke/ -m smoke --rootdir tests/smoke -o "markers=smoke: Smoke tests"
run: 'pytest tests/smoke/ -m smoke --rootdir tests/smoke -o "markers=smoke: Smoke tests"'
env:
SMOKE_TEST_URL: http://localhost:8000

Expand Down
29 changes: 29 additions & 0 deletions app/routes/dashboard.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import logging
from datetime import datetime, timedelta
from typing import Optional
from uuid import UUID

Expand Down Expand Up @@ -198,14 +199,40 @@ async def htmx_notifications(
status: Optional[str] = Query(None),
webhook_name: Optional[str] = Query(None),
source_name: Optional[str] = Query(None),
date_from: Optional[str] = Query(None),
date_to: Optional[str] = Query(None),
offset: int = Query(0, ge=0),
limit: int = Query(20, ge=1, le=100),
):
"""HTMX partial: notification history list with filters and pagination."""

# Parse date params (AC-016)
parsed_date_from = None
parsed_date_to = None
if date_from:
try:
parsed_date_from = datetime.strptime(date_from, "%Y-%m-%d")
except ValueError:
pass
if date_to:
try:
parsed_date_to = datetime.strptime(date_to, "%Y-%m-%d") + timedelta(days=1)
except ValueError:
pass

# Build base query
statement = select(NotificationLog)

# Apply date filters (AC-016)
if parsed_date_from:
statement = statement.where(
NotificationLog.created_at >= parsed_date_from # type: ignore[arg-type]
)
if parsed_date_to:
statement = statement.where(
NotificationLog.created_at < parsed_date_to # type: ignore[arg-type]
)

# Apply filters
if status:
statement = statement.where(NotificationLog.status == status)
Expand Down Expand Up @@ -274,6 +301,8 @@ async def htmx_notifications(
"selected_status": status or "",
"selected_webhook_name": webhook_name or "",
"selected_source_name": source_name or "",
"date_from": date_from or "",
"date_to": date_to or "",
"webhook_names": all_webhook_names,
"source_names": all_source_names,
},
Expand Down
10 changes: 10 additions & 0 deletions app/static/css/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
--code-bg: #1f2937;
--code-text: #f9fafb;
--focus-ring: rgba(37, 99, 235, 0.1);
--bg-secondary: #f3f4f6;
--input-bg: #ffffff;
--input-text: inherit;
}
Expand All @@ -44,6 +45,7 @@
--code-bg: #0f172a;
--code-text: #e2e8f0;
--focus-ring: rgba(59, 130, 246, 0.2);
--bg-secondary: #1f2937;
--input-bg: #374151;
--input-text: #f9fafb;
}
Expand Down Expand Up @@ -1362,6 +1364,14 @@ main {
font-size: 0.875rem;
}

.info-box {
padding: 0.5rem 0.75rem;
background: var(--bg-secondary);
border-radius: 4px;
font-size: 0.8rem;
color: var(--text-muted);
}

/* Print: force light theme */
@media print {
:root {
Expand Down
39 changes: 38 additions & 1 deletion app/templates/partials/analytics.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
<div class="analytics-panel">
<section class="analytics-controls">
<div class="quick-select" id="analytics-quick-select">
<button class="quick-select-btn" data-range="7d" onclick="analyticsSelectRange('7d')">Last 7 Days</button>
<button class="quick-select-btn active" data-range="30d" onclick="analyticsSelectRange('30d')">Last 30 Days</button>
<button class="quick-select-btn" data-range="max" onclick="analyticsSelectRange('max')">Max</button>
<button class="quick-select-btn" data-range="custom" onclick="analyticsSelectRange('custom')">Custom</button>
</div>
<form id="analytics-form" class="analytics-filter-form">
<div class="analytics-filter-row">
<div class="filter-group">
Expand Down Expand Up @@ -249,7 +255,38 @@ <h3>Export Data</h3>
document.getElementById('export-json').href = '/api/v1/analytics/export?format=json&' + exportParams.toString();
}

// Default date range: last 30 days
// Quick-select range handler (AC-012, AC-013, AC-014)
window.analyticsSelectRange = function(range) {
var btns = document.querySelectorAll('#analytics-quick-select .quick-select-btn');
btns.forEach(function(b) { b.classList.remove('active'); });
var clicked = document.querySelector('#analytics-quick-select [data-range="' + range + '"]');
if (clicked) clicked.classList.add('active');

var now = new Date();
var startEl = document.getElementById('analytics-start');
var endEl = document.getElementById('analytics-end');

if (range === '7d') {
var s7 = new Date(now);
s7.setDate(s7.getDate() - 7);
startEl.value = s7.toISOString().split('T')[0];
endEl.value = now.toISOString().split('T')[0];
fetchAnalytics();
} else if (range === '30d') {
var s30 = new Date(now);
s30.setDate(s30.getDate() - 30);
startEl.value = s30.toISOString().split('T')[0];
endEl.value = now.toISOString().split('T')[0];
fetchAnalytics();
} else if (range === 'max') {
startEl.value = '';
endEl.value = '';
fetchAnalytics();
}
// 'custom' highlights but does NOT auto-submit
};

// Default date range: last 30 days (AC-014)
var now = new Date();
var start = new Date(now);
start.setDate(start.getDate() - 30);
Expand Down
12 changes: 10 additions & 2 deletions app/templates/partials/notifications_list.html
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
<!-- Quick Select (AC-015, AC-020) -->
<div class="quick-select" id="notif-quick-select">
<button class="quick-select-btn active" data-range="7d">Last 7 Days</button>
<button class="quick-select-btn" data-range="30d">Last 30 Days</button>
<button class="quick-select-btn" data-range="max">Max</button>
<button class="quick-select-btn" data-range="custom">Custom</button>
</div>

<!-- Notification Filters -->
<div class="notification-filters">
<form class="notification-filter-form" hx-get="/htmx/notifications" hx-target="#notifications-container" hx-swap="innerHTML">
Expand Down Expand Up @@ -94,7 +102,7 @@
<div class="pagination">
{% if offset > 0 %}
<button class="btn btn-secondary"
hx-get="/htmx/notifications?offset={{ offset - limit }}&limit={{ limit }}{% if selected_status %}&status={{ selected_status }}{% endif %}{% if selected_webhook_name %}&webhook_name={{ selected_webhook_name }}{% endif %}{% if selected_source_name %}&source_name={{ selected_source_name }}{% endif %}"
hx-get="/htmx/notifications?offset={{ offset - limit }}&limit={{ limit }}{% if selected_status %}&status={{ selected_status }}{% endif %}{% if selected_webhook_name %}&webhook_name={{ selected_webhook_name }}{% endif %}{% if selected_source_name %}&source_name={{ selected_source_name }}{% endif %}{% if date_from %}&date_from={{ date_from }}{% endif %}{% if date_to %}&date_to={{ date_to }}{% endif %}"
hx-target="#notifications-container">
Previous
</button>
Expand All @@ -106,7 +114,7 @@

{% if offset + limit < total %}
<button class="btn btn-secondary"
hx-get="/htmx/notifications?offset={{ offset + limit }}&limit={{ limit }}{% if selected_status %}&status={{ selected_status }}{% endif %}{% if selected_webhook_name %}&webhook_name={{ selected_webhook_name }}{% endif %}{% if selected_source_name %}&source_name={{ selected_source_name }}{% endif %}"
hx-get="/htmx/notifications?offset={{ offset + limit }}&limit={{ limit }}{% if selected_status %}&status={{ selected_status }}{% endif %}{% if selected_webhook_name %}&webhook_name={{ selected_webhook_name }}{% endif %}{% if selected_source_name %}&source_name={{ selected_source_name }}{% endif %}{% if date_from %}&date_from={{ date_from }}{% endif %}{% if date_to %}&date_to={{ date_to }}{% endif %}"
hx-target="#notifications-container">
Next
</button>
Expand Down
4 changes: 2 additions & 2 deletions app/templates/partials/oidc_settings.html
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
</div>
{% endif %}

<div style="margin-bottom: 1rem; padding: 0.5rem 0.75rem; background: var(--bg-secondary, #f5f5f5); border-radius: 4px; font-size: 0.8rem; color: var(--text-muted);">
<div class="info-box" style="margin-bottom: 1rem;">
<strong>Callback URL</strong> — configure this in your OIDC provider:<br>
<code style="user-select: all;">{{ oidc_callback_url }}</code>
</div>
Expand Down Expand Up @@ -84,7 +84,7 @@
</label>
</div>

<div style="margin-top: 0.75rem; padding: 0.5rem 0.75rem; background: var(--bg-secondary, #f5f5f5); border-radius: 4px; font-size: 0.8rem; color: var(--text-muted);">
<div class="info-box" style="margin-top: 0.75rem;">
Local login can always be forced via the <code>FORCE_LOCAL_LOGIN</code> environment variable as a safety fallback.
</div>

Expand Down
Loading