jpmschweitzerandClaude Sonnet 5 80034bbacf feat(tenants): add a default address on each consumer grant (T-11)
claude.ai's connector keeps no MCP session between tool calls, so the
Office cannot remember which address a conversation chose. Each consumer
grant now carries a fallback: the Office uses it when a call names none
and has no session binding (tatlock-office T-5, in a parallel round,
reads it).

Migration 008 (down_revision="007") adds
tenant_consumers.default_address — nullable varchar(63) — and a
composite foreign key (tenant_id, default_address) ->
scribe_addresses(owner_tenant, name), so a default always names a real
address in the same tenant. scribe_addresses already carries both a
primary key and a named unique constraint on (owner_tenant, name)
(migration 006), so the composite FK has something to bind to; Postgres's
default MATCH SIMPLE means a NULL default_address never triggers the
check, which is the state every grant but one is left in. The data step
sets jpmschweitzer/office-claude -> claude-ai only if that address exists
at upgrade time (true in production, false in core_api_test, where no
scribe_addresses are seeded) — an UPDATE ... WHERE EXISTS, not a bare
UPDATE, so it is a no-op rather than a failure when absent.
llm_tester/office-claude is untouched and stays null. Downgrade drops the
FK then the column.

TenantConsumer (models.py) gains the column and a matching
ForeignKeyConstraint. TenantsService.get_me now returns a third element,
the caller's own consumer's default_address (looked up only when
caller.consumer is not None — a user-kind caller carries no consumer, so
there is no grant row to hold a default for it) or None. GET /tenants/me
answers it; GET /tenants and its TenantSummary schema are unchanged.

Tests: TestMigration008RoundTrip/ForeignKey/DataStep added to
tests/test_tenants_migration.py (round-trip 007<->008, the FK existing
and refusing an unknown address while allowing null, the data step's
no-op and positive cases). tests/conftest.py gains a
default_address_grant fixture (registers a throwaway address, points a
grant at it, reverses both on teardown). tests/test_tenants.py gains
TestMeDefaultAddress (an office caller with/without a default, a user
caller always null).

Friction found and fixed in the same commit: the new composite FK made
Postgres refuse tests/test_scribe.py's existing clean_scribe_tables
TRUNCATE ("cannot truncate a table referenced in a foreign key
constraint") — the restriction is on the constraint's existence, not on
whether any row's default_address is actually non-null. tenant_consumers
joins that TRUNCATE; CASCADE was rejected because it would silently drop
every grant rather than only addresses, so the four grants
(jpmschweitzer/llm_tester x office-claude/office-gemini) are reinserted
in the same fixture, right after.

Mutation-checked: forcing default_address to null in the controller
fails exactly test_office_caller_with_a_default_sees_it, leaving the
without-a-default and user-caller tests green. Disabling the FK's
creation (and, symmetrically, its drop in downgrade — to keep the
round-trip self-consistent against the live core_api_test schema) fails
test_fk_exists and test_fk_refuses_a_default_naming_an_address_that_does_not_exist,
leaving test_fk_allows_a_null_default_regardless_of_tenant green. Both
mutations reverted; the full suite re-verified green (543 + 11 = 554
passed).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-10-19 22:05:46 +02:00
2026-01-04 21:51:21 +01:00

Core Code API

Central API service providing infrastructure management, home automation, and utility endpoints for the homelab ecosystem.

Features

Infrastructure Management

  • Portainer Integration: Stack and container management
  • NPM Integration: Nginx Proxy Manager domain and certificate management
  • Service Control: Start/stop services with container orchestration

Home Automation (Housekeeping API)

  • Device Control: Turn on/off, toggle, and set brightness for smart devices
  • Scene Activation: Trigger Home Assistant scenes
  • Script Execution: Run Home Assistant scripts
  • Automation Management: Enable/disable automations
  • State History: Query device state changes over time
  • Area Discovery: List rooms and areas

Utilities

  • DNS Lookup: Query DNS records (A, AAAA, MX, TXT, CNAME, NS, SOA, PTR)
  • Health Checks: Service health monitoring with database connectivity status

Architecture

src/
├── config.py                      # Global application settings
├── logging_config.py              # Logging configuration
├── base_controller.py             # Base controller pattern
├── main.py                        # FastAPI application entry point
├── auth/
│   └── oidc.py                    # OIDC authentication
├── clients/
│   ├── homeassistant_client.py    # Home Assistant REST client
│   ├── npm_client.py              # Nginx Proxy Manager client
│   └── portainer_client.py        # Portainer API client
├── controllers/
│   ├── health_controller.py       # Health endpoints
│   ├── housekeeping_controller.py # Home automation endpoints
│   ├── infrastructure_controller.py # Infrastructure management
│   ├── static_controller.py       # Static file serving
│   └── tools_controller.py        # DNS and utility tools
└── dns/
    ├── service.py                 # DNS lookup service
    └── exceptions.py              # DNS-specific exceptions

API Endpoints

Health

  • GET / - Service info and documentation links
  • GET /health - Basic health status
  • GET /health/full - Detailed component health
  • GET /health/diagnostics - Full diagnostic information

Infrastructure (/infrastructure)

  • GET /infrastructure/health - Portainer/NPM connection status
  • GET /infrastructure/services - List all services (stacks)
  • GET /infrastructure/services/{name} - Get service details
  • GET /infrastructure/services/{name}/status - Service status
  • POST /infrastructure/services/{name}/start - Start service
  • POST /infrastructure/services/{name}/stop - Stop service
  • GET /infrastructure/containers - List containers
  • GET /infrastructure/containers/{name} - Container details
  • GET /infrastructure/containers/{name}/logs - Container logs
  • GET /infrastructure/ports - List exposed ports
  • GET /infrastructure/domains - List proxy domains
  • GET /infrastructure/widget-data - Dashboard widget data

Housekeeping (/housekeeping)

  • GET /housekeeping/health - Home Assistant connection status
  • GET /housekeeping/devices - List controllable devices
  • GET /housekeeping/devices/{entity_id} - Device details
  • POST /housekeeping/devices/{entity_id}/control - Control device
  • GET /housekeeping/scenes - List scenes
  • POST /housekeeping/scenes/{scene_id}/activate - Activate scene
  • GET /housekeeping/scripts - List scripts
  • POST /housekeeping/scripts/{script_id}/run - Run script
  • GET /housekeeping/automations - List automations
  • POST /housekeeping/automations/{automation_id}/toggle - Toggle automation
  • GET /housekeeping/history - State history
  • GET /housekeeping/areas - List areas/rooms

Tools (/tools)

  • POST /tools/dns/lookup - DNS record lookup

Development

Requirements

  • Python 3.12+
  • Docker (for containerized deployment)

Local Development

# Install dependencies
pip install -r requirements.txt

# Run locally
uvicorn src.main:app --reload --host 0.0.0.0 --port 8083

Running Tests

# Run all tests
pytest

# Run with coverage
pytest --cov=src --cov-report=term-missing

# Run specific test file
pytest tests/test_housekeeping.py -v

Adding New Dependencies

Dependencies use major version pinning (~=) for automatic patch updates while preventing breaking changes.

  1. Add package to requirements.txt with major version constraint:

    package-name~=1.2.0  # Allows 1.2.x, blocks 1.3.0
    
  2. Restart the container to install:

    docker restart core-api
    

Docker Build

# Build image
docker build -t core-code:latest .

# Run container
docker run -p 8083:8083 core-code:latest

Deployment

Portainer Stack

  1. Navigate to Portainer UI
  2. Go to Stacks → Add Stack
  3. Name: core-code
  4. Upload stacks/core-code.yml or paste contents
  5. Deploy

Environment Variables

Variable Description Default
PORTAINER_URL Portainer API URL http://localhost:9000
PORTAINER_API_KEY Portainer API key -
NPM_URL Nginx Proxy Manager URL http://localhost:81
NPM_EMAIL NPM admin email -
NPM_PASSWORD NPM admin password -
HOMEASSISTANT_URL Home Assistant URL http://localhost:8123
HOMEASSISTANT_TOKEN HA long-lived access token -
OIDC_ENABLED Enable OIDC auth false
OIDC_ISSUERS OIDC issuer URLs (comma-separated) See config.py
OIDC_AUDIENCES OIDC audiences (comma-separated) See config.py

API Documentation

Once deployed, access documentation at:

Health Checks

  • Basic: GET /health - Fast liveness check for container orchestration
  • Full: GET /health/full - Returns database status (503 if unhealthy)
  • Diagnostics: GET /health/diagnostics - Service info and configuration

Security

  • Runs as non-root user (uid 1000)
  • OIDC authentication support via Authentik
  • CORS configured for same-network access
  • Admin endpoints require authentication when OIDC enabled

License

Internal use only.

S
Description
the core system management api for the tower-of-joy system.
Readme
1,014 KiB
2026-10-20 00:57:58 +02:00
Languages
Python 96.8%
HTML 2.4%
Makefile 0.4%
Shell 0.3%