WebStatusπ is a lightweight web monitoring system designed for Raspberry Pi 1B+. It monitors configured URLs, tracks success/failure statistics, and provides a JSON API.
Hardware Target: Raspberry Pi 1B+ (512MB RAM, single-core 700MHz ARM11)
- Python: 3.11+ (required for modern type hints and datetime.UTC constant)
- HTTP Client:
requestslibrary for URL monitoring - Database:
sqlite3(stdlib) for persistent storage - Web Server:
http.server(stdlib) for JSON API - zero dependencies - Configuration:
PyYAMLfor YAML parsing - Concurrency:
threading(stdlib) for concurrent monitoring + web server
Follow standard Python conventions and these project-specific rules.
This project uses ruff for linting and formatting. The pre-commit hook runs ruff automatically before each commit.
Key configuration (see pyproject.toml):
- Line length: 120 characters maximum
- Target: Python 3.11+
Rules enabled:
| Code | Description |
|---|---|
| E, W | pycodestyle (style errors and warnings) |
| F | Pyflakes (logical errors) |
| I | isort (import sorting) |
| UP | pyupgrade (modernize to Python 3.11+ syntax) |
Run ruff manually:
# Check for errors
ruff check .
# Auto-fix errors
ruff check --fix .
# Format code
ruff format .Tip: Install the pre-commit hook to run ruff automatically:
pre-commit install
- Type hints: All functions must have type hints (see Type Hints below)
- Dataclasses: Use
dataclassesfor configuration and data transfer objects - No classes for logic: Use modules with pure functions (classes only for data structures)
- Functional approach: Prefer immutability, avoid side effects where possible
- Exception handling: Only at boundaries (network I/O, file I/O, database operations)
Use modern Python 3.11+ type syntax. The UP (pyupgrade) rule in ruff enforces this automatically.
Use built-in types instead of typing module:
# ✓ Correct (Python 3.11+)
def get_urls(active_only: bool = True) -> list[str]:
...
def fetch(url: str) -> dict[str, str] | None:
...
def process(items: list[int]) -> tuple[int, ...]:
...
# ✗ Incorrect (deprecated, triggers UP006/UP007)
from typing import List, Optional, Dict, Tuple
def get_urls(active_only: bool = True) -> List[str]:
...Quick reference:
Deprecated (typing) |
Modern (built-in) |
|---|---|
List[X] |
list[X] |
Dict[K, V] |
dict[K, V] |
Tuple[X, ...] |
tuple[X, ...] |
Set[X] |
set[X] |
Optional[X] |
X | None |
Union[X, Y] |
X | Y |
Import order enforced by isort (ruff rule I):
# Standard library imports (alphabetical, `import` before `from`)
import sqlite3
from dataclasses import dataclass
from datetime import UTC, datetime
# Third-party imports (separated by blank line)
import requests
import yaml
# Local imports (use package name for first-party)
from webstatuspi.config import load_configisort rules:
- Groups separated by a single blank line
- Alphabetical order within each group
import xcomes beforefrom x import y- First-party imports use
webstatuspi.prefix
- Functions:
snake_casewith descriptive names (check_url,get_all_stats) - Variables:
snake_casewith auxiliary verbs where appropriate (is_success,has_error) - Constants:
UPPER_SNAKE_CASE(DEFAULT_TIMEOUT,MAX_RETRIES) - Types:
PascalCasefor dataclasses and type aliases (UrlConfig,CheckResult)
- Names must be unique across all URLs
- Names must be ≤ 10 characters (optimized for OLED display)
- Use uppercase and underscores for readability (e.g.,
APP_ES,API_PROD)
The HTML dashboard is embedded in api.py as HTML_DASHBOARD. Follow these guidelines when modifying it.
- Use CSS custom properties: All colors defined in
:root(e.g.,--cyan,--red,--bg-dark) - No external stylesheets: All CSS must be inline in
<style>tag - Font stack:
'JetBrains Mono', 'Fira Code', 'Consolas', monospace - Color palette (cyberpunk theme):
--cyan: #00fff9- Primary accent, headers, success indicators--magenta: #ff00ff- Secondary accent (reserved)--green: #00ff66- Online/success status--red: #ff0040- Offline/error status--yellow: #f0ff00- Warning states--orange: #ff8800- Degraded states
- BEM-like classes:
.card,.card-header,.card-name - State modifiers:
.card.down,.status-indicator.up - Utility classes:
.count-dimmed,.progress-fill.warning
- Performance: Use
transformandopacityfor animations (GPU-accelerated) - Subtlety: Keep CRT effects subtle (
opacity: 0.04for scanlines) - Duration: Long pauses between effect cycles (32s for scanline, 36s for flicker)
- Purpose: Animations should enhance UX, not distract
pulse- Live indicator heartbeaterrorFlicker- Attention on failureslatencyPulse- Data update feedbackglitch- Hover microinteraction
- Vanilla JS only: No frameworks or libraries
- Polling interval: 10 seconds (
POLL_INTERVAL = 10000) - Data source: Fetch from
/statusendpoint only - Error handling: Display connection errors in
updatedTimeelement - XSS prevention: Always use
escapeHtml()for user-generated content
UPPER_SNAKE_CASEfor constants (POLL_INTERVAL)camelCasefor functions and variables (formatTime,isUpdating)- Descriptive function names:
renderCard,getLatencyClass,fetchStatus
- Colors: Add to
:rootcustom properties, never hardcode - Cards: Follow existing
.cardstructure (header → metrics → footer) - Metrics: Use
.metricgrid layout with.progress-barfor visual indicators - States: Add modifier classes (
.card.warning) rather than inline styles
- Add external dependencies (CDN scripts, external CSS)
- Use
document.write()orinnerHTMLwith unescaped user input - Create CPU-intensive animations (respect Pi 1B+ constraints)
- Add images or assets (keep dashboard self-contained)
- Use modern JS features not supported in older browsers (target ES6)
PyYAML==6.0.1
requests==2.31.0
Rationale:
- No heavy frameworks (Flask, FastAPI add 50+ MB)
- Use stdlib wherever possible (
http.server,sqlite3,threading,json) - Only two external dependencies absolutely required
When adding hardware features, see docs/HARDWARE.md.
- Timeouts: Treat as failure, log as "Connection timeout"
- DNS failures: Treat as failure, log as "DNS resolution failed"
- Connection refused: Treat as failure, log as "Connection refused"
- SSL errors: Treat as failure, log specific SSL error
- Configuration errors: Fail fast on startup (don't run with invalid config)
- Database errors: Log error, attempt to continue (monitoring more important than stats)
- API errors: Log error, return 500 status, keep monitoring running
- If database fails, continue monitoring but log to console only
- If API server fails to start, continue monitoring (primary function)
- If config reload fails, keep using previous valid configuration
Keep logging minimal (SD card wear):
- Console output: All check results (success/failure)
- Error logging: Only for exceptions and failures
- Info logging: Startup, shutdown, configuration changes
- Debug logging: Available via
--verboseflag, not enabled by default
Do NOT log:
- Successful routine operations
- Every database write
- API requests (unless error)
The Raspberry Pi 1B+ has severe resource limitations:
- CPU: Single-core 700MHz ARM11 processor
- RAM: 512MB (shared with GPU ~256MB available)
- Network: 10/100 Ethernet only
- Storage: SD card (slow I/O, wear considerations)
Design Constraints:
- Must be extremely lightweight
- Avoid heavy frameworks (no Flask, FastAPI, Django)
- Use stdlib wherever possible
- Minimize dependencies
- Avoid CPU-intensive operations
- Minimize disk writes (SD card wear)
Expected Targets:
- CPU usage: < 20% with 5 URLs being monitored
- RAM usage: < 200MB steady state
- API response time: < 100ms
- Startup time: < 5 seconds
pytest>=7.0.0
pytest-mock>=3.0.0
coverage>=6.0.0
- Start with valid config - system starts successfully
- Start with invalid config - system fails with clear error message
- Monitor URL that always succeeds (e.g., Google)
- Monitor URL that always fails (e.g., httpstat.us/500)
- Monitor URL that times out (e.g., httpstat.us/200?sleep=15000)
- API returns correct stats for all URLs
- API returns correct stats for specific URL
- API returns 404 for non-existent URL
- Run for 24 hours, check memory doesn't grow unbounded
- Disconnect network, verify graceful error handling
- Stop with Ctrl+C, verify graceful shutdown
Real-time monitoring output:
[2026-01-16 22:30:15] Google (https://www.google.com) - ✓ 200 OK (234ms)
[2026-01-16 22:30:45] Google (https://www.google.com) - ✗ 503 Service Unavailable (123ms)
Format: [timestamp] name (url) - status status_code status_text (response_time)
- Success:
✓(green in terminals that support color) - Failure:
✗(red in terminals that support color)
Before starting any task, consult docs/dev/LEARNINGS.md - this file contains critical lessons learned during development, including Pi 1B+ specific constraints, project patterns, and solutions to common problems. Ignoring this file may lead to repeating past mistakes.
- README.md - User guide, API reference, configuration
- CONTRIBUTING.md - Guidelines for human contributors
- docs/ARCHITECTURE.md - System architecture, design decisions, database schema
- docs/HARDWARE.md - Hardware specifications, GPIO pin assignments
- docs/RESOURCES.md - Learning resources for hardware concepts (GPIO, I2C, etc.)
- docs/TROUBLESHOOTING.md - Common issues and solutions
- docs/testing/ - Testing strategies and mocking guidelines
- docs/dev/ - Task management and development workflow
- docs/dev/LEARNINGS.md - CRITICAL: Lessons learned, patterns, and past solutions
Never hardcode SSH hosts, users, or credentials in code. Use environment variables from .env.local.
- Copy the example file:
cp .env.local.example .env.local - Edit
.env.localwith your actual values - The file is gitignored - safe to store real values
Password authentication is not supported. The helper script requires SSH key authentication for security.
First-time setup:
# 1. Generate an SSH key (if you don't have one)
ssh-keygen -t ed25519 -C "claude-deploy"
# 2. Copy your public key to the Pi
ssh-copy-id -p 22 claude@webstatuspi.lan
# 3. Test the connection (should not ask for password)
ssh -p 22 claude@webstatuspi.lan "echo 'SSH key auth working'"Why SSH keys instead of passwords?
- No credentials stored in plain text
- More secure than passwords
- Works with automated deployments
- Can be revoked individually
Troubleshooting:
- If
ssh-copy-idfails, ensure password auth is temporarily enabled on the Pi - Check
~/.ssh/authorized_keyson the Pi contains your public key - Verify permissions:
chmod 700 ~/.ssh && chmod 600 ~/.ssh/authorized_keys
| Variable | Description | Example |
|---|---|---|
PI_SSH_HOST |
Hostname or IP of the Pi | webstatuspi.lan |
PI_SSH_USER |
SSH username | pi or claude |
PI_SSH_PORT |
SSH port (default: 22) | 22 |
PI_PROJECT_PATH |
Project path on Pi | /opt/webstatuspi |
PI_SERVICE_NAME |
systemd service name | webstatuspi |
Preferred method - Use the helper script:
# Interactive SSH session (asks for confirmation)
./scripts/ssh-pi.sh
# Execute a remote command (no confirmation needed)
./scripts/ssh-pi.sh "systemctl status webstatuspi"
# Skip confirmation prompt with -y flag
./scripts/ssh-pi.sh -yThe script automatically:
- Validates SSH key authentication is configured
- Shows confirmation prompt for interactive sessions
- Displays clear error messages if key auth fails
Alternative - Load variables manually (not recommended):
source .env.local
ssh -p "$PI_SSH_PORT" "$PI_SSH_USER@$PI_SSH_HOST"- DO: Use
./scripts/ssh-pi.shfor all SSH connections - DO: Reference variables like
$PI_SSH_HOST,$PI_SSH_USER - DO: Ensure SSH key auth is configured before deploying
- DO NOT: Hardcode
claude@webstatuspi.lanor any host/user - DO NOT: Use password authentication (keys only)
- DO NOT: Include SSH credentials in commit messages or logs
- DO NOT: Store
.env.localin git (it's gitignored)
Record of key architectural decisions made during development. Add new entries as decisions are made.
Date: 2026-01-16
Status: Accepted
Context: Need lightweight HTTP server for JSON API on Pi 1B+
Decision: Use http.server from stdlib instead of Flask/FastAPI
Rationale:
- Zero additional dependencies
- ~50MB RAM savings vs Flask
- Sufficient for simple JSON API
- Threading support via
ThreadingMixInConsequences: - Manual routing required
- No automatic JSON parsing
- Limited middleware support
Date: 2026-01-16
Status: Accepted
Context: Need persistent storage for monitoring stats
Decision: Use SQLite via sqlite3 stdlib module
Rationale:
- No external process (PostgreSQL/MySQL would use 100MB+ RAM)
- File-based, easy backup
- Sufficient for single-writer scenario
- stdlib, no dependencies Consequences:
- Single-writer limitation (fine for this use case)
- No network access to DB
- Must handle WAL mode for SD card wear
Date: 2026-01-16
Status: Accepted
Context: Need human-readable configuration format
Decision: Use YAML via PyYAML library
Rationale:
- More readable than JSON for config files
- Supports comments
- Single small dependency acceptable Consequences:
- One external dependency
- Must validate schema manually
Date: 2026-01-18
Status: Accepted
Context: Need a visual dashboard for monitoring status without external dependencies
Decision: Embed HTML/CSS/JS as a Python string constant (HTML_DASHBOARD) directly in api.py
Rationale:
- Zero external files or static asset management
- No template engine dependency (Jinja2 would add ~5MB)
- Dashboard auto-refreshes via JavaScript fetch to
/status - CRT/cyberpunk aesthetic provides clear visual hierarchy
- Single file keeps deployment simple Consequences:
- HTML embedded in
api.pyasHTML_DASHBOARDconstant - No hot-reload for frontend development
- Must follow embedded dashboard guidelines (see Dashboard Code Style section) Note: Evaluated template engines (Jinja2, string.Template, str.format) but dashboard uses client-side rendering, so no server-side templating needed.
### ADR-XXX: Title
**Date**: YYYY-MM-DD
**Status**: Proposed | Accepted | Deprecated | Superseded
**Context**: What is the issue that we're seeing that is motivating this decision?
**Decision**: What is the change that we're proposing and/or doing?
**Rationale**: Why this decision over alternatives?
**Consequences**: What becomes easier or more difficult because of this decision?