A FastAPI-based slideshow that serves a modern UI and a Legacy UI optimized for the iPad 2 (iOS 9.3.5). Ships as a containerized app with HTMX-powered interactions, calendar alarms, and weather widgets.
This application has NO built-in authentication. It is designed for internal-network-only deployment on a trusted home or office network. See SECURITY.md for details and guidance on adding authentication if needed for LAN/internet exposure.
- Photo Slideshow: Rotates through user-uploaded images.
- Legacy Mode: Specialized frontend for iPad 2 (1024x768, no CSS Grid, resized images).
- Calendar Alarms: Integrates with iCloud Calendar and other ICS feeds to display pop-up alarms for events. All alarm logic is handled server-side using the
icaleventslibrary for robust iCalendar parsing and recurrence support. The frontend displays rendered HTML fragments. See ADR-2026-02-14-alarm-dataflow.md for architecture. - Weather: Real-time weather widget.
- Unified Index Refresh: Weather and alarm UI updates are now driven by a single, configurable interval (default 5 minutes) via the
/components/index-refreshendpoint. This interval is set inapp/main.pyand exposed to templates for both modern and legacy UIs.
Weather and alarm widgets are refreshed together every 5 minutes (configurable) using a single HTMX poll to /components/index-refresh. This endpoint returns out-of-band fragments to update the relevant UI sections. The interval is set in app/main.py (INDEX_UPDATE_INTERVAL_SECONDS) and exposed to templates as index_update_interval_seconds.
Legacy and modern UIs both use this mechanism for consistent, reliable updates.
This project is also an experiment to explore agentic full stack development. We use vscode with copilot for code generation.
We adopted the ask (if necessary)/plan (GPT 4-1)/Implement (GPT 5-mini) loop.
The actual takeout is that GPT4.1 seems good for planing and explaining, but not so good for coding. GPT-5-mini (with the 4.1 Beast Mode) agent prompt is convincing.
This repository is also configured for OpenCode/OpenCode CLI so it can reuse the same repo guidance authored for GitHub Copilot.
- Root discovery starts from
AGENTS.md. - Project configuration lives in
opencode.jsonc. - OpenCode loads the canonical repo guidance from
AGENTS.md,.github/AGENTS.md, and.github/copilot-instructions.md. - Repo-specific OpenCode agents live in
.opencode/agents/.
Current OpenCode agents available in this repo:
beast-litebeast-mode-v3-1context-architectcustom-agent-foundry-optimizedjira-ticket-composerreviewersenior-cloud-architecttask-plannertask-researcher
If you launch OpenCode from the repo root, it should pick up this configuration automatically.
Note: As of 2026-02-10, all calendar event parsing and recurrence logic uses the icalevents library. The previous icalendar dependency has been fully removed. See TASK011 for migration details.
Recent Fixes (2026-02-17):
- Fixed recurring event caching to preserve all occurrences (e.g., biweekly events now show all dates)
- Fixed all-day event timezone handling to display correct dates in negative UTC offset timezones
- See ADR-2026-02-17 for technical details
The application is configured via environment variables (see .env.example for all options). Key variables:
LOG_LEVEL: Log verbosity (DEBUG, INFO, WARNING, ERROR, CRITICAL)WEBAPP_DEBUG: Enable debug mode (never enable in production)TZ: Timezone (IANA name, e.g., America/Toronto)CALDAV_URL: CalDAV server URLCALDAV_USERNAME: CalDAV usernameCALDAV_PASSWORD: CalDAV passwordCALDAV_PROVIDER: Optional provider hint (e.g.,icloudfor Apple-specific fixes)CALDAV_CALENDAR: Exact calendar URL/path to selectCALDAV_SYNC_ENABLED: Enable/disable CalDAV sync (default: true)CALDAV_DISABLE_HTTP3: Attempt to disable HTTP/3 for CalDAV client (default: false)CALDAV_CONNECT_TIMEOUT_SECONDS: CalDAV connect timeout (default: 20)CALDAV_READ_TIMEOUT_SECONDS: CalDAV read timeout (default: 60)CALDAV_MAX_RETRIES: CalDAV fetch retry attempts (default: 5)CALDAV_VERIFY_SSL: Verify SSL for CalDAV (default: true)BACKGROUND_SYNC_DELAY_MINUTES: Delay between syncing calendar sources (default: 0)BACKGROUND_SYNC_DEFAULT_MINUTES: Default background sync delay (default: 120)
All variables can be set in a .env file or passed via Docker Compose.
Requirements: Python 3.13+, uv installed.
- Install dependencies
uv sync --dev- Run the app
uv run uvicorn app.main:app --reloaduv run pytest tests/ -v --cov=app --cov-report=xmlAll current tests are unit/integration; no Docker build test is required.
The project provides an espima command for database and CalDAV workflows.
Initialize database tables and seed default rows:
uv run espima db initApply Alembic migrations:
uv run espima db migrateList calendars from a CalDAV account:
uv run espima caldav list --url <caldav-url> --username <user> --password <pass>Add a calendar source using one selector (--guid, --index, or --name):
uv run espima caldav add --index 1 --url <caldav-url> --username <user> --password <pass>Run calendar sync:
uv run espima caldav syncLint and validate HTML, CSS, and JavaScript:
# Run all validators (HTML, CSS, JS)
npm run lint
# Run individual validators
npm run lint:html # Validate HTML structure and accessibility
npm run lint:css # Validate CSS syntax and browser compatibility
npm run lint:js # Validate JavaScript syntax and compatibility
# Auto-fix CSS and JavaScript issues
npm run lint:fixPython code is linted with Ruff:
# Check Python code
uv run ruff check .
# Auto-format Python code
uv run ruff format .The project uses GitHub Actions for continuous integration and deployment:
Every push and pull request triggers:
- Python Linting: Ruff checks code style and formatting
- Frontend Validation: HTML, CSS, and JavaScript linting
- HTML structure and accessibility (WCAG 2.1)
- CSS syntax and browser compatibility (iOS 9.3+)
- JavaScript ES5/ES6 compliance and compatibility
- Unit Tests: pytest with coverage reporting
- Security Scanning: Trivy vulnerability scanner
- Docker Build: Validation of container build
Run the same checks locally before pushing:
# Python checks
uv run ruff check .
uv run ruff format . --check
# Frontend checks
npm run lint
# Run tests
uv run pytest tests/ -v --cov=appAll checks must pass before merging to main.
The recommended way to run Espace-Image in production is with Docker Compose. All environment variables from .env are automatically passed to the container. You can override any variable in the docker-compose.yml or your own .env file.
docker-compose up --buildBuild the image:
docker build -t espace-image:latest .Run the container (set env vars as needed):
docker run -d \
--name espace-image \
-p 8000:8000 \
-v ./data:/app/data \
--env-file .env \
--restart unless-stopped \
espace-image:latestThe app listens on http://localhost:8000 and mounts ./data for uploads.
Run the espima CLI against the project inside the container:
- One-off run (build image first if necessary):
docker compose run --rm web espima --help
# or run a specific command
docker compose run --rm web espima db migrate- Against a running container (recommended when the service is up):
docker compose exec web espima db migrate
# or
docker exec -it espace-image espima caldav list- Open an interactive shell in the container and run the CLI manually:
docker compose exec web sh
# then inside container:
espima --help
# or
python -m espima.cli db migrateNotes: the Compose service is named web and the container name is espace-image.
If espima cannot locate alembic.ini, run the migrate command from the repository root inside the container or use the python -m espima.cli entry which locates the repo root.