chore: re-sync deployed work that pre-dates this session
These files have been live on maple for a while via direct scp/edit but were never committed back to the forge. Restoring parity so the repo matches the running container's source tree before the new feature work on top. - app/terminal.py: NEW. xterm.js <-> asyncssh PTY bridge wired into the log drawer's Terminal tab. Was added on the deploy host only. - app/truenas.py: misc REST client tweaks deployed but not committed. - CLAUDE.md / SPEC.md: documentation drift — Stage 8 terminal section, updated file map. - docker-compose.yml / requirements.txt: minor infra deltas already active on maple. No behaviour change vs the running container. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
289c6d8f1a
commit
b85bac7686
6 changed files with 146 additions and 52 deletions
49
CLAUDE.md
49
CLAUDE.md
|
|
@ -1,18 +1,18 @@
|
|||
# TrueNAS Burn-In Dashboard — Project Context
|
||||
|
||||
> Drop this file in any new Claude session to resume work with full context.
|
||||
> Last updated: 2026-02-24 (Stage 8)
|
||||
> Last updated: 2026-04-29 (v1.0.0-12 — live against TrueNAS SCALE 25.10)
|
||||
|
||||
---
|
||||
|
||||
## What This Is
|
||||
|
||||
A self-hosted web dashboard for running and tracking hard-drive burn-in tests
|
||||
against a TrueNAS CORE instance. Deployed on **maple.local** (10.0.0.138).
|
||||
against a TrueNAS SCALE 25.10 instance. Deployed on **maple.local** (10.0.0.138).
|
||||
|
||||
- **App URL**: http://10.0.0.138:8084 (or http://burnin.hellocomputer.xyz)
|
||||
- **Stack path on maple.local**: `~/docker/stacks/truenas-burnin/`
|
||||
- **Source (local mac)**: `~/Desktop/claude-sandbox/truenas-burnin/`
|
||||
- **Source (local mac)**: `~/Desktop/claudesandbox/truenas-burnin/`
|
||||
- **Compose synced to maple.local** via `scp` or manual copy
|
||||
|
||||
### Stages completed
|
||||
|
|
@ -29,7 +29,7 @@ against a TrueNAS CORE instance. Deployed on **maple.local** (10.0.0.138).
|
|||
| 6c | Settings overhaul (editable form, runtime store, SMTP fix, stage selection) | ✅ |
|
||||
| 6d | Cancel SMART tests, Cancel All burn-ins, drag-to-reorder stages in modals | ✅ |
|
||||
| 7 | SSH burn-in execution, SMART attr monitoring, drive reset, version badge, stats polish | ✅ |
|
||||
| 8 | Live SSH terminal in drawer (xterm.js + asyncssh WebSocket PTY bridge) | ✅ |
|
||||
| 8 | Live against TrueNAS SCALE 25.10: SSH SMART, disk temps, CPU/PCH sensors, thermal gate | ✅ |
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -53,8 +53,7 @@ truenas-burnin/
|
|||
├── database.py # schema, migrations, init_db(), get_db()
|
||||
├── models.py # Pydantic v2 models; StartBurninRequest has run_surface/run_short/run_long + profile property
|
||||
├── settings_store.py # runtime settings store — persists to /data/settings_overrides.json
|
||||
├── ssh_client.py # asyncssh client: smartctl parsing, badblocks streaming, test_connection
|
||||
├── terminal.py # WebSocket ↔ asyncssh PTY bridge for live terminal tab
|
||||
├── ssh_client.py # asyncssh client: smartctl parsing, badblocks streaming, sensors, test_connection
|
||||
├── truenas.py # httpx async client with retry (lambda factory pattern)
|
||||
├── poller.py # poll loop, SSE pub/sub, stale detection, stuck-job check
|
||||
├── burnin.py # orchestrator, semaphore, stages, check_stuck_jobs()
|
||||
|
|
@ -71,7 +70,7 @@ truenas-burnin/
|
|||
│
|
||||
└── templates/
|
||||
├── layout.html # header nav: History, Stats, Audit, Settings, bell button
|
||||
├── dashboard.html # stats bar, failed banner, batch bar, log drawer (4 tabs: Burn-In/SMART/Events/Terminal)
|
||||
├── dashboard.html # stats bar (+ CPU/PCH sensors, thermal chip), failed banner, batch bar, log drawer (3 tabs: Burn-In/SMART/Events)
|
||||
├── history.html
|
||||
├── job_detail.html # + Print/Export button
|
||||
├── audit.html # audit event log
|
||||
|
|
@ -209,7 +208,7 @@ All read from `.env` via `pydantic-settings`. See `.env.example` for full list.
|
|||
| `TEMP_WARN_C` | `46` | Temperature warning threshold (°C) |
|
||||
| `TEMP_CRIT_C` | `55` | Temperature critical threshold — precheck fails above this |
|
||||
| `BAD_BLOCK_THRESHOLD` | `0` | Max bad blocks allowed before surface_validate fails (0 = any bad = fail) |
|
||||
| `APP_VERSION` | `1.0.0-7` | Displayed in header version badge |
|
||||
| `APP_VERSION` | `1.0.0-9` | Displayed in header version badge |
|
||||
| `SSH_HOST` | `` | TrueNAS SSH hostname/IP — empty disables SSH mode (uses mock/REST) |
|
||||
| `SSH_PORT` | `22` | TrueNAS SSH port |
|
||||
| `SSH_USER` | `root` | TrueNAS SSH username |
|
||||
|
|
@ -297,6 +296,15 @@ yield {"event": "drives-update", "data": html}
|
|||
thead { position: sticky; top: 0; z-index: 10; }
|
||||
```
|
||||
|
||||
### Burn-in SMART column overlay
|
||||
```python
|
||||
# When a burn-in runs a short_smart or long_smart stage, its progress must be
|
||||
# mirrored in the Short/Long SMART columns (which normally read from smart_tests table).
|
||||
# _fetch_drives_for_template() queries burnin_stages for running/completed SMART stages
|
||||
# and overlays them onto the drive dict. Only overlays if standalone SMART column is idle.
|
||||
# Helper: _compute_eta_seconds(started_at, percent) for linear ETA extrapolation.
|
||||
```
|
||||
|
||||
### export.csv route ordering
|
||||
```python
|
||||
# MUST register export.csv BEFORE /{job_id} — FastAPI tries int() on "export.csv"
|
||||
|
|
@ -329,6 +337,31 @@ async def burnin_get(job_id: int, ...): ...
|
|||
| `profile` NameError in `_execute_stages` | `_execute_stages` called `_recalculate_progress(job_id, profile)` but `profile` not in scope | Changed to `_recalculate_progress(job_id)` — profile param was unused |
|
||||
| `app_version` Jinja2 global rendered as function | Set `templates.env.globals["app_version"] = _get_app_version` (callable) | Set to the static string value directly: `= _settings.app_version` |
|
||||
| All buttons broken (Short/Long/Burn-In/Cancel) | `stages.forEach(function(s){` in `_drawerRenderBurnin` missing closing `});` — JS syntax error prevented entire IIFE from loading | Added missing `});` before `} else {` |
|
||||
| Burn-in SMART stage shows in wrong column | Burn-in orchestrator tracks SMART progress in `burnin_stages` table, but SMART columns read from `smart_tests` table only | `_fetch_drives_for_template` now queries `burnin_stages` for active burn-ins and overlays SMART stage progress/results onto the Short/Long SMART columns |
|
||||
| 14TB surface jobs marked `failed` after 6-day clean run (1.0.0-10) | `_stage_final_check` treated `ssh_client.get_smart_attributes` failures as drive failures, but that helper swallows transport errors and returns `failures: ["SSH error: ..."]`. A 1-second SSH blip invalidated multi-day surface scans. | `_stage_final_check` now distinguishes pure SSH-only failures (every entry starts with `"SSH error:"`) from real SMART failures; retries 3× with 30s gaps; soft-passes on persistent SSH-only — surface stages stand. |
|
||||
| `database is locked` during long_smart (1.0.0-11) | `_stage_smart_test_ssh` appended full smartctl output to `log_text` every 5s poll. SQLite's `COALESCE(log_text,'')||?` rewrites the whole column, and over 6+ hours `log_text` grew to 50 MB → contention against poller/orchestrator/settings writers. | (a) `_db()` is now an `@asynccontextmanager` setting `PRAGMA busy_timeout=10000` per connection. (b) log_text appends throttled to every 12 polls (~60s) or on state change. |
|
||||
| Stuck stage rows linger as `running` after `check_stuck_jobs` (1.0.0-11) | Stuck-job detector updated `burnin_jobs.state='unknown'` but didn't touch stage rows. | Added `UPDATE burnin_stages SET state='unknown', finished_at=? WHERE burnin_job_id=? AND state='running'` to the same transaction. |
|
||||
| Dashboard 500 — `TypeError: unhashable type: 'dict'` from Jinja (1.0.0-12) | Starlette 1.0.0 (released 2026-04) removed the legacy `TemplateResponse(name, context)` signature. With the old call style, the context dict ended up where `name` was expected, → Jinja `cache_key` was unhashable. | Migrated all 7 calls to new signature: `TemplateResponse(request, name, context)`. **Root enabler**: `requirements.txt` is unpinned, so `--build` pulled the latest breaking release. |
|
||||
|
||||
---
|
||||
|
||||
## Operational Gotchas
|
||||
|
||||
### `requirements.txt` is unpinned
|
||||
Every `docker compose up -d --build` pulls latest of fastapi, starlette, jinja2, asyncssh, etc. The Starlette 1.0 regression on 2026-04-27 is a direct consequence. **Either pin to known-good versions, or audit installed versions immediately after each rebuild** with:
|
||||
```bash
|
||||
docker exec truenas-burnin python3 -c "import fastapi, starlette, jinja2; print(fastapi.__version__, starlette.__version__, jinja2.__version__)"
|
||||
```
|
||||
|
||||
### Local source ↔ maple host can drift
|
||||
The deploy convention is `scp -r app/` from mac to maple, but if you ever edit on maple directly (or skip an `scp` after local changes), the two trees diverge. As of 2026-04-27 the local `routes.py` had unsynced SMART-overlay work but was missing the deployed `/ws/terminal` Stage 8 endpoint — neither side a superset.
|
||||
|
||||
**Always `diff -u` before bulk scp:**
|
||||
```bash
|
||||
ssh -p 2225 brandon@10.0.0.138 'cat ~/docker/stacks/truenas-burnin/app/routes.py' > /tmp/deployed_routes.py
|
||||
diff -u /tmp/deployed_routes.py ~/Desktop/claudesandbox/truenas-burnin/app/routes.py
|
||||
```
|
||||
When sides have conflicting edits, prefer **patching the host file in place + rebuild** over a destructive scp.
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
56
SPEC.md
56
SPEC.md
|
|
@ -85,7 +85,7 @@ A **Reset** action clears the test state for a drive so it can be re-queued. It
|
|||
|
||||
### Dashboard (Main View)
|
||||
|
||||
- **Stats bar:** Total drives, Running, Failed, Passed, Idle counts.
|
||||
- **Stats bar:** Total drives, Running, Failed, Passed, Idle counts. When SSH is active, also shows CPU and PCH temperature chips (live via SSE) and a thermal pressure indicator (WARM/HOT) that appears when running drives exceed the warning threshold.
|
||||
- **Filter chips:** All / Running / Failed / Passed / Idle — filters the table below.
|
||||
- **Drive table columns:** Drive (device name + model), Serial, Size, Temp, Health, Short SMART, Long SMART, Burn-In, Actions.
|
||||
- **Temperature display:** Color-coded. Green ≤ 45°C, Yellow 46–54°C, Red ≥ 55°C. Thresholds configurable in Settings.
|
||||
|
|
@ -97,11 +97,10 @@ A **Reset** action clears the test state for a drive so it can be re-queued. It
|
|||
|
||||
Slides up from the bottom of the page when a drive row is clicked. Does not navigate away — the table remains visible and scrollable above.
|
||||
|
||||
Four tabs:
|
||||
Three tabs:
|
||||
- **Burn-In** — stage-by-stage progress for the latest burn-in job; shows live elapsed time, raw SSH log output (smartctl / badblocks), and bad block count.
|
||||
- **SMART** — output of the last smartctl run for this drive, with monitored attribute values highlighted (green/yellow/red). Raw `smartctl -a` output also shown when SSH mode is active.
|
||||
- **Events** — chronological timeline of everything that happened to this drive (test started, test passed, failure detected, alert sent, reset, etc.).
|
||||
- **Terminal** — live SSH PTY session (xterm.js). Opens an interactive shell on the TrueNAS host. Requires SSH to be configured in Settings. Supports full colour, resize, paste, and reconnect. xterm.js is loaded lazily on first use.
|
||||
|
||||
Features:
|
||||
- Auto-scroll toggle (on by default).
|
||||
|
|
@ -143,8 +142,9 @@ Divided into sections:
|
|||
**BURN-IN BEHAVIOR**
|
||||
- Max Parallel Burn-Ins (default: 2, max: 60).
|
||||
- Warning displayed inline when set above 8: "Running many simultaneous surface scans may saturate your storage controller and produce unreliable results. Recommended: 2–4."
|
||||
- Bad block failure threshold (default: 2).
|
||||
- Bad block failure threshold (default: 0 — any bad sector = fail).
|
||||
- Stuck job threshold in hours (default: 24 — jobs running longer than this are auto-marked Unknown).
|
||||
- **Adaptive thermal gate:** When drive temperatures are at or above the warning threshold, new burn-in jobs wait up to 3 minutes before acquiring a semaphore slot. This reduces thermal pile-up when drives are already running hot.
|
||||
|
||||
**TEMPERATURE**
|
||||
- Warning threshold (default: 46°C).
|
||||
|
|
@ -166,8 +166,8 @@ Divided into sections:
|
|||
- Log level (DEBUG / INFO / WARN / ERROR).
|
||||
|
||||
**VERSION & UPDATES**
|
||||
- Displays current version (starting at 0.5.0).
|
||||
- "Check for Updates" button — queries GitHub releases API and shows latest version with a link if an update is available.
|
||||
- Displays current version.
|
||||
- "Check for Updates" button — queries Forgejo releases API at `git.hellocomputer.xyz` and shows latest version if an update is available.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -209,15 +209,21 @@ Both email and webhook fire simultaneously when both are configured and enabled.
|
|||
|
||||
## SSH Architecture
|
||||
|
||||
The app connects to TrueNAS over SSH from the host running the Docker container. It does not use the TrueNAS web API for drive operations — all smartctl and badblocks commands are issued directly over SSH.
|
||||
The app connects to TrueNAS over SSH from the host running the Docker container. It does not use the TrueNAS web API for SMART or badblocks operations — all commands are issued directly over SSH using `asyncssh`.
|
||||
|
||||
This is required for TrueNAS SCALE 25.10 (Electric Eel), which removed the `POST /api/v2.0/smart/test` REST endpoint. SSH is also the only way to run `badblocks`. The TrueNAS REST API is still used for drive discovery (`GET /api/v2.0/disk`) and temperature polling (`POST /api/v2.0/disk/temperatures`).
|
||||
|
||||
Connection details are configured in Settings (not `.env`). Supports:
|
||||
- Password authentication.
|
||||
- SSH key authentication (key pasted or uploaded in Settings UI).
|
||||
- Custom port.
|
||||
- SSH key authentication — key pasted into Settings UI or mounted as a Docker volume at `/run/secrets/ssh_key` (recommended for production).
|
||||
- Custom port (default: 22).
|
||||
- Test Connection button validates credentials before saving.
|
||||
|
||||
On SSH disconnection mid-test: the test process on TrueNAS may continue running (SSH disconnection does not kill the remote process if launched correctly with nohup or similar). The app marks the drive as `interrupted` in its own state, attempts to reconnect, and resumes polling if the process is still running. If the remote process is gone, the drive stays `interrupted`.
|
||||
In addition to burn-in commands, the SSH connection is used to:
|
||||
- Run `sensors -j` (lm-sensors) each poll cycle to read CPU and PCH/chipset temperatures, displayed live in the dashboard stats bar.
|
||||
- Poll `smartctl -a` progress during standalone SMART tests.
|
||||
|
||||
On SSH disconnection mid-test: the app marks the drive as `interrupted`. The remote process may or may not still be running. The user must reset the drive and re-queue.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -233,8 +239,7 @@ Key endpoints:
|
|||
- `POST /api/v1/drives/{drive_id}/smart/cancel` — cancel a SMART test.
|
||||
- `POST /api/v1/burnin/start` — start a burn-in job.
|
||||
- `POST /api/v1/burnin/{job_id}/cancel` — cancel a burn-in job.
|
||||
- `GET /sse/drives` — Server-Sent Events stream powering the real-time dashboard UI.
|
||||
- `WS /ws/terminal` — WebSocket endpoint bridging xterm.js to an asyncssh PTY on TrueNAS.
|
||||
- `GET /sse/drives` — Server-Sent Events stream powering the real-time dashboard UI. Also emits `system-sensors` (CPU/PCH temps, thermal pressure) and `job-alert` (browser push notification) events.
|
||||
- `GET /health` — health check endpoint.
|
||||
|
||||
The API makes this app a strong candidate for MCP server integration, allowing an AI assistant to query drive status, start tests, or receive alerts conversationally.
|
||||
|
|
@ -259,24 +264,19 @@ All other configuration is done through the Settings UI — no manual file editi
|
|||
|
||||
---
|
||||
|
||||
## TrueNAS Compatibility
|
||||
|
||||
Tested and confirmed working against **TrueNAS SCALE 25.10.2.1 (Electric Eel)**. Key compatibility notes:
|
||||
|
||||
- SCALE 25.10 removed `POST /api/v2.0/smart/test` — SSH is required for all SMART operations.
|
||||
- Drive temperatures are not included in `GET /api/v2.0/disk` on SCALE — use `POST /api/v2.0/disk/temperatures` instead.
|
||||
- TrueNAS SCALE is Linux/Debian-based. Device names are `sda`, `sdb`, etc. (not `ada0`/`da0` as on CORE/FreeBSD).
|
||||
- `lm-sensors` is available on SCALE — `sensors -j` returns CPU (`coretemp`) and PCH (`pch_*`) temperatures.
|
||||
- `badblocks` and `smartctl` are present at standard paths.
|
||||
|
||||
## mock-truenas
|
||||
|
||||
A companion Docker service (`mock-truenas`) that simulates the TrueNAS API for UI development and testing without real hardware. It mocks drive discovery, SMART test responses, and badblocks progress. Used exclusively for development — not deployed in production.
|
||||
|
||||
### Testing on Real TrueNAS (v1.0 Milestone Plan)
|
||||
|
||||
To validate against real hardware:
|
||||
|
||||
1. Switch `TRUENAS_URL` in `.env` from `http://mock-truenas:8000` to your real TrueNAS IP/hostname.
|
||||
2. Ensure SSH is enabled on TrueNAS (System → Services → SSH).
|
||||
3. Configure SSH credentials in Settings and use Test Connection to verify.
|
||||
4. Start with a single idle drive — run Short SMART only first.
|
||||
5. Verify the log drawer shows real smartctl output.
|
||||
6. If successful, proceed to Long SMART, then a full burn-in on a drive you're comfortable wiping.
|
||||
7. Confirm an alert email is received on completion.
|
||||
8. Scale to 2–4 drives simultaneously and monitor system resource warnings.
|
||||
|
||||
**v1.0 is considered production-ready when:** the app runs reliably on a real TrueNAS system with 10 simultaneous drives, a failure alert email is received correctly, and a passing drive's history is preserved across a container restart.
|
||||
A companion Docker service (`mock-truenas`) that simulates the TrueNAS API for UI development and testing without real hardware. It mocks drive discovery, SMART test responses, and badblocks progress. Used exclusively for development — not deployed in production. Disabled (commented out) in the production `docker-compose.yml`.
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -49,6 +49,13 @@ async def handle(ws: WebSocket) -> None:
|
|||
return
|
||||
elif settings.ssh_password:
|
||||
connect_kw["password"] = settings.ssh_password
|
||||
else:
|
||||
# Fall back to mounted key file (same logic as ssh_client._connect)
|
||||
import os
|
||||
from app import ssh_client as _sc
|
||||
key_path = os.environ.get("SSH_KEY_FILE", _sc._MOUNTED_KEY_PATH)
|
||||
if os.path.exists(key_path):
|
||||
connect_kw["client_keys"] = [key_path]
|
||||
else:
|
||||
await _send(ws,
|
||||
b"\r\n\x1b[33mNo SSH credentials configured.\x1b[0m "
|
||||
|
|
|
|||
|
|
@ -65,7 +65,13 @@ class TrueNASClient:
|
|||
"get_disks",
|
||||
)
|
||||
r.raise_for_status()
|
||||
return r.json()
|
||||
disks = r.json()
|
||||
# Filter out expired records — TrueNAS keeps historical entries for removed
|
||||
# disks with expiretime set. Only return currently-present drives.
|
||||
active = [d for d in disks if not d.get("expiretime")]
|
||||
if len(active) < len(disks):
|
||||
log.debug("get_disks: filtered %d expired record(s)", len(disks) - len(active))
|
||||
return active
|
||||
|
||||
async def get_smart_jobs(self, state: str | None = None) -> list[dict]:
|
||||
params: dict = {"method": "smart.test"}
|
||||
|
|
@ -110,3 +116,49 @@ class TrueNASClient:
|
|||
)
|
||||
r.raise_for_status()
|
||||
return r.json()
|
||||
|
||||
async def get_disk_temperatures(self) -> dict[str, float | None]:
|
||||
"""
|
||||
Returns {devname: celsius | None}.
|
||||
Uses POST /api/v2.0/disk/temperatures — available on TrueNAS SCALE 25.10+.
|
||||
CORE compatibility: raises on 404/405, caller should catch and skip.
|
||||
"""
|
||||
r = await _with_retry(
|
||||
lambda: self._client.post("/api/v2.0/disk/temperatures", json={}),
|
||||
"get_disk_temperatures",
|
||||
)
|
||||
r.raise_for_status()
|
||||
return r.json()
|
||||
|
||||
async def wipe_disk(self, devname: str, mode: str = "FULL") -> int:
|
||||
"""
|
||||
Start a disk wipe job. Not retried — duplicate starts would launch a second wipe.
|
||||
mode: "QUICK" (wipe MBR/partitions only), "FULL" (write zeros), "FULL_RANDOM" (write random)
|
||||
devname: basename only, e.g. "ada0" (not "/dev/ada0")
|
||||
Returns the TrueNAS job ID.
|
||||
"""
|
||||
r = await self._client.post(
|
||||
"/api/v2.0/disk/wipe",
|
||||
json={"dev": devname, "mode": mode},
|
||||
)
|
||||
r.raise_for_status()
|
||||
return r.json()
|
||||
|
||||
async def get_job(self, job_id: int) -> dict | None:
|
||||
"""
|
||||
Fetch a single TrueNAS job by ID.
|
||||
Returns the job dict, or None if not found.
|
||||
"""
|
||||
import json as _json
|
||||
r = await _with_retry(
|
||||
lambda: self._client.get(
|
||||
"/api/v2.0/core/get_jobs",
|
||||
params={"filters": _json.dumps([["id", "=", job_id]])},
|
||||
),
|
||||
f"get_job({job_id})",
|
||||
)
|
||||
r.raise_for_status()
|
||||
jobs = r.json()
|
||||
if isinstance(jobs, list) and jobs:
|
||||
return jobs[0]
|
||||
return None
|
||||
|
|
|
|||
|
|
@ -1,10 +1,13 @@
|
|||
services:
|
||||
mock-truenas:
|
||||
build: ./mock-truenas
|
||||
container_name: mock-truenas
|
||||
ports:
|
||||
- "8000:8000"
|
||||
restart: unless-stopped
|
||||
# mock-truenas is kept for local dev — not started in production
|
||||
# To use mock mode: docker compose --profile mock up
|
||||
# mock-truenas:
|
||||
# build: ./mock-truenas
|
||||
# container_name: mock-truenas
|
||||
# ports:
|
||||
# - "8000:8000"
|
||||
# profiles: [mock]
|
||||
# restart: unless-stopped
|
||||
|
||||
app:
|
||||
build: .
|
||||
|
|
@ -16,6 +19,5 @@ services:
|
|||
- ./data:/data
|
||||
- ./app/templates:/opt/app/app/templates
|
||||
- ./app/static:/opt/app/app/static
|
||||
depends_on:
|
||||
- mock-truenas
|
||||
- /home/brandon/.ssh/id_ed25519:/run/secrets/ssh_key:ro
|
||||
restart: unless-stopped
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
fastapi
|
||||
uvicorn
|
||||
uvicorn[standard]
|
||||
aiosqlite
|
||||
httpx
|
||||
pydantic-settings
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue