Xiber NetOS — Admin & Operations Manual
Development setup, services, configuration, migrations, roles, and troubleshooting.
Tech Stack
| Component | Technology | Notes |
|---|---|---|
| Database | PostgreSQL 16 | PostGIS (geospatial), pgvector (embeddings) |
| Backend | FastAPI + SQLAlchemy 2.0 | Async via asyncpg, Pydantic v2 validation |
| Migrations | Alembic | Baseline + infrastructure + agreements + audit/feedback versions |
| Frontend | Next.js 14 + React 18 | TypeScript 5.5, TailwindCSS, App Router |
| Maps | MapLibre GL | Vector tile rendering |
| Scheduling | In-process asyncio loop | Weekly renewal report + activity digest run inside the API; no Celery app/worker yet (Redis provisioned but dormant) |
| MCP | Python FastMCP | 22 token-aware read/context tools for XOS agents; write tools and gateway registration pending |
| Dev Deployment | Docker Compose | Hot-reload for both API and web |
Prerequisites
- Docker Engine 24+
- Docker Compose v1 (the standalone
docker-composebinary, e.g.1.29.2). The production NetOS server runs Compose v1; the Compose v2 plugin (docker compose, no hyphen) is not installed for theroot/sudocontext on that host, so all server operations use the hyphenateddocker-composecommand. For consistency with the server, every command in this guide uses the hyphenateddocker-compose(v1) form. - 4 GB RAM minimum
- Ports 3000, 5432, 6379, 8000 available
- (Optional) Python 3.12+ and Node.js 20+ for local development outside Docker
Repository Layout
circuitos/
├── apps/
│ ├── api/ FastAPI backend
│ │ ├── app/
│ │ │ ├── main.py Entry point
│ │ │ ├── api/v1/ Route handlers
│ │ │ ├── core/ Auth, config
│ │ │ ├── db/ Session management
│ │ │ ├── models/ SQLAlchemy entities + enums
│ │ │ ├── schemas/ Pydantic request/response models
│ │ │ └── services/ Business logic (import, extraction, scheduler)
│ │ ├── alembic/ Migration versions
│ │ ├── scripts/ Seed data, utilities
│ │ ├── pyproject.toml Python dependencies
│ │ └── Dockerfile
│ ├── web/ Next.js frontend
│ │ ├── app/ App Router pages
│ │ ├── components/ React components
│ │ ├── lib/ API client, utilities
│ │ ├── package.json
│ │ └── Dockerfile
│ └── mcp/ MCP server skeleton
│ ├── app/server.py
│ └── pyproject.toml
├── infra/
│ └── docker/
│ ├── docker-compose.yml Local dev stack
│ ├── docker-compose.public.yml Production profile (SSO + tunnel)
│ ├── nginx/ Public reverse proxy config
│ └── postgres/ Custom PostgreSQL Dockerfile
├── docs/ Documentation (you are here)
├── examples/ Sample CSV data
└── .env.example Environment variable templateLocal Development
Start Everything
cd infra/docker
docker-compose up --buildRun Migrations
docker-compose exec api alembic upgrade headSeed Sample Data
docker-compose exec -T -e PYTHONPATH=/app api python scripts/seed_sample_data.pyCreates: 4 carriers (Lumen, Zayo, Cogent, Hurricane Electric), 10 endpoints with coordinates, 8 circuits with contracts, and lifecycle events. The script is idempotent — safe to run multiple times.
Restart Services
docker-compose restart api webRebuild After Dependency Changes
docker-compose up --build api webView Logs
docker-compose logs -f api # API logs
docker-compose logs -f web # Frontend logs
docker-compose logs -f postgres # Database logsServices
| Service | URL | Docker Name |
|---|---|---|
| Web UI | http://localhost:3000 | web |
| API | http://localhost:8000 | api |
| API Docs (Swagger) | http://localhost:8000/docs | api |
| API Health | http://localhost:8000/healthz | api |
| PostgreSQL | localhost:5432 | postgres |
| Redis | localhost:6379 | redis |
Server Deployment
The current demo deployment runs on the Xiber NetOS server.
| Service | URL |
|---|---|
| Public UI | https://circuitos.xiberian.net |
| Direct UI | http://127.0.0.1:3010 on the VM |
| Protected internal API | http://10.10.67.118:8010 |
| Direct API Docs | http://10.10.67.118:8010/docs |
Server path:
/home/stephen/netosServer compose file:
infra/docker/docker-compose.server.ymlThe server runs Docker Compose v1 (docker-compose 1.29.2). Always use the hyphenated docker-compose binary with the -p netos project name and the server compose file:
docker-compose -p netos -f docker-compose.server.yml ps
docker-compose -p netos -f docker-compose.server.yml logs -f api
docker-compose -p netos -f docker-compose.server.yml logs -f webContainers on this project are named
netos_<service>_1— e.g.netos_api_1,netos_web_1,netos_postgres_1,netos_redis_1.
Compose v1 recreate bug (KeyError: 'ContainerConfig')
Compose v1 (1.29.x) frequently fails when asked to recreate an already-running service in place, aborting with:
ERROR: for netos_api_1 'ContainerConfig'
KeyError: 'ContainerConfig'The safe pattern the checked-in deploy scripts use is to stop and remove the old container first, then bring it up fresh:
cd ~/netos/infra/docker
docker-compose -p netos -f docker-compose.server.yml build api web
# Remove the existing containers BEFORE `up` to avoid the ContainerConfig bug
docker stop netos_api_1 netos_web_1 || true
docker rm netos_api_1 netos_web_1 || true
docker-compose -p netos -f docker-compose.server.yml up -d api webDo this per service you are recreating. docker-compose ... up -d <svc> on a service whose container was already stopped+removed creates it cleanly; up -d on a live container is what triggers the bug.
The server web service runs next start from a built image. Do not mount apps/web or .next over the production container; only docs/ is mounted read-only at /app/content/docs.
Releases that include migrations
When a release ships new Alembic migrations, apply them with a one-off container BEFORE swapping the running `api` container, so the schema is already at head when the new code starts:
cd ~/netos/infra/docker
docker-compose -p netos -f docker-compose.server.yml build api
docker-compose -p netos -f docker-compose.server.yml run --rm api alembic upgrade head
# then recreate api (and web) using the stop+rm+up sequence aboveRollback
Tag the currently-good images before deploying so you can revert quickly if a new build misbehaves:
# Before deploy: retag the known-good images
docker tag netos_api:latest netos_api:rollback
docker tag netos_web:latest netos_web:rollback
# To roll back: point :latest back at the saved images and recreate
docker tag netos_api:rollback netos_api:latest
docker tag netos_web:rollback netos_web:latest
docker stop netos_api_1 netos_web_1 || true
docker rm netos_api_1 netos_web_1 || true
docker-compose -p netos -f docker-compose.server.yml up -d api webNote that a rollback only reverts application code/images, not the database. If the failed release ran migrations, plan the corresponding downgrade separately.
For fast frontend iteration, run a separate local preview service instead of putting the public web container into development mode:
cd ~/netos
scripts/start-web-preview.shThe preview service runs next dev with the web source bind-mounted. It is available locally at http://127.0.0.1:3011/preview and through the protected public proxy at https://netos.xiberian.net/preview. When a set of changes is ready for the public app, promote it with:
cd ~/netos
scripts/deploy-prod-web.shRun migrations on the server:
cd ~/netos/infra/docker
docker-compose -p netos -f docker-compose.server.yml run --rm api alembic upgrade headConfiguration
RF System Standards
RF system standards are maintained from the Admin page. They define the selectable defaults used when creating RF Links.
| Field | Purpose |
|---|---|
| Name | System label used in generated RF Link names and equipment defaults |
| Description | Short operating note or intended use case |
| Frequency Used | Human-readable spectrum band, such as CBRS / 5 GHz or 11 GHz licensed |
| Capacity Mbps | Default capacity applied to new RF Links |
| Cost | Default CAPEX applied to new RF Links |
| Type | PTP or PtMP |
| Role | Standalone, PtMP Base/Sector, or PtMP Subscriber |
| Compatible Bases | For PtMP subscriber radios, the base/sector systems that can serve that subscriber |
| Active | Controls whether the system appears as a selectable option for new RF Links |
Use PtMP Base/Sector for equipment installed on an infrastructure asset as a PtMP node, such as Tarana BN, Siklu Terragraph DN, or Cambium ePMP AP. Use PtMP Subscriber for customer-side radios, such as Tarana RN/RNv, Siklu T280/T265, or Cambium Force 4600c. When a user creates a PtMP RF Link from a selected PtMP source node, NetOS shows only subscriber radios whose compatible base list includes that source node's base system.
RF Links no longer require a manually entered link name. NetOS generates the display name from the selected A facility, Z facility or customer endpoint, and RF system/equipment. Links between two infrastructure assets are classified as backhaul. Links from an infrastructure asset to a customer endpoint are classified as customer endlinks.
Environment Variables
The API reads its configuration from environment variables via apps/api/app/core/config.py (Settings). Names below are the environment-variable form (Settings maps them case-insensitively). Defaults are the application defaults from Settings; the server compose file (docker-compose.server.yml) overrides some of them (noted where it matters). Copy .env.example to .env for local dev; the production stack is configured through docker-compose.server.yml and infra/docker/.env.public.
Database and Redis
| Variable | Default | Description |
|---|---|---|
DATABASE_URL | postgresql+asyncpg://circuitos:circuitos@postgres:5432/circuitos | Async (asyncpg) database connection string. Also used to derive a sync (+psycopg) URL for Alembic. |
REDIS_URL | redis://redis:6379/0 | Redis connection (reserved as a future Celery broker; no worker app exists yet, so Redis is provisioned but dormant). |
Authentication and proxy trust (see Authorization & RBAC)
| Variable | Default | Description |
|---|---|---|
ALLOW_DEV_AUTH | false | Enables the header-based dev-auth shim (x-user-email / x-user-role). Must be `false` in production. Server compose defaults this to false. |
TRUST_PROXY_AUTH_HEADERS | false | Trust SSO identity headers (e.g. X-Auth-Request-Email) forwarded by the reverse proxy. Server compose defaults this to true. |
TRUSTED_AUTH_PROXY_CIDRS | 127.0.0.1/32,172.16.0.0/12 | Comma-separated CIDRs whose client IP is allowed to supply trusted identity headers. Requests from outside these ranges cannot spoof identity headers. |
DEV_AUTH_DEFAULT_ROLE | exec | Legacy identity role used by the dev shim when x-user-role is absent. |
DEV_AUTH_DEFAULT_EMAIL | dev@xiber.com | Fallback identity email under dev auth. Note: this address is on the bootstrap Super Admin allowlist when dev auth is on — another reason dev auth must be off in production. |
RBAC_BOOTSTRAP_SUPER_ADMIN_EMAILS | *(empty)* | Comma-separated emails auto-provisioned as Super Admin on first login. Falls back to ADMIN_EMAIL when unset. |
ADMIN_EMAIL | admin@xiber.com | Super Admin bootstrap fallback and default admin notification recipient. |
CORS_ALLOWED_ORIGINS | http://localhost:3000 | Comma-separated allowed browser origins. Server compose sets https://netos.xiberian.net,https://circuitos.xiberian.net. |
ENTRA_TENANT_ID | common | Entra tenant (placeholder; live SSO is handled by oauth2-proxy — see Public Hostname & SSO). |
ENTRA_CLIENT_ID | dev-client-id | Entra app client ID (placeholder; not used for in-app token validation today). |
JWT_AUDIENCE | api://circuitos | Expected JWT audience claim (reserved for future in-app JWT validation). |
OBJECT_STORE_BUCKET | circuitos-dev | Object-store bucket name (reserved). |
SMTP and notifications (bootstrap defaults; Admin > Notifications overrides these once saved)
| Variable | Default | Description |
|---|---|---|
SMTP_HOST | *(unset)* | SMTP host. If unset and no Admin setting exists, scheduled email is skipped. Server compose sets mgw.xiberian.net. |
SMTP_PORT | 587 | 587 for STARTTLS or 465 for implicit TLS. |
SMTP_USERNAME | *(unset)* | Optional SMTP username. Server compose sets agents@xiberian.net. |
SMTP_PASSWORD | *(unset)* | Optional SMTP password. Encrypted at rest when saved via Admin. |
SMTP_FROM | netos@xiber.com | From address. Server compose sets NetOS <agents@xiberian.net>; the Xiber gateway requires an @xiberian.net sender for SPF/DKIM/DMARC alignment. |
NETOS_PUBLIC_BASE_URL | https://netos.xiberian.net | Public URL used to generate report links back into NetOS. |
NEW_USER_EMAIL_ENABLED | true | Sends a welcome email when an admin creates a new NetOS user. |
AI providers
| Variable | Default | Description |
|---|---|---|
AI_PROVIDER | deterministic | Bootstrap provider until Admin > AI is saved. Supported: openai, minimax, deterministic. |
OPENAI_API_KEY | *(unset)* | Bootstrap API key for the backend assistant; never exposed to the browser. |
OPENAI_MODEL | gpt-5.2 | Bootstrap model for assistant extraction. |
OPENAI_BASE_URL | https://api.openai.com/v1 | Bootstrap OpenAI-compatible base URL. |
AI_SECRET_KEY | *(unset)* | Encryption seed for provider API keys stored in the database (Fernet). Gotcha: when unset, the seed is derived from DATABASE_URL, so changing DATABASE_URL (host, credentials, or db name) makes previously stored provider keys undecryptable. Set AI_SECRET_KEY explicitly in any real deployment so stored keys survive database URL changes. |
Geocoding
| Variable | Default | Description |
|---|---|---|
GEOCODING_ENABLED | true | Enables address-to-coordinate lookups. |
GEOCODING_PROVIDER_URL | https://nominatim.openstreetmap.org/search | Geocoding endpoint. |
GEOCODING_USER_AGENT | Xiber NetOS geocoder | User-Agent sent with geocoding requests (Nominatim requires an identifiable UA). |
Scheduling (weekly renewal report and activity digest; times are UTC, Monday = 0)
| Variable | Default | Description |
|---|---|---|
RENEWAL_REPORT_ENABLED | true | Toggle for the weekly renewal email scheduler. |
RENEWAL_REPORT_WEEKDAY | 0 | UTC weekday for the scheduled report. |
RENEWAL_REPORT_HOUR | 8 | UTC hour after which the report can send. |
RENEWAL_REPORT_LOOKAHEAD_DAYS | 180 | Renewal decision lookahead window included in the report. |
ACTIVITY_DIGEST_ENABLED | true | Toggle for the weekly activity digest scheduler. |
ACTIVITY_DIGEST_WEEKDAY | 0 | UTC weekday for the digest. |
ACTIVITY_DIGEST_HOUR | 9 | UTC hour after which the digest can send. |
ACTIVITY_DIGEST_LOOKBACK_DAYS | 7 | Activity lookback window included in the digest. |
Frontend (web container) — these are read by Next.js, not by Settings:
| Variable | Default | Description |
|---|---|---|
API_BASE_URL | http://api:8000 | Server-side (SSR) API URL over Docker internal DNS. |
NEXT_PUBLIC_API_BASE_URL | *(varies)* | Browser-side API base. Server/preview set same-origin so browser calls go back through the proxy. |
Copy .env.example to .env and adjust as needed. For production SSO, see Public Hostname & SSO.
AI Assistant Provider
The NetOS Assistant uses /api/v1/ai/extract. With no provider configured it uses the deterministic parser. Admin users should normally configure the live provider from Admin > AI, where API keys are encrypted server-side and only the last four characters are shown after save.
Supported AI providers:
- OpenAI: default base URL
https://api.openai.com/v1, using the Responses API with strict JSON output. - MiniMax: default base URL
https://api.minimax.io/v1, default modelMiniMax-M3, using the OpenAI-compatible Chat Completions API for text/image tasks. - Fallback parser: deterministic extraction with no external model.
Use Test in Admin > AI after entering a key. The test calls the provider's OpenAI-compatible /models endpoint and verifies that the selected model is advertised before users rely on the provider.
The assistant never writes inventory directly. It returns draft payloads that users apply to forms and save through the normal NetOS APIs. Durable assistant memory is stored in ai_memory_entries and is limited to explicit user or organization preferences/rules, such as lines beginning with "remember..." or "always...".
Site photo analysis uses the same configured provider. Uploaded images are stored under /app/storage and the Docker deployment mounts that path from apps/api/storage so documents survive API container rebuilds. If an image analysis fails because the provider returned an empty or invalid response, users can open the site document gallery and choose Re-run AI after settings are corrected.
The Admin > AI page also includes a Customer Research Connector for Sonar/MCP-style enrichment. Configure the MCP URL, tool name, optional authorization token, and enable the connector. NetOS calls this connector only from the backend, then converts the research result into a normal customer import preview row. Users must still review, edit, select, and commit the staged row through the standard import workflow.
The connector supports either an MCP SSE URL such as http://mcp.xiberian.net:8081/sse?agent_id=netos-customer-research or a direct HTTP endpoint that accepts MCP JSON-RPC tools/call requests. For SSE, NetOS opens the stream, reads the server-provided message endpoint, initializes the MCP session, and then calls the configured tool. The configured tool should accept a query argument and return either structured JSON with draft, citations, warnings, and summary, or text containing that JSON. Browser clients never receive the connector credential. If the MCP server expects Authorization: <token> rather than Authorization: Bearer <token>, paste only the raw token in the API key field; NetOS sends the value exactly as entered.
Use the Test button after entering connector values. The test initializes the MCP session and runs tools/list; it does not perform customer research or create import rows. A passing test means NetOS can reach the MCP server and the configured tool name appears in the advertised tool list.
Email Notifications
Admin users with settings_view / settings_edit can manage delivery from Admin > Notifications. This page controls:
- SMTP host, port, username, password, and sender
- public NetOS base URL used in email links
- welcome emails for newly created users
- weekly report enablement, day, UTC hour, and lookahead window
- weekly activity digest enablement, day, UTC hour, and lookback window
- test email delivery before saving or sending reports
Saved Admin notification settings override environment variables. Environment variables remain useful as bootstrap defaults and for deployments that have not yet saved a database-backed notification setting. SMTP passwords are encrypted server-side and are never returned to the browser; the UI only shows whether a password exists and its last four characters. The Xiber gateway at mgw.xiberian.net refuses plaintext authentication; NetOS uses STARTTLS on port 587 and implicit TLS on port 465.
The Timeline page includes a Weekly Renewal Email Report section. It previews and manually sends an HTML report for circuits and infrastructure agreements whose renewal decision deadline is overdue or within the configured lookahead window. Each report includes:
- urgency state (
watch,active,critical,overdue) - agreement/vendor/type/status details
- MRC, term end, renewal notice, and renewal term
- links back to the circuit or infrastructure detail
- links to available agreement documents
- an HTML timeline-style snapshot for the report items
The API process also runs a lightweight scheduler. When SMTP is configured and the renewal report is enabled, it checks every 30 minutes and sends once on the configured UTC weekday/hour to all active registered users. Sends are recorded in user_activity_logs with event_type=report, so a container restart does not send the same report date twice.
The same scheduler sends the weekly activity digest when enabled. The digest summarizes write events, authorization changes, API-token usage, report sends, and renewal notice counts for the configured lookback window. Admins can also send the digest immediately from Admin > Notifications.
Docker Network
- Server-side web calls (Next.js SSR) use
API_BASE_URL=http://api:8000(Docker internal DNS) - Browser-side calls use the UI host origin.
- Machine clients and MCP connectors should use
http://10.10.67.118:8010
with Authorization: Bearer ntos_live_.... The API is also bound to 127.0.0.1:8010 for local VM checks and is not bound to the public NIC.
- The VM has persistent netplan routes for
10.10.75.0/24and10.10.43.0/24
via 10.10.67.1 dev ens19. These keep replies to internal API clients on the protected LAN path instead of returning through the public ens18 default route.
Authentication & RBAC
Authentication (who you are) and authorization (what you may do) are separate. Microsoft SSO / Entra proves identity; NetOS decides authorization with its own database-driven roles, permissions, and field-level filtering. The authoritative reference is Authorization & RBAC; this section is the operator summary.
Identity: production vs. dev auth
Two mutually exclusive identity paths, both selected in apps/api/app/core/config.py:
- Production (proxy-trusted headers). With
TRUST_PROXY_AUTH_HEADERS=true, the API trusts SSO identity headers (X-Auth-Request-Email,X-Forwarded-Email,CF-Access-Authenticated-User-Email,X-Ms-Client-Principal-Name, etc.) — but only when the request's client IP is inside `TRUSTED_AUTH_PROXY_CIDRS`. This is how browser SSO flows through the reverse proxy. See Public Hostname & SSO. - Dev auth (header shim). With
ALLOW_DEV_AUTH=true, the API acceptsx-user-email/x-user-rolewith no identity verification:
``bash curl -H "x-user-role: exec" http://localhost:8000/api/v1/circuits
When no email header is present it falls back to dev@xiber.com, which auto-bootstraps as Super Admin. For that reason `ALLOW_DEV_AUTH` must be `false` in production. The server compose file already defaults it to false.
If neither path yields an identity, the API returns 401.
Roles
Real authorization uses the DB-seeded RBAC roles — Super Admin, Executive, Finance Admin, Project Manager, Network Engineer, Field Technician, Read-Only User — each composed of granular permissions. See Authorization & RBAC for the full model, field-level financial masking, and API tokens.
The lowercase identity roles below are the legacy `user.role` values and the values accepted by the dev-auth x-user-role header. They gate a few coarse write checks but are not the primary authorization mechanism:
| Legacy role | Description | Write Access |
|---|---|---|
exec | Executive | Yes |
finance | Finance team | Yes |
network_eng | Network engineering | Yes |
operations | Operations | Yes |
pm | Project management | Yes |
sales | Sales | No (not in the write-role set) |
read_only | View only | No |
agent | MCP/XOS agent | Limited |
Database
Custom PostgreSQL Image
Built from pgvector/pgvector:pg16 with PostGIS installed.
Dockerfile: infra/docker/postgres/Dockerfile
Core Tables
| Table | Purpose |
|---|---|
users | User identity and role |
carriers | Carrier/provider companies |
service_type_catalog | Editable circuit service type catalog |
rf_system_catalog | Editable RF equipment/system presets |
customers | Commercial and MFC customer sites used by circuits, RF Links, and attribution |
endpoints | Physical locations with PostGIS geometry |
circuits | Wholesale circuit records |
rf_links | Wireless backhaul and customer endlink records |
rf_link_documents | FCC licenses and supporting RF Link documents |
infrastructure_assets | Towers, rooftops, data centers, POPs, carrier hotels, offices, and aggregation sites |
infrastructure_documents | Site agreements, floorplans, invoices, and supporting infrastructure documents |
subtended_links | Explicit infrastructure/customer/circuit/RF attribution hierarchy |
electrical_services | Electrical utility service accounts attached to infrastructure or customer sites |
electrical_service_documents | Bills, invoices, agreements, meter photos, and supporting electrical service documents |
contracts | Contract terms, renewal windows, ETF |
customer_attributions | Revenue attribution (future Sonar link) |
lifecycle_events | Timeline events (order, install, outage, etc.) |
invoices | Carrier invoices |
invoice_line_items | Invoice detail lines |
renewal_decisions | Renewal state machine records |
outage_events | Outage tracking |
sla_credits | SLA credit recovery records |
import_jobs | CSV/XLSX import metadata |
import_staging_rows | Staged import rows before commit |
user_activity_logs | Request/write audit activity |
feedback_requests | Bug and feature requests |
feedback_comments | Progress comments on bug/feature requests |
Uploaded document files are persisted outside the API container at apps/api/storage, mounted into the container as /app/storage. Do not remove this mount in production; otherwise database document records can outlive the uploaded files after a container rebuild.
Materialized View
circuit_financial_mv— pre-computed financial rollups for dashboard performance
Documentation PDFs
Markdown documentation is the source of truth. Downloadable PDFs are generated from the Markdown files and published from apps/web/public/docs.
Run the exporter after material documentation changes:
python3 scripts/build_doc_pdfs.pyGenerated files include:
| Public URL | |
|---|---|
| Complete bundle | /docs/netos-documentation.pdf |
| User Manual | /docs/user-manual.pdf |
| Admin & Operations | /docs/admin-operations.pdf |
| Data & Import Guide | /docs/data-import-guide.pdf |
| API & Integrations | /docs/api-integrations.pdf |
| Public Hostname & SSO | /docs/public-hostname-sso.pdf |
| Authorization & RBAC | /docs/authorization-rbac.pdf |
| Infrastructure Attribution | /docs/infrastructure-attribution.pdf |
| Roadmap & Known Gaps | /docs/roadmap-known-gaps.pdf |
The exporter embeds public documentation images such as the infrastructure attribution and waterfall diagrams.
Extensions
- PostGIS — geometry columns for endpoint coordinates, GeoJSON queries
- pgvector — vector embeddings (installed, not yet used)
Migrations
Migration Files
Located in apps/api/alembic/versions/, numbered sequentially (0001 … 0054 and growing). Always deploy schema with alembic upgrade head rather than tracking individual files. Notable milestones:
| Migration | Description |
|---|---|
0001_foundation_schema.py | Core tables, enums, constraints |
0002_infrastructure_assets.py | Infrastructure asset tables |
0004_audit_feedback.py | User activity logs and feedback requests |
0013–0015, 0026, 0054 | Subtended-link attribution, history, and redundancy |
0016, 0035, 0037, 0053 | RF links, PtMP nodes, RF system roles, endpoint customers |
0017–0025, 0036, 0039 | Customers, sites/services, revenue, commission, MFC fields |
0021 | Electrical services |
0030, 0031 | RBAC authorization tables and audit columns |
0033, 0034, 0038 | AI assistant memory, provider settings, research connector |
0040, 0052 | Notification settings and digest records |
0043–0051 | Vendor quotes, pricing options, escalators, line items, scope docs |
0046 | API access tokens |
Run docker-compose -p netos -f docker-compose.server.yml run --rm api alembic current to see the applied revision.
Commands
Apply all migrations:
# Via Docker
docker-compose exec api alembic upgrade head
# Or standalone
cd apps/api
alembic upgrade headCheck current version:
docker-compose exec api alembic currentCreate a new migration:
docker-compose exec api alembic revision --autogenerate -m "description"Review auto-generated migrations before applying — they may need manual adjustments for PostGIS columns or custom types.
Seed Data
Script: apps/api/scripts/seed_sample_data.py
What it creates:
| Entity | Count | Examples |
|---|---|---|
| Carriers | 4 | Lumen, Zayo, Cogent, Hurricane Electric |
| Endpoints | 10 | With coordinates for map rendering |
| Circuits | 8 | Various service types and statuses |
| Contracts | 8 | With term dates and renewal windows |
| Lifecycle Events | ~15 | Orders, installs, bandwidth changes |
The script uses upsert logic on natural keys — safe to run repeatedly without creating duplicates.
Audit Trail
Audit data is available in the Admin view at /admin.
What Is Logged
| Event Type | Examples |
|---|---|
| Request activity | API path, method, user, role, IP address, response status |
| Record writes | Create/update circuit, infrastructure, provider |
| Bulk actions | Per-record entries for bulk circuit and infrastructure updates/deletes |
| Feedback | Bug/feature request creation, status/priority changes, progress comments |
Timestamps are stored in UTC in PostgreSQL and displayed in the UI as Eastern time with the timezone label.
Soft Deletes
Circuit and infrastructure bulk deletes set deleted_at. They do not physically remove rows. Normal list/map/dashboard views filter out soft-deleted rows.
Bug & Feature Queue
The Admin view includes a feedback form and queue.
| Field | Values |
|---|---|
| Priority | low, normal, high, urgent |
| Status | new, triaged, planned, in_progress, done, declined |
Admins can add progress comments to each request. Public comments attempt to email the requester when SMTP is configured. SMTP failures are non-blocking, so feedback updates still save if email is unavailable.
SMTP configuration is normally managed from Admin > Notifications. The variables below are bootstrap fallbacks before Admin settings are saved:
| Variable | Description |
|---|---|
SMTP_HOST | SMTP server hostname |
SMTP_PORT | SMTP port, default 587 |
SMTP_USERNAME | Optional SMTP username |
SMTP_PASSWORD | Optional SMTP password |
SMTP_FROM | From address, default netos@xiber.com |
ADMIN_EMAIL | Admin notification recipient |
Verification Commands
Check API Health
curl -fsS http://localhost:8000/healthz
# → {"status": "ok"}Check Map Data
curl -fsS http://localhost:8000/api/v1/circuits/map | python3 -m json.tool | head -20Check Dashboard
curl -fsS http://localhost:8000/api/v1/dashboard/summary | python3 -m json.tool | head -20API Tests and Lint (pytest + httpx)
The API has an integration test harness under apps/api/tests/ (pytest, pytest-asyncio, and an in-process httpx client against the ASGI app). Run it from the API venv:
cd apps/api
.venv/bin/pytest tests/ -v
.venv/bin/ruff check app testsKey facts for operators:
- Tests run against a dedicated `circuitos_test` database, never the live
circuitosdatabase. The defaultTEST_DATABASE_URLispostgresql+asyncpg://circuitos:circuitos@127.0.0.1:55432/circuitos_test(override via theTEST_DATABASE_URLenv var). conftest.pyhard-refuses any target database whose name does not end in_test, and it auto-creates the test database and runs migrations on first use.- The harness forces
ALLOW_DEV_AUTH=true; with no identity headers, requests resolve todev@xiber.com(auto-bootstrapped Super Admin), so tests exercise authorized paths by default.
Compile Check (Python)
python3 -m compileall apps/api/app apps/api/scripts apps/mcp/appWeb Gates (Next.js)
cd apps/web
npm run typecheck # tsc --noEmit
npm run lint # next lint
npm run build # next buildTroubleshooting
Map Doesn't Load
| Check | Solution |
|---|---|
| API reachable? | curl http://localhost:8000/api/v1/circuits/map |
| CORS? | Ensure http://localhost:3000 is in allowed origins |
| Data exists? | Run seed script, verify circuits have endpoint coordinates |
| Browser cache? | Hard refresh (Cmd+Shift+R / Ctrl+Shift+R) |
| Containers running? | docker-compose ps — restart api and web if needed |
Frontend 500 on Server-Rendered Pages
- Verify
API_BASE_URL=http://api:8000is set in Docker Compose - Verify the
apicontainer is running:docker-compose logs api - Check for Python exceptions in API logs
No Sample Circuits Showing
- Run the seed script (see above)
- Verify:
curl http://localhost:8000/api/v1/circuitsreturns data - Check migrations:
docker-compose exec api alembic current
PostGIS Extension Errors
- Rebuild the custom PostgreSQL image:
docker-compose build postgres - Verify
infra/docker/postgres/Dockerfileis referenced indocker-compose.yml - Check:
docker-compose exec postgres psql -U circuitos -c "SELECT PostGIS_Version();"
Container Won't Start
docker-compose logs <service> # Check for errors
docker-compose down -v # Nuclear option: remove volumes and rebuild
docker-compose up --buildWarning: down -v destroys all data. Re-run migrations and seed script after.
