Initial commit: add .gitignore and README
Some checks failed
CI / lint-and-test (push) Has been cancelled

This commit is contained in:
defiQUG
2026-02-09 21:51:50 -08:00
commit 93df3c8c20
116 changed files with 10080 additions and 0 deletions

17
docs/api-error-format.md Normal file
View File

@@ -0,0 +1,17 @@
# API error response format
All API errors use a consistent JSON body:
```json
{
"error": "Human-readable message",
"code": "UNAUTHORIZED",
"details": {}
}
```
- **error** (string): Message for clients and logs.
- **code** (string, optional): Machine-readable code. One of `BAD_REQUEST`, `UNAUTHORIZED`, `FORBIDDEN`, `NOT_FOUND`, `CONFLICT`, `INTERNAL_ERROR`.
- **details** (object, optional): Extra data (e.g. validation errors under `details` when `code` is `BAD_REQUEST`).
HTTP status matches the error (400, 401, 403, 404, 409, 500). The OpenAPI spec references the `ApiError` schema in `components.schemas`.

20
docs/architecture.md Normal file
View File

@@ -0,0 +1,20 @@
# Sankofa HW Infra — Architecture
## Component diagram
See the plan file for the Mermaid flowchart (Control Plane UI, API, Workflow Engine, PostgreSQL, S3, Integration Layer, IAM, Audit, Logging).
## Components
- **Control Plane UI**: React SPA; inventory, procurement, sites, approvals, audit.
- **API Layer**: REST `/api/v1`; CRUD for core entities; JWT + RBAC/ABAC; file upload to S3.
- **Workflow Engine**: Purchase approvals, inspection checklists (Phase 1+).
- **PostgreSQL**: Transactions, core entities, audit_events (append-only).
- **Object Storage (S3)**: Invoices, packing lists, inspection photos, serial dumps.
- **Integration Layer**: UniFi, Proxmox, Redfish connectors; credentials in Vault.
- **IAM**: Roles, permissions; ABAC attributes (site_id, project_id).
- **Audit Log**: Who/when/what, before/after; WORM retention.
## Sovereign cloud positioning
Sankofa Phoenix operates as a **sovereign cloud services provider**. Multi-tenant isolation is per sovereign (org); UniFi, Proxmox, and hardware inventory form **one source of truth** for determinism and compliance. UniFi telemetry (with product intelligence), rack/power metadata, and Proxmox workloads are synthesized for root-cause analysis, capacity planning, and enforced hardware standards per sovereign profile. See [sovereign-controller-topology.md](sovereign-controller-topology.md), [rbac-sovereign-operations.md](rbac-sovereign-operations.md), and [purchasing-feedback-loop.md](purchasing-feedback-loop.md).

View File

@@ -0,0 +1,7 @@
# Capacity planning dashboard spec
- **RU utilization**: Per site, sum of assigned positions vs total RU (from racks); show percentage. **Implemented.** API: `GET /api/v1/capacity/sites/:siteId` returns `usedRu`, `totalRu`, and `utilizationPercent`.
- **Power headroom**: From rack `power_feeds` (circuit limits). **Implemented.** API: `GET /api/v1/capacity/sites/:siteId/power` returns `circuitLimitWatts`, `measuredDrawWatts` (null until Phase 4), `headroomWatts` (null). Measured draw can be added when telemetry is available.
- **GPU inventory**: By type (part number) and location. **Implemented.** API: `GET /api/v1/capacity/gpu-inventory` returns `total`, `bySite`, and `byType`.
- **Read-only**: All capacity endpoints are read-only; no edits.
- **Web**: Capacity dashboard at `/capacity` shows RU utilization, power headroom, and GPU inventory by site and type.

11
docs/cicd.md Normal file
View File

@@ -0,0 +1,11 @@
# CI/CD pipeline
- Lint: `pnpm run lint` (ESLint over apps and packages)
- Test: `pnpm run test` (Vitest per package)
- Build: `pnpm run build` (all workspace packages)
GitHub Actions: `.github/workflows/ci.yml` runs on push/PR to main: install, lint, test, build.
Environments: Dev (local + docker-compose), Staging/Production (set DATABASE_URL, S3_*, JWT_SECRET).
Runbook: Start Postgres via `infra/docker-compose up -d`. Migrate: `pnpm db:migrate`. API: `pnpm --filter @sankofa/api run dev`. Web: `pnpm --filter @sankofa/web run dev`.

View File

@@ -0,0 +1,23 @@
# Compliance profiles
Compliance profiles define **firmware freeze**, **allowed hardware generations**, and **approved SKUs** per sovereign (org) or per site. They feed purchasing (approved buy lists) and UniFi device approval.
## Purpose
- **Firmware freeze:** Lock to a version or range (e.g. 2024.Q2, or min/max version) so only compliant firmware is allowed.
- **Allowed generations:** Restrict hardware to e.g. Gen2 and Enterprise only (from UniFi product catalog).
- **Approved SKUs:** Explicit list of SKUs that may be purchased or deployed; optional per-site override.
Profiles are attached to `org_id` (sovereign/tenant); optionally `site_id` for site-specific rules.
## API
- `GET /api/v1/compliance-profiles` — list profiles for the current org.
- `GET /api/v1/compliance-profiles/:id` — get one profile.
- `POST /api/v1/compliance-profiles` — create (body: name, firmwareFreezePolicy, allowedGenerations, approvedSkus, siteId).
- `PATCH /api/v1/compliance-profiles/:id` — update.
- `DELETE /api/v1/compliance-profiles/:id` — delete.
## Use in validation
When generating the **approved purchasing catalog** or when syncing UniFi devices, filter or flag by compliance profile: only SKUs in `approved_skus` or in `allowed_generations` (from the UniFi product catalog) are considered approved for that sovereign/site.

70
docs/erd.md Normal file
View File

@@ -0,0 +1,70 @@
# Database ERD
## Entity relationship overview
```mermaid
erDiagram
org_units ||--o{ org_units : parent
org_units ||--o{ users : org_unit
users ||--o{ user_roles : user
roles ||--o{ user_roles : role
sites ||--o{ user_roles : scope_site
vendors ||--o{ vendor_bank_details : vendor
vendors ||--o{ offers : vendor
vendors ||--o{ purchase_orders : vendor
regions ||--o{ sites : region
sites ||--o{ rooms : site
rooms ||--o{ rows : room
rows ||--o{ racks : row
racks ||--o{ positions : rack
sites ||--o{ assets : site
positions ||--o{ assets : position
users ||--o{ assets : owner
assets ||--o{ asset_components : parent
assets ||--o{ asset_components : child
assets ||--o{ provisioning_records : asset
assets ||--o{ maintenances : asset
purchase_orders }o--|| sites : inspection_site
purchase_orders }o--|| sites : delivery_site
purchase_orders ||--o{ shipments : po
users ||--o{ audit_events : actor
org_units { uuid id text name uuid parent_id text org_id }
users { uuid id text email text org_id uuid org_unit_id }
vendors { uuid id text org_id text legal_name text trust_tier }
offers { uuid id text org_id uuid vendor_id int quantity decimal unit_price text status }
purchase_orders { uuid id text org_id uuid vendor_id jsonb line_items text status }
shipments { uuid id uuid purchase_order_id text tracking text status }
regions { uuid id text org_id text name }
sites { uuid id text org_id uuid region_id text name jsonb network_metadata }
rooms { uuid id uuid site_id text name }
rows { uuid id uuid room_id text name }
racks { uuid id uuid row_id text name int ru_total jsonb power_feeds }
positions { uuid id uuid rack_id int ru_start int ru_end uuid asset_id }
assets { uuid id text org_id text asset_id text category text status uuid site_id uuid position_id }
asset_components { uuid id uuid parent_asset_id uuid child_asset_id text role }
provisioning_records { uuid id uuid asset_id text hypervisor_node text cluster_id }
maintenances { uuid id text org_id uuid asset_id text type text status }
audit_events { uuid id text org_id uuid actor_id text action text resource_type text resource_id jsonb before_state jsonb after_state timestamp occurred_at }
roles { uuid id text name jsonb permissions }
user_roles { uuid user_id uuid role_id uuid scope_site_id text scope_project_id }
```
## Core tables
- **org_units**, **users**: Tenancy and org hierarchy.
- **vendors**, **vendor_bank_details**: Vendor master; versioned bank details with dual approval.
- **offers**: SKU/MPN, quantity, price, evidence_refs, risk_score, status.
- **purchase_orders**: Line items, approval_stage, escrow_terms, inspection_site_id, delivery_site_id.
- **shipments**: PO link, tracking, customs_docs_refs.
- **regions**, **sites**, **rooms**, **rows**, **racks**, **positions**: Site hierarchy and RU mapping.
- **assets**: asset_id, category, serials, proof_artifact_refs, site_id, position_id, status, chain_of_custody.
- **asset_components**: parent_asset_id, child_asset_id, role (gpu/cpu/dimm/nic).
- **provisioning_records**: OS image, hypervisor node, cluster_id.
- **maintenances**: RMA/incident/part_swap; vendor_ticket_ref.
- **audit_events**: Append-only; actor_id, action, resource_type, resource_id, before_state, after_state.
- **roles**, **user_roles**: RBAC; scope_site_id, scope_project_id for ABAC.

View File

@@ -0,0 +1,2 @@
# Proxmox integration spec
Use cases: nodes, inventory. Auth: token per site (Vault). Map Asset to node via integration_mappings.

View File

@@ -0,0 +1,2 @@
# Redfish integration spec
Use cases: verify serials, power cycle. Credentials in Vault per site.

View File

@@ -0,0 +1,21 @@
# UniFi integration spec
UniFi is positioned as a **hardware identity and telemetry source**, a **product-line intelligence feed**, and a **procurement and lifecycle signal**—not only as networking gear. The platform integrates UniFi OS, UniFi Network Application, firmware catalogs, device generation, and support-horizon mapping so Sankofa Phoenix can answer: what exact hardware is deployed, what generation and firmware lineage, what support status, and is this infrastructure policy-compliant for this sovereign body?
**Use cases:** Discover devices, map ports, push port profiles; plus hardware identity, EoL/support horizon, and compliance-relevant metadata. Auth: API token per site (Vault). Sync: nightly; store in integration_mappings.
## UniFi Product Intelligence layer
UniFi is used as a **hardware identity and telemetry source**, not only networking. The platform maintains a canonical **UniFi product catalog** (`unifi_product_catalog`) with:
- SKU, model name, generation (Gen1 / Gen2 / Enterprise)
- Performance class, EoL date, support horizon
- `approved_sovereign_default` for purchasing and compliance
**API:** `GET /api/v1/integrations/unifi/product-catalog` (optional `?generation=`, `?approved_sovereign=true`), `GET /api/v1/integrations/unifi/product-catalog/:sku`. Device list `GET .../unifi/sites/:siteId/devices` returns devices enriched with `generation` and `support_horizon` from the catalog when the device model matches.
This layer feeds **purchasing** (approved buy lists, BOMs) and **compliance** (approved SKUs per sovereign, support-risk views).
## Sovereign-safe controller architecture
Per-sovereign UniFi controller domains with no cross-sovereign write. See [sovereign-controller-topology.md](sovereign-controller-topology.md) for the diagram and trust boundaries. Optionally store controller endpoints in the `unifi_controllers` table (org_id, site_id, base_url, role: sovereign_write | oversight_read_only, region); credentials remain in Vault. API: CRUD under `GET/POST/PATCH/DELETE /api/v1/unifi-controllers`, scoped by org_id.

View File

@@ -0,0 +1,111 @@
# Next steps before full Swagger docs and UX/UI
Do these in order so the API contract is stable and the front end has a clear target.
---
## 1. Auth and identity
- **Login / token endpoint**
There is no in-app login. JWTs are assumed to come from an external IdP. Before UI:
- Either add **POST /auth/login** (or /auth/token) that accepts credentials, looks up `users` + `user_roles`, and returns a JWT with `roles` and (for vendor users) `vendorId`, **or**
- Document the exact JWT shape and how your IdP must set `roles` and `vendorId` so the UI can integrate.
- **User and role management (optional)**
Schema has `users`, `roles`, `user_roles`, but no API. For a self-contained product, add **CRUD for users** and **assignment of roles** (and `vendor_id` for vendor users) so admins can onboard users and vendors without touching the DB directly.
---
## 2. API contract and behavior
- **Request validation**
Add JSON Schema (or Zod) for request bodies and path/query params on all routes so invalid input returns **400** with a consistent error shape instead of 500 or undefined behavior.
- **Error response format**
Standardize error payloads (e.g. `{ error: string, code?: string, details?: unknown }`) and document them so Swagger and the UI can show the same errors.
- **Optional: list pagination**
List endpoints (vendors, offers, assets, sites, etc.) return full arrays. Add `limit`/`offset` or `page`/`pageSize` and a total/cursor so the UI and docs can assume a stable list contract.
---
## 3. RBAC enforcement
- **Wire permissions to routes**
`requirePermission` exists but is not used on route handlers. For each route, add the appropriate `requirePermission(...)` (or equivalent) so that missing permission returns **403** with a clear message. This makes the API safe to document and use from the UI.
---
## 4. OpenAPI completeness (prerequisite for Swagger)
- **Document all paths**
OpenAPI currently documents only health, vendors, offers, purchase-orders, and ingestion. Add the rest so Swagger matches the real API:
- **Assets**: GET/POST /assets, GET/PATCH/DELETE /assets/:id
- **Sites**: GET/POST /sites, GET/PATCH/DELETE /sites/:id, and nested (rooms, rows, racks, positions) if exposed
- **Workflow**: POST /workflow/offers/:id/risk-score, POST /workflow/purchase-orders/:id/submit, approve, reject, PATCH status
- **Inspection**: templates and runs
- **Shipments**: CRUD
- **Asset components**: CRUD
- **Capacity**: GET endpoints
- **Integrations**: UniFi, product-catalog, Proxmox, mappings
- **Maintenances**: CRUD
- **Compliance profiles**: CRUD
- **UniFi controllers**: CRUD
- **Reports**: BOM, support-risk
- **Upload**: POST /upload (multipart)
- **Request/response schemas**
For each path, add `requestBody` and `responses` with schema (or $ref to `components/schemas`) so Swagger can show request/response bodies and generate client types.
- **Security per path**
Mark which paths use BearerAuth, which use IngestionApiKey, and which are public (e.g. health).
---
## 5. Environment and config
- **env.example**
Add `INGESTION_API_KEY` (and any OIDC/SSO vars if you add login) so deployers and the docs know what to set.
- **API base URL for web**
Ensure the web app can be configured with the API base URL (e.g. env `VITE_API_URL` or similar) so Swagger and the UI both target the same backend.
---
## 6. Testing
- **Stabilize the contract**
Add or expand API tests for critical paths (e.g. vendors, offers, purchase-orders, workflow, ingestion) so that when you add Swagger and the UI, changes to the API are caught by tests.
- **Optional: contract tests**
Consider testing that responses match a minimal schema (e.g. required fields) so the OpenAPI spec and the implementation stay in sync.
---
## 7. Web app baseline (before full UX/UI)
- **API client**
Add a minimal API client (fetch or axios) that sends the JWT (and `x-org-id` if required) so all UI calls go through one place and can be swapped for generated clients later.
- **Auth in the client**
Implement login (or redirect to IdP), store the token, and attach it to every request; handle 401 (e.g. redirect to login or refresh).
- **Feature flags or minimal nav**
Add a simple nav or list of areas (e.g. Vendors, Offers, Purchase orders, Assets, Sites) so the “full UX/UI” phase can fill in one screen at a time without redoing routing.
---
## 8. Then: full Swagger and UX/UI
After the above:
- **Full Swagger**
Serve the OpenAPI spec (e.g. from `/api/openapi.json` or `/api/docs`) and mount Swagger UI (or Redoc) so all operations and schemas are discoverable and try-it-now works.
- **Full UX/UI**
Build out screens, forms, and flows using the stable API and client; keep OpenAPI and the UI in sync via the same base URL and error format.
---
## Summary checklist
| # | Area | Action |
|---|-------------------|--------|
| 1 | Auth | Login/token endpoint or IdP contract; optional users/roles API |
| 2 | API contract | Request validation; consistent error format; optional pagination |
| 3 | RBAC | Use requirePermission on routes; return 403 where appropriate |
| 4 | OpenAPI | Document all paths, request/response schemas, security |
| 5 | Env | env.example (INGESTION_API_KEY, etc.); web API base URL |
| 6 | Tests | Broader API tests; optional contract/schema tests |
| 7 | Web baseline | API client, auth (token + 401), minimal nav/routes |
| 8 | Swagger + UI | Serve spec + Swagger UI; build out full screens |

2
docs/observability.md Normal file
View File

@@ -0,0 +1,2 @@
# Observability
Central logging: ELK or OpenSearch. Metrics: Prometheus + Grafana. Alerting on API errors and integration sync failures. API uses Fastify logger (structured).

99
docs/offer-ingestion.md Normal file
View File

@@ -0,0 +1,99 @@
# Offer ingestion (scrape and email)
Offers can be ingested from external sources so they appear in the database for potential purchases, without manual data entry.
## Sources
1. **Scraped** e.g. site content from theserverstore.com (Peter as Manager). A scraper job fetches pages, parses offer-like content, and creates offer records.
2. **Email** a dedicated mailbox accepts messages (e.g. from Sergio and others); a pipeline parses them and creates offer records.
Ingested offers are stored with:
- `source`: `scraped` or `email`
- `source_ref`: URL (scrape) or email message id (email)
- `source_metadata`: optional JSON (e.g. sender, subject, page title, contact name)
- `ingested_at`: timestamp of ingestion
- `vendor_id`: optional; may be null until procurement assigns the offer to a vendor
## API: ingestion endpoint
Internal or automated callers use a dedicated endpoint, secured by an API key (no user JWT).
**POST** `/api/v1/ingestion/offers`
- **Auth:** Header `x-ingestion-api-key` must equal the environment variable `INGESTION_API_KEY`. If missing or wrong, returns `401`.
- **Org:** Header `x-org-id` (default `default`) specifies the org for the new offer.
**Body (JSON):**
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `source` | `"scraped"` \| `"email"` | yes | Ingestion source |
| `source_ref` | string | no | URL or message id |
| `source_metadata` | object | no | e.g. `{ "sender": "Sergio", "subject": "...", "page_url": "..." }` |
| `vendor_id` | UUID | no | Vendor to attach; omit for unassigned |
| `sku` | string | no | |
| `mpn` | string | no | |
| `quantity` | number | yes | |
| `unit_price` | string | yes | Decimal |
| `incoterms` | string | no | |
| `lead_time_days` | number | no | |
| `country_of_origin` | string | no | |
| `condition` | string | no | |
| `warranty` | string | no | |
| `evidence_refs` | array | no | `[{ "key": "s3-key", "hash": "..." }]` |
**Response:** `201` with the created offer (including `id`, `source`, `source_ref`, `source_metadata`, `ingested_at`).
Example (scrape):
```json
{
"source": "scraped",
"source_ref": "https://theserverstore.com/...",
"source_metadata": { "contact": "Peter", "site": "theserverstore.com" },
"vendor_id": null,
"sku": "DL380-G9",
"quantity": 2,
"unit_price": "450.00",
"condition": "refurbished"
}
```
Example (email):
```json
{
"source": "email",
"source_ref": "msg-12345",
"source_metadata": { "from": "sergio@example.com", "subject": "Quote for R630" },
"vendor_id": null,
"mpn": "PowerEdge R630",
"quantity": 1,
"unit_price": "320.00"
}
```
## Scraper (e.g. theserverstore.com)
- **Responsibility:** Fetch pages (respecting robots.txt and rate limits), extract product/offer fields, then POST to `POST /api/v1/ingestion/offers` for each offer.
- **Where:** Can run as a scheduled job in `apps/` or `packages/`, or as an external service that calls the API. No scraper implementation is in-repo yet; this doc defines the contract.
- **Vendor:** If the site is known (e.g. The Server Store, Peter as Manager), the scraper can resolve or create a vendor and pass `vendor_id`; otherwise leave null for procurement to assign later.
- **Idempotency:** Use `source_ref` (e.g. canonical product URL) so the same offer is not duplicated; downstream you can upsert by `(org_id, source, source_ref)` if desired.
## Email intake (e.g. Sergio and others)
- **Flow:** Incoming messages to a dedicated mailbox (e.g. `offers@your-org.com`) are read by an IMAP poller or processed via an inbound webhook (SendGrid, Mailgun, etc.). The pipeline parses sender, subject, body, and optional attachments, then POSTs one or more payloads to `POST /api/v1/ingestion/offers`.
- **Storing raw email:** Attachments or full message can be uploaded to object storage (e.g. S3/MinIO) and referenced in `evidence_refs` or `source_metadata` (e.g. `raw_message_key`).
- **Vendor matching:** Match sender address or name to an existing vendor and set `vendor_id` when possible; otherwise leave null and set `source_metadata.sender` / `from` for later assignment.
## Configuration
- Set `INGESTION_API_KEY` in the environment where the API runs. Scraper and email pipeline must use the same value in `x-ingestion-api-key`.
- Use `x-org-id` on each request to target the correct org.
## Procurement workflow
- Ingested offers appear in the offers list with `source` = `scraped` or `email` and optional `vendor_id`.
- Offers with `vendor_id` null are “unassigned”; procurement can assign them to a vendor (PATCH offer or create/link vendor then update offer).
- Existing RBAC and org/site scoping apply; audit can track creation via `ingested_at` and `source_metadata`.

175
docs/openapi.yaml Normal file
View File

@@ -0,0 +1,175 @@
openapi: 3.0.3
info:
title: Sankofa HW Infra API
version: 0.1.0
servers:
- url: /api/v1
security:
- BearerAuth: []
components:
schemas:
ApiError:
type: object
properties:
error: { type: string, description: Human-readable message }
code: { type: string, enum: [BAD_REQUEST, UNAUTHORIZED, FORBIDDEN, NOT_FOUND, CONFLICT, INTERNAL_ERROR] }
details: { type: object, description: Optional validation or extra data }
securitySchemes:
BearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
description: JWT with optional vendorId for vendor users
IngestionApiKey:
type: apiKey
in: header
name: x-ingestion-api-key
description: Required for POST /ingestion/offers (env INGESTION_API_KEY)
paths:
/health:
get:
summary: Health
security: []
/auth/token:
post:
summary: Get JWT token
description: Exchange email (and optional password) for a JWT with roles and vendorId. No auth required.
security: []
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [email]
properties:
email: { type: string, format: email }
password: { type: string }
responses:
"200":
description: Token and user info
"401":
description: Invalid credentials
/vendors:
get:
summary: List vendors
description: If JWT contains vendorId (vendor user), returns only that vendor.
post:
summary: Create vendor
description: Forbidden for vendor users.
/vendors/{id}:
get:
summary: Get vendor
description: Vendor users may only request their own vendor id.
/offers:
get:
summary: List offers
description: If JWT contains vendorId, returns only that vendor's offers.
post:
summary: Create offer
description: Vendor users' vendorId is forced to their vendor.
/offers/{id}:
get:
summary: Get offer
patch:
summary: Update offer
delete:
summary: Delete offer
/purchase-orders:
get:
summary: List purchase orders
description: If JWT contains vendorId, returns only POs for that vendor.
/purchase-orders/{id}:
get:
summary: Get purchase order
/ingestion/offers:
post:
summary: Ingest offer (scrape or email)
description: Creates an offer with source (scraped|email), source_ref, source_metadata. Secured by x-ingestion-api-key only; no JWT. Use x-org-id for target org.
security:
- IngestionApiKey: []
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [source, quantity, unit_price]
properties:
source:
type: string
enum: [scraped, email]
source_ref:
type: string
description: URL or email message id
source_metadata:
type: object
vendor_id:
type: string
format: uuid
nullable: true
sku:
type: string
mpn:
type: string
quantity:
type: integer
unit_price:
type: string
incoterms:
type: string
lead_time_days:
type: integer
country_of_origin:
type: string
condition:
type: string
warranty:
type: string
evidence_refs:
type: array
items:
type: object
properties:
key: { type: string }
hash: { type: string }
responses:
"201":
description: Offer created
"401":
description: Invalid or missing x-ingestion-api-key
/capacity/sites/{siteId}:
get:
summary: RU utilization for a site
description: Returns usedRu, totalRu, utilizationPercent for the site (from racks and assigned positions).
parameters:
- name: siteId
in: path
required: true
schema: { type: string, format: uuid }
responses:
"200":
description: Site capacity (usedRu, totalRu, utilizationPercent)
"404":
description: Site not found
/capacity/sites/{siteId}/power:
get:
summary: Power headroom for a site
description: Returns circuitLimitWatts from rack power_feeds; measuredDrawWatts/headroomWatts null until Phase 4.
parameters:
- name: siteId
in: path
required: true
schema: { type: string, format: uuid }
responses:
"200":
description: Power info (circuitLimitWatts, measuredDrawWatts, headroomWatts)
"404":
description: Site not found
/capacity/gpu-inventory:
get:
summary: GPU inventory
description: Returns total, bySite, and byType (part number) counts.
responses:
"200":
description: GPU counts (total, bySite, byType)

View File

@@ -0,0 +1,95 @@
# Operational baseline — current hardware running / in-hand
Hardware already deployed, active, or physically in-hand (not part of available wholesale inventory). Quantities marked **TBD** are to be confirmed and locked during physical audit. Once confirmed, this document is the **authoritative operational baseline** for Sankofa Phoenix.
---
## A1. Compute servers (operational)
### HPE ProLiant ML110 series
- **Role:** Core services / management / utility workloads
- **Form factor:** Tower / rack-convertible
- **Status:** Running / in-hand
- **Quantity:** TBD
- **Notes:** Suitable for control-plane services, monitoring, identity, light virtualization
### Dell PowerEdge R630
- **Role:** General compute / virtualization / legacy workloads
- **Form factor:** 1U rackmount
- **Status:** Running / in-hand
- **Quantity:** TBD
- **Notes:** Ideal for Proxmox clusters, utility VMs, staging environments
---
## A2. Network and edge infrastructure
### UniFi Dream Machine Pro (UDM Pro)
- **Role:** Edge gateway, UniFi OS controller, firewall
- **Status:** Running / in-hand
- **Quantity:** TBD
- **Notes:** Per-site edge control; candidate for per-sovereign controller domains
### UniFi XG switches
- **Role:** High-throughput aggregation / core switching
- **Status:** Running / in-hand
- **Quantity:** TBD
- **Notes:** 10G/25G backbone for compute and storage traffic
---
## A3. ISP and external connectivity
### Spectrum Business cable modems
- **Role:** Primary or secondary WAN connectivity
- **Status:** Installed / in-hand
- **Quantity:** TBD
- **Notes:** Business-class internet access; typically paired with UDM Pro
---
## A4. Physical infrastructure and power
### APC equipment cabinets
- **Role:** Secure rack enclosure
- **Status:** Installed / in-hand
- **Quantity:** TBD
- **Notes:** Houses compute, network, and power equipment
### APC UPS units
- **Role:** Power conditioning and battery backup
- **Status:** Installed / in-hand
- **Quantity:** TBD
- **Notes:** Runtime and load to be captured per site for capacity planning
---
## A5. Operational classification summary
| Category | Status | Quantity |
| ------------------- | --------- | -------- |
| ML110 servers | Running | TBD |
| Dell R630 servers | Running | TBD |
| UDM Pro | Running | TBD |
| UniFi XG switches | Running | TBD |
| Spectrum modems | Installed | TBD |
| APC cabinets | Installed | TBD |
| APC UPS units | Installed | TBD |
---
## A6. Next actions (to finalize baseline)
1. **Physical audit** — Lock quantities and serials per site/rack.
2. **Import into sankofa-hw-infra** — Create as **Operational Assets** (assets with category, site, rack position).
3. **Attach to sites, racks, power feeds** — Populate site hierarchy and power metadata.
4. **Enable integrations** — UniFi (device mapping), Proxmox (node ↔ server), UPS monitoring where supported.
After quantities and serials are confirmed, this appendix is the authoritative operational baseline for capacity planning, BOM, and compliance.

View File

@@ -0,0 +1,29 @@
# Purchasing feedback loop
How UniFi telemetry and product intelligence drive approved buy lists, BOMs, and support-risk views.
## Data flow
1. **UniFi device sync** — Devices are synced from each sovereigns controller; device list includes model/SKU.
2. **Product catalog lookup** — Each device model/SKU is matched against `unifi_product_catalog` (generation, EoL, support horizon).
3. **Outputs:**
- **SKU-normalized BOM** per sovereign/site: which exact hardware is deployed, with generation and support status.
- **Support-risk heatmap:** devices near EoL or with short support horizon.
- **Firmware divergence alerts:** when firmware versions drift from policy (see compliance profiles).
- **Approved purchasing catalog:** only SKUs that meet the sovereigns compliance profile (allowed generations, approved_skus).
## Approved buy list
The “approved buy list” is the intersection of:
- Devices in use or recommended (from UniFi + catalog), and
- Catalog entries with `approved_sovereign_default` or matching the orgs **compliance profile** (allowed_generations, approved_skus).
So operations (what we have and whats supported) drives procurement (what were allowed to buy), not the other way around.
## Optional API
- `GET /api/v1/reports/bom?org_id=&site_id=` — Aggregate assets + UniFi mappings + catalog for a BOM.
- `GET /api/v1/reports/support-risk?org_id=&horizon_months=12` — Devices with EoL or support horizon within the next N months.
These can be implemented as thin wrappers over existing schema, `unifi_product_catalog`, and `integration_mappings`.

View File

@@ -0,0 +1,36 @@
# RBAC matrix for sovereign operations
Who can **see**, who can **change**, and who can **approve** (by role and by site/sovereign) for UniFi, compliance, and purchasing.
## Permissions
| Permission | Description |
|------------|-------------|
| unifi:read | Read UniFi devices and product catalog within assigned site/org |
| unifi:write | Change UniFi mappings and controller config within assigned site/org |
| unifi_oversight:read | Read-only across sovereigns (central oversight; no write) |
| compliance:read | View compliance profiles |
| compliance:write | Create/update/delete compliance profiles |
| purchasing_catalog:read | View approved buy lists and BOMs |
## Role vs permission (sovereign-relevant)
| Role | unifi:read | unifi:write | unifi_oversight:read | compliance:read | compliance:write | purchasing_catalog:read |
|------|:----------:|:-----------:|:--------------------:|:----------------:|:-----------------:|:------------------------:|
| super_admin | yes | yes | yes | yes | yes | yes |
| security_admin | | | yes | yes | yes | |
| procurement_manager | yes | | | | | yes |
| finance_approver | | | | | | yes |
| site_admin | yes | yes | | yes | | |
| noc_operator | yes | | | | | |
| read_only_auditor | yes | | | yes | | yes |
| partner_inspector | | | | | | |
## Scoping rules
- **unifi:read** and **unifi:write** apply only within the operators assigned **site** or **org** (via `user_roles.scope_site_id` / org). No cross-sovereign write.
- **unifi_oversight:read** is the only cross-sovereign read; used by central Sankofa Phoenix oversight. No write authority.
- **compliance:read** / **compliance:write** are scoped by org (sovereign); enforce in API so users only see/edit profiles for their org.
- **purchasing_catalog:read** is scoped by org/site so approved lists and BOMs are sovereign-specific.
Existing ABAC (e.g. `scope_site_id` on user_roles) enforces these boundaries; ensure new integration and compliance endpoints check permission and org/site scope.

View File

@@ -0,0 +1,4 @@
# Incident response runbook
1. Triage: check audit log and health endpoints.
2. Isolate affected assets or revoke credentials if compromise.
3. Notify; post-mortem and update runbooks.

View File

@@ -0,0 +1,6 @@
# Runbook: Provisioning and integration checks
- **Proxmox**: Register node; add mapping via POST /api/v1/integrations/mappings (provider=proxmox, externalId=node name). Sync nodes via scheduled job or manual trigger.
- **UniFi**: Map switch/port to rack position; store in integration_mappings with metadata (device id, port index).
- **Redfish**: At receiving, optionally call Redfish to verify serial and firmware; store result in asset proof artifacts.
- **Checks**: Verify mapping exists for asset before provisioning; confirm credentials in Vault for the site.

View File

@@ -0,0 +1,9 @@
# Runbook: Receiving and Inspection
## Inspection
1. Create inspection run from template.
2. Upload evidence; set pass/fail.
3. If fail: claim. If pass: approve release.
## Receiving
1. Reconcile shipment with PO. 2. Assign rack; set asset Received then Staged.

View File

@@ -0,0 +1,13 @@
# Runbook: Receiving and Racking
## Receiving
1. Create shipment for PO; scan items.
2. POST /api/v1/shipments/:id/receive with assetIds to set assets to received.
3. Update shipment status to received.
## Racking
1. Assign asset to position: PATCH /api/v1/assets/:id with positionId.
2. Set asset status to staged.
## Capacity
GET /api/v1/capacity/sites/:siteId and GET /api/v1/capacity/gpu-inventory for dashboards.

2
docs/security.md Normal file
View File

@@ -0,0 +1,2 @@
# Security
Secrets: Vault/KMS; rotate API tokens. MFA for privileged roles. Dual control: vendor bank details and PO final approval (Phase 1). Attachment malware scanning (Phase 4). Data retention policies by doc type.

View File

@@ -0,0 +1,42 @@
# Sovereign controller topology
Per-sovereign UniFi controller domains, regionally isolated management planes, and a central read-only oversight layer. No cross-sovereign write authority.
## Diagram
```mermaid
flowchart TB
subgraph sovereignA [Sovereign A]
CtrlA[UniFi Controller A]
CtrlA -->|write| NetA[Network A]
end
subgraph sovereignB [Sovereign B]
CtrlB[UniFi Controller B]
CtrlB -->|write| NetB[Network B]
end
subgraph oversight [Central oversight]
Phoenix[Sankofa Phoenix]
end
Phoenix -->|read only| CtrlA
Phoenix -->|read only| CtrlB
CtrlA -.->|no write| CtrlB
CtrlB -.->|no write| CtrlA
```
## Architecture
- **Per-sovereign controller domains:** Each sovereign (org/tenant) has its own UniFi controller(s). Write authority stays within that sovereign.
- **Regional isolation:** Controllers and management planes can be deployed per region so data and control stay in-region.
- **Central read-only oversight:** Sankofa Phoenix has a read-only view across controllers for audit, BOM, support-risk, and compliance—no write into any sovereigns controller.
- **Trust boundaries:** No cross-sovereign write; sovereign A cannot change sovereign Bs network or config.
This satisfies sovereignty, auditability, compartmentalization, and trust boundaries between different bodies (e.g. governmental).
## Optional: controller registry
If you store controller endpoints in the DB, use a table `unifi_controllers` with: org_id, site_id (optional), base_url, role (sovereign_write | oversight_read_only), region. Credentials remain in Vault; the table only stores topology and role. API: CRUD for controllers scoped by org_id.
See [integration-spec-unifi.md](integration-spec-unifi.md) for the “Sovereign-safe controller architecture” subsection.

49
docs/vendor-portal.md Normal file
View File

@@ -0,0 +1,49 @@
# Vendor portal and vendor users
Selected vendors can log in to assist in fulfilling needs: view and update their offers, and see purchase orders relevant to them.
## Model
- **Vendor user:** A user record with `vendor_id` set is a *vendor user*. That user can be assigned the role `vendor_user` and receive a JWT that includes `vendorId` in the payload.
- **Scoping:** When the API sees `req.user.vendorId`, it restricts:
- **Vendors:** List returns only that vendor; GET/PATCH/DELETE only for that vendor; POST (create vendor) is forbidden.
- **Offers:** List/GET/PATCH/DELETE only offers for that vendor; on create, `vendorId` is forced to the logged-in vendor.
- **Purchase orders:** List/GET only POs for that vendor.
## Onboarding a vendor user
1. Create or select a **Vendor** in the org (e.g. "The Server Store", "Sergio's Hardware").
2. Create a **User** with:
- `org_id` = same as org
- `vendor_id` = that vendor's ID
- `email` / `name` as needed
3. Assign the role **vendor_user** to that user (via your IdP or `user_roles` if you manage roles in-app).
4. At **login**, ensure the issued JWT includes:
- `roles`: e.g. `["vendor_user"]`
- `vendorId`: the vendor's UUID
Then the vendor can call the same API under `/api/v1` with that JWT (and `x-org-id`). They will only see and modify data for their vendor.
## Permissions for vendor_user
The role `vendor_user` has:
- `vendor:read_own` read own vendor
- `vendor:write_offers_own` create/update own offers
- `vendor:view_pos_own` view POs for their vendor
- `offers:read`, `offers:write` used in combination with `vendorId` scoping above
- `purchase_orders:read` used with vendor filter
Vendor users cannot create/update/delete vendor records, nor see other vendors' offers or POs.
## API surface (vendor portal)
Vendor users use the same endpoints as procurement, with automatic scoping:
- **GET /api/v1/vendors** Returns only their vendor.
- **GET /api/v1/vendors/:id** Allowed only when `:id` is their vendor.
- **GET/POST/PATCH/DELETE /api/v1/offers** Only their vendor's offers; POST forces their vendorId.
- **GET /api/v1/purchase-orders** Only POs where vendorId is their vendor.
- **GET /api/v1/purchase-orders/:id** Allowed only for POs of their vendor.
No changes to URLs or request bodies are required; scoping is derived from the JWT `vendorId`.