diff --git a/.github/DockStat2-04.png b/.github/DockStat2-04.png index d375bd49..01fad580 100644 Binary files a/.github/DockStat2-04.png and b/.github/DockStat2-04.png differ diff --git a/.github/workflows/docs-sync.yaml b/.github/workflows/docs-sync.yaml index ad45589f..db5ad0d6 100644 --- a/.github/workflows/docs-sync.yaml +++ b/.github/workflows/docs-sync.yaml @@ -1,4 +1,6 @@ name: Outline Sync +permissions: + contents: write on: push: diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index abfde21a..d30ff682 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -19,5 +19,5 @@ jobs: run: | git config --global user.name 'actions-user' git config --global user.email 'its4nik@users.noreply.github.com' - git commit --allow-empty -am "chore(ci) [skip ci]: Lint" + git commit --allow-empty -am "chore(ci): Lint [skip ci]" git push diff --git a/apps/docs/dockstat/README.md b/apps/docs/dockstat/README.md new file mode 100644 index 00000000..04424f01 --- /dev/null +++ b/apps/docs/dockstat/README.md @@ -0,0 +1,223 @@ +--- +id: 7dddd764-6483-4f84-96a3-988304e772d3 +title: DockStat +collectionId: b4a5e48f-f103-480b-9f50-8f53f515cab9 +parentDocumentId: null +updatedAt: 2025-12-17T09:43:38.464Z +urlId: zqa4IyZtl0 +--- + +![](/api/attachments.redirect?id=2c39d8bf-6a65-43f6-9781-287b6950d350 "full-width =2834x274") + + +--- + + +:::warning +***DockStat is currently under active alpha-development, expect breaking changes*** + +::: + + +--- + +> ***DockStat is a Docker container monitoring and management platform built as a monorepo. The system consists of a Bun/Elysia backend API, a React Router frontend, companion services (DockNode, DockStore), and shared packages.*** + +## Documentation Index + +| Section | Description | +|----|----| +| [Architecture](/doc/d56ca448-563a-4206-9585-c45f8f6be5cf) | System design, data flow, and component relationships | +| [API Reference](/doc/b174143d-f906-4f8d-8cb5-9fc96512e575) | Complete REST API endpoint documentation | +| [Apps overview](/doc/fb89c77f-9f0a-497a-bb24-c41d21b37478) | Guide to all DockStat applications | +| [Configuration](/doc/dec1cb2c-9a13-4e67-a31c-d3a685391208) | Environment variables and settings | +| [Integration guide](/doc/e4e04545-fd9f-4fbf-becb-94da81f48bc5) | Package interoperability and external integrations | +| [Packages](/doc/bbcefaa2-6bd4-46e8-ae4b-a6b823593e67) | Shared package documentation | +| [Troubleshooting](/doc/88a5f959-3f89-4266-9d8e-eb50193425b0) | Common issues and solutions | + +## Architecture + +```mermaidjs + +graph TB + subgraph Frontend + DS[dockstat
React Router SSR] + end + + subgraph Backend + API[api
Elysia + Bun] + DCM[DockerClientManager] + PH[PluginHandler] + DB[(SQLite)] + end + + subgraph Companion + DN[docknode
Remote Agent] + DST[dockstore
Plugin Registry] + end + + subgraph Packages + PKG_DC["@dockstat/docker-client"] + PKG_DB["@dockstat/db"] + PKG_SW["@dockstat/sqlite-wrapper"] + PKG_PH["@dockstat/plugin-handler"] + PKG_LOG["@dockstat/logger"] + PKG_TYP["@dockstat/typings"] + PKG_UI["@dockstat/ui"] + end + + DS -->|Eden client| API + API --> DCM + API --> PH + API --> DB + DCM --> PKG_DC + PH --> PKG_PH + DB --> PKG_DB + PKG_DB --> PKG_SW + DN -->|Stack deploy| API + DST -->|Plugin bundles| PH +``` + +## Repository Structure + +``` +apps/ +├── api/ Backend API (Elysia, prefix /api/v2) +├── dockstat/ Frontend (React Router v7, SSR) +├── docknode/ Remote agent for stack deployment +├── dockstore/ Plugin and theme registry +└── docs/ Documentation (this folder) + +packages/ +├── db/ Database layer (@dockstat/db) +├── docker-client/ Docker operations (@dockstat/docker-client) +├── logger/ Logging utility (@dockstat/logger) +├── plugin-handler/Plugin system (@dockstat/plugin-handler) +├── sqlite-wrapper/SQLite query builder (@dockstat/sqlite-wrapper) +├── typings/ Shared types (@dockstat/typings) +├── ui/ UI components (@dockstat/ui) +└── utils/ Utilities (@dockstat/utils) +``` + +## Quick Start + +Install dependencies from the monorepo root: + +```bash +bun install +``` + +Start the API in development mode: + +```bash +cd apps/api + +bun run dev +``` + +Start the frontend in development mode: + +```bash +cd apps/dockstat + +bun run dev +``` + +The API listens on port 9876 by default with prefix `/api/v2`. The frontend dev server runs on port 5173. + +## Applications + +| Application | Type | Port | Documentation | +|----|----|----|----| +| `dockstat` | Frontend | 5173 / 3000 | [Apps overview](/doc/fb89c77f-9f0a-497a-bb24-c41d21b37478) | +| `api` | Backend API | 9876 | [API Reference](/doc/b174143d-f906-4f8d-8cb5-9fc96512e575) | +| `docknode` | Remote Agent | 4000 | [Apps overview](/doc/fb89c77f-9f0a-497a-bb24-c41d21b37478) | +| `dockstore` | Plugin Registry | — | [Apps overview](/doc/fb89c77f-9f0a-497a-bb24-c41d21b37478) | + +## Packages + +| Package | Description | +|----|----| +| [@dockstat/sqlite-wrapper](/doc/f543683b-68be-431f-a6d5-7b4012b1345a) | Type-safe SQLite query builder | +| [@dockstat/docker-client](/doc/ef12194c-404e-4bcd-a5b0-31aaf7b1b798) | Docker operations with monitoring | +| [@dockstat/db](/doc/5176f3ba-1242-4c85-8290-491dcc0f9963) | Database layer with themes | +| [@dockstat/plugin-handler](/doc/eadaaa93-6c65-4207-a4ac-9b19afc8f2a5) | Plugin lifecycle management | +| [@dockstat/logger](/doc/e913e6bd-3f7c-485f-812d-3e626b4f6b5b) | Colorized logging utility | +| [@dockstat/typings](/doc/ecb9e07b-37e7-430a-b3fc-eb51515ab9ac) | Shared TypeScript types | +| [@dockstat/ui](/doc/a04555b5-b827-4441-ae20-9cad1a2be714) | React UI components | +| [@dockstat/utils](/doc/d3039895-f53c-46ab-89ce-de0ad22ce03c) | Common utilities | + +## Environment Variables + +### API (`apps/api`) + +| Variable | Description | Default | +|----|----|----| +| `DOCKSTAT_MAX_WORKERS` | Max worker threads for DockerClientManager | `200` | +| `DOCKSTATAPI_SHOW_TRACES` | Enable server timing traces | `true` | +| `DOCKSTATAPI_DEFAULT_PLUGIN_DIR` | Default plugin directory | `src/plugins/default-plugins` | + +### Logger (`packages/logger`) + +| Variable | Description | +|----|----| +| `DOCKSTAT_LOGGER_FULL_FILE_PATH` | Show full file paths in logs | +| `DOCKSTAT_LOGGER_IGNORE_MESSAGES` | Comma-separated messages to ignore | +| `DOCKSTAT_LOGGER_DISABLED_LOGGERS` | Comma-separated logger names to disable | +| `DOCKSTAT_LOGGER_ONLY_SHOW` | Only show these loggers (comma-separated) | +| `DOCKSTAT_LOGGER_SEPERATOR` | Logger name separator (default `:`) | + +### DockNode (`apps/docknode`) + +| Variable | Description | +|----|----| +| `DOCKNODE_DOCKSTACK_AUTH_PSK` | Production pre-shared key | +| `DOCKNODE_DOCKSTACK_DEV_AUTH` | Development auth key | +| `DOCKNODE_DOCKSTACK_AUTH_PRIORITY` | Auth method priority | +| `PORT` | Server port (default `4000`) | + +[Full Configuration Documentation →](./configuration) + +## Build for Production + +Frontend: + +```bash +cd apps/dockstat + +bun run build + +bun run start +``` + +The frontend Dockerfile at `apps/dockstat/Dockerfile` produces a production image. + +## Tech Stack + +| Layer | Technology | +|----|----| +| Runtime | Bun | +| Backend Framework | Elysia | +| Frontend Framework | React Router v7 (SSR) | +| Database | SQLite (via `@dockstat/sqlite-wrapper`) | +| Docker Integration | Dockerode (via `@dockstat/docker-client`) | +| UI Styling | TailwindCSS | +| Type Safety | TypeScript + Typebox schemas | + +## Key Features + +* **Multi-Host Docker Management**: Manage multiple Docker hosts from a single interface +* **Real-Time Monitoring**: Live container statistics and event streaming +* **Plugin System**: Extensible plugin architecture for custom functionality +* **Theme Support**: CSS variable-based theming system +* **Type Safety**: End-to-end TypeScript with runtime validation + +## Getting Help + +* **Troubleshooting**: [Common issues and solutions](./troubleshooting) +* **GitHub Issues**: [github.com/Its4Nik/DockStat/issues](https://github.com/Its4Nik/DockStat/issues) +* **Wiki**: [outline.itsnik.de](https://outline.itsnik.de/s/9d88c471-373e-4ef2-a955-b1058eb7dc99) + +## Contributing + +Contributions, ideas and bug reports are welcome. See the main repository README for contribution guidelines. \ No newline at end of file diff --git a/apps/docs/dockstat/api-reference/README.md b/apps/docs/dockstat/api-reference/README.md index ba3632d4..bf16cbb6 100644 --- a/apps/docs/dockstat/api-reference/README.md +++ b/apps/docs/dockstat/api-reference/README.md @@ -1,628 +1,298 @@ --- -id: c85f4dd0-6855-418c-854d-062d86adb158 -title: API reference +id: b174143d-f906-4f8d-8cb5-9fc96512e575 +title: API Reference collectionId: b4a5e48f-f103-480b-9f50-8f53f515cab9 -parentDocumentId: 9550ca5f-3b09-4e4d-9ea9-634b4cc1553d -updatedAt: 2025-08-18T22:50:49.841Z -urlId: 1PTxqx1MQ6 +parentDocumentId: 7dddd764-6483-4f84-96a3-988304e772d3 +updatedAt: 2025-12-16T19:16:50.491Z +urlId: gVYlljv3Fs --- -Since the new backend is currently under active development this page might be outdated. Please see the following issue for a more up-to-date status: - - (hover this link for quick overview). - -# Available root-routes: - -| Root-Routes | Functionality | -|----|----| -| /auth | Controlling of authentication service | -| /data | Database queries, used for historic data and database management | -| /frontend | Exposed routes for configuring various settings for a/the frontend | -| /api | Only get endpoints, used for fetching configuration data and forcing a new request to the docker sockets instead of querying the database. | -| /conf | Endpoints for configuring backend options | -| /notification-service | configuration of the notification service integrated into the DockStatApi. | -| /ha | Only used for the High Availability synchro | - - ---- - -# Symbol legend - -| **✅** | Required | -|----|----| -| **❌** | Optional | -| ⛔ | Not needed | - -## Auth routes: - -### POST: /auth/enable - - -:::info -sets a current password and enables auth for all endpoints, except the api-docs - -::: - -| Parameter type | Parameter name | Required? | -|----|----|----| -| Query | password | :white_check_mark: | - - ---- - -### POST: /auth/disable - - -:::info -Disables authentication for all endpoints - -::: - -| Parameter type | Parameter name | Required? | -|----|----|----| -| Query | password | :white_check_mark: | - - ---- - -## Database queries - -### GET: /data/latest - - -:::info -Queries the latest entry of the database and provides it as a JSON response. - -::: - -| Parameter type | Parameter name | Required? | -|----|----|----| -| None | None | :no_entry: | - - ---- - -### GET: /data/time/24h - - -:::info -Queries all the latest data of the database in a 24h timeframe and organizes them in a JSON array. - -::: - -| Parameter type | Parameter name | Required? | -|----|----|----| -| None | None | **⛔** | - - ---- - -### DELETE: /data/clear - - -:::info -Clears \*\*\*ALL \*\*\*entries of the SQLite database. - -::: - -| Parameter type | Parameter name | Required? | -|----|----|----| -| None | None | **⛔** | - - ---- - -## Frontend routes: - - -:::info -**\***) When referring to a config the frontend config at the path: `/data/frontendConfiguration.json` is meant. - -::: - -### POST: /frontend/show/{==containerName==} - - -:::info -Sets a container to visible in the config\*\*\*\*\* - -::: - -| Parameter Type | Parameter name | Required? | -|----|----|----| -| Path | ==containerName== | :white_check_mark: | - - ---- - -### POST: /frontend/tag/{==containerName==/{==tag==} - - -:::info -Adds a tag to a container inside the config\*, as an array for multiple tags - -::: - -| Parameter type | Parameter name | Required? | -|----|----|----| -| Path | ==containerName== | :white_check_mark: | -| Path | ==tag== | :white_check_mark: | - - ---- - -### POST: /frontend/pin/{==containerName==} - - -:::info -#### Sets "pinned" to true, inside the config\* - -::: - -| Parameter type | Parameter name | Required? | -|----|----|----| -| Path | ==containerName== | :white_check_mark: | - - ---- - -### POST: /frontend/add-link/{==containerName==}/{==link==} - - -:::info -Sets the "link" string inside the config\* - -::: - -| Parameter type | Parameter name | Required? | -|----|----|----| -| Path | ==containerName== | :white_check_mark: | -| Path (might change) | ==link== | :white_check_mark: | - - ---- - -### POST: /frontend/add-icon/{==containerName==}/{==icon==}/{==useCustomIcon==} - - -:::info -Configures the icon string inside the config, when useCustomIcon is true the path file path of the icon gets to adjust with custom/{==icon==}.png - -::: - -| Parameter type | Parameter name | Required? | -|----|----|----| -| Path | ==containerName== | :white_check_mark: | -| Path (string WITHOUT file type) | ==icon== | :white_check_mark: | -| Path (boolean) | ==useCustomicon== | :x: | - -### DELETE: /frontend/hide/{==containerName==} - - -:::info -Sets "hidden" to true inside the config\* - -::: - -| Parameter type | Parameter name | Required? | -|----|----|----| -| Path | ==containerName== | :white_check_mark: | - - ---- - -### DELETE: /frontend/remove-tag/{==containerName==}/{==tag==} - - -:::info -Removes the specified tag from the frontend config\* tag-array - -::: - -| Parameter type | Parameter name | Required? | -|----|----|----| -| Path | ==containerName== | :white_check_mark: | -| Path | ==tag== | :white_check_mark: | - - ---- - -### DELETE: /frontend/unpin/{==containerName==} - - -:::info -Sets "pinned" to false inside the config\* - -::: - -| Parameter type | Parameter name | Required? | -|----|----|----| -| Path | ==containerName== | :white_check_mark: | - - ---- - -### DELETE: /frontend/remove-link/{==containerName==} - - -:::info -Removes the "link" string from the config\* - -::: - -| Parameter type | Parameter name | Required? | -|----|----|----| -| Path | ==cotnainerName== | :white_check_mark: | - - ---- - -### DELETE: /frontend/remove-icon/{==containerName==} - - -:::info -Removes the "icon" string from the config\* - -::: - -| Parameter type | Parameter name | Required? | -|----|----|----| -| Path | ==containerName== | :white_check_mark: | - -## API - -### GET: /api/hosts - - -:::info -Retrieves a JSON list of all available hosts - -::: - -| Parameter type | Parameter name | Required? | -|----|----|----| -| None | None | **⛔** | - - ---- - -### GET: /api/host/{==hostName==}/stats - - -:::info -Queries a specified host and provides data as a JSON structure - -::: - -Example response: - -```javascript -{ - "hostName": "XXX", - "info": { - "ID": "XXX", - "Containers": 19, - "ContainersRunning": 19, - "ContainersPaused": 0, - "ContainersStopped": 0, - "Images": 17, - "OperatingSystem": "Ubuntu 22.04.5 LTS", - "KernelVersion": "5.15.0-121-generic", - "Architecture": "x86_64", - "MemTotal": 8123764736, - "NCPU": 4 - }, - "version": { - "Components": { - "Engine": "27.3.1", - "containerd": "1.7.22", - "runc": "1.1.14", - "docker-init": "0.19.0" - } - } -} -``` - -| Parameter type | Parameter name | Required? | -|----|----|----| -| Path | ==hostName== | :white_check_mark: | - - ---- - -### GET: /api/containers - - -:::info -Queries all docker hosts directly and provides a JSON output - -::: - -Example response: - -```javascript -{ - "XXX": [ - { - "name": "portainer", - "id": "XXX", - "hostName": "XXX", - "state": "running", - "cpu_usage": 1670000, - "mem_usage": 10727424, - "mem_limit": 8123764736, - "net_rx": 584519242, - "net_tx": 27036706, - "current_net_rx": 584519242, - "current_net_tx": 27036706, - "networkMode": "docker-important_default" - } - ], - "YYY": [ - { - "name": "dozzle", - "id": "XXX", - "hostName": "YYY", - "state": "running", - "cpu_usage": 1670000, - "mem_usage": 10727424, - "mem_limit": 8123764736, - "net_rx": 584519242, - "net_tx": 27036706, - "current_net_rx": 584519242, - "current_net_tx": 27036706, - "networkMode": "default" - } - ] - } +> The DockStatAPI is implemented with Elysia and exposed under the prefix `/api/v2`. The canonical route definitions are in `apps/api/src/routes/` with schemas in `apps/api/src/models/`. + +## Base URL + +Development: `http://localhost:9876/api/v2` + +## Route Overview + +```mermaidjs +graph LR + subgraph /api/v2 + direction TB + DOCKER["/docker"] + METRICS["/metrics"] + PLUGINS["/plugins"] + DB["/db"] + end + + subgraph Docker Routes + DOCKER --> STATUS[GET /status] + DOCKER --> HOSTS["/hosts"] + DOCKER --> CLIENT["/client"] + DOCKER --> CONTAINERS["/containers"] + DOCKER --> MANAGER["/manager"] + end + + subgraph Host Routes + HOSTS --> H_LIST[GET /] + HOSTS --> H_GET[GET /:clientId] + HOSTS --> H_ADD[POST /add] + HOSTS --> H_UPDATE[POST /update] + end + + subgraph Client Routes + CLIENT --> C_REG[POST /register] + CLIENT --> C_DEL[DELETE /delete] + CLIENT --> C_ALL[GET /all/:stored] + CLIENT --> C_MON_START[POST /monitoring/:clientId/start] + CLIENT --> C_MON_STOP[POST /monitoring/:clientId/stop] + end ``` -| Parameter type | Parameter name | Required? | -|----|----|----| -| None | None | ⛔ | - - ---- - -### GET: /api/config - +## Docker Routes `/api/v2/docker` -:::info -Provides the current backend config as JSON +### GET `/docker/status` -::: +Returns the overall DockerClientManager status, including worker pool metrics. -Example response: +**Response 200:** -```javascript +```json { - "hosts": [ + "hosts": [{ "name": "string", "id": 1, "clientId": 1 }], + "totalWorkers": 4, + "activeWorkers": 2, + "totalHosts": 3, + "totalClients": 2, + "averageHostsPerWorker": 1, + "workers": [ { - "name": "XXX", - "url": "YYY", - "port": "ZZZ" + "workerId": 1, + "clientId": 1, + "clientName": "local", + "hostsManaged": 2, + "activeStreams": 0, + "isMonitoring": true, + "initialized": true, + "memoryUsage": { "rss": 0, "heapTotal": 0, "heapUsed": 0, "external": 0 }, + "uptime": 3600 } ] } ``` -| Parameter type | Parameter name | Required? | -|----|----|----| -| None | None | ⛔ | - - ---- - -### GET: /api/current-shedule - +### Hosts `/docker/hosts` -:::info -Retrieves and provides the current schedule settings in seconds - -::: +| Method | Path | Description | +|----|----|----| +| GET | `/hosts/` | List all hosts | +| GET | `/hosts/:clientId` | Get metrics for a specific client | +| POST | `/hosts/add` | Add a new host | +| POST | `/hosts/update` | Update an existing host | -Example response: +**POST** `**/hosts/add**` **body:** -```javascript +```json { - "interval": 300 + "clientId": 1, + "hostname": "docker.local", + "name": "Local Docker", + "secure": false, + "port": 2375 } ``` -| Parameter type | Parameter name | Required? | -|----|----|----| -| None | None | ⛔ | - - ---- - -### GET: /api/frontend-config +**POST** `**/hosts/update**` **body:** - -:::info -Provides the frontend config used for various settings - -::: - -Example response: - -```javascript -[ - { - "name": "XXX", - "hidden": true, - "tags": [ - "YYY" - ], - "pinned": true +```json +{ + "clientId": 1, + "host": { + "id": 1, + "host": "docker.local", + "name": "Updated Name", + "secure": true, + "port": 2376 } -] +} ``` -| Parameter type | Parameter name | Required? | -|----|----|----| -| None | None | ⛔ | - +### Client `/docker/client` ---- - -### GET: /api/status - - -:::info -Returns a 200 status with an "up" message to indicate the server is up and running. Used for Health checks - -::: +| Method | Path | Description | +|----|----|----| +| POST | `/client/register` | Register a new Docker client | +| DELETE | `/client/delete` | Remove a client | +| GET | `/client/all/:stored` | List all clients | +| POST | `/client/monitoring/:id/start` | Start monitoring for a client | +| POST | `/client/monitoring/:id/stop` | Stop monitoring for a client | -Example response: +**POST** `**/client/register**` **body:** ```json { - "status": "up" + "clientName": "production", + "options": null } ``` -| Parameter type | Parameter name | Required? | -|----|----|----| -| None | None | ⛔ | - +**Response 200:** ---- - -### PUT: /conf/addHost - - -:::info -Adds another host as target +```json +{ + "success": true, + "message": "Client registered", + "clientId": 1 +} +``` -::: +### Containers `/docker/containers` -| Response Code | Description | -|----|----| -| 200 | Host added successfully. | -| 400 | Bad request, invalid input. | -| 500 | An error occurred while adding the host. | - -| Parameter type | Parameter name | Required? | +| Method | Path | Description | |----|----|----| -| Query | name | :white_check_mark: | -| Query | URL | :white_check_mark: | -| Query | port | :white_check_mark: | - - ---- - -## Conf - -### PUT: /conf/scheduler - - -:::info -Set a new scheduler interval (has to be 5 minutes or more) +| GET | `/containers/all/:clientId` | Get all containers for a client | -::: +**Response:** Array of container objects with stats. -| Response code | Description | -|----|----| -| 200 | Fetch interval set successfully. | -| 400 | Invalid interval format or out of range. | +### Manager `/docker/manager` -| Parameter type | Parameter name | Required? | +| Method | Path | Description | |----|----|----| -| Query | interval | :white_check_mark: | - +| GET | `/manager/pool-stats` | Get worker pool statistics | +| POST | `/manager/init-all-clients` | Initialize all registered clients | ---- +## Metrics Routes `/api/v2/metrics` -### DELETE: /conf/removeHost +### GET `/metrics/` +Returns Prometheus-formatted metrics for the API and database. -:::info -Removes a specified host from the config +**Response 200:** Prometheus text format with: -::: - -| Response code | Description | -|----|----| -| 200 | Host removed successfully. | -| 404 | Host not found. | -| 500 | An error occurred while removing the host. | +* HTTP request counters +* Request duration histograms +* Database size and table metrics +* Memory usage statistics -| Parameter type | Parameter name | Required? | -|----|----|----| -| Query | hostName | :white_check_mark: | - - ---- +## Plugin Routes `/api/v2/plugins` -## Notification Services +```mermaidjs -### GET: /notification-service/get-template +sequenceDiagram + participant Client + participant API + participant PluginHandler + participant DB + Client->>API: POST /plugins/install + API->>PluginHandler: savePlugin() + PluginHandler->>DB: INSERT plugin + DB-->>PluginHandler: success + PluginHandler-->>API: { success: true, id: 1 } + API-->>Client: 200 OK -:::info -Retrieve the notification template + Client->>API: POST /plugins/activate + API->>PluginHandler: loadPlugins([1]) + PluginHandler->>DB: SELECT plugin code + PluginHandler->>PluginHandler: Dynamic import + PluginHandler-->>API: { successes: [1], errors: [] } + API-->>Client: 200 OK +``` -::: +| Method | Path | Description | +|----|----|----| +| GET | `/plugins/all` | List all installed plugins | +| GET | `/plugins/hooks` | Get available hook handlers | +| GET | `/plugins/status` | Get plugin system status | +| POST | `/plugins/install` | Install a plugin | +| POST | `/plugins/activate` | Activate plugins by ID | +| POST | `/plugins/delete` | Delete a plugin | +| GET | `/plugins/routes` | List plugin-provided routes | +| ALL | `/plugins/:id/routes/*` | Proxy requests to plugin Elysia instance | -Example response: +**POST** `**/plugins/install**` **body:** ```json { - "message": "{{name}} is {{state}}" + "name": "my-plugin", + "version": "1.0.0", + "description": "Plugin description", + "repoType": "github", + "repository": "user/repo", + "manifest": "manifest.yml", + "author": { "name": "Author", "email": "a@b.com" }, + "plugin": "/* JS code */" } ``` -| Parameter type | Parameter name | Required? | -|----|----|----| -| None | None | ⛔ | - - ---- - -### POST: /notification-service/set-template - +**POST** `**/plugins/activate**` **body:** -:::info -Update the notification text with templating functionality - -::: +```json +[1, 2, 3] +``` -Example Request body: +**Response 200:** -```javascript +```json { - "message": "string" + "successes": [1, 2], + "errors": [{ "pluginId": 3, "error": "..." }] } ``` -| Parameter type | Parameter name | Required? | +## Database Routes `/api/v2/db` + +| Method | Path | Description | |----|----|----| -| Request body | None | :white_check_mark: | +| GET | `/db/config` | Get current configuration | +| POST | `/db/config` | Update configuration | +**POST** `**/db/config**` **body:** Configuration object matching `DockStatConfigTable` schema from `@dockstat/typings`. ---- +## OpenAPI Documentation -### POST: /notification-service/test/{==type==}/{==containerId==} +The API exposes OpenAPI documentation via `@elysiajs/openapi` at `/api/v2/docs` using the Scalar provider. +## Error Handling -:::info -Send a test notification using an existing container as a data source for the template +All routes use a global error handler that returns structured errors: -::: +**Validation Error (400):** -> might change in the future to use a standart test message +```json +{ + "error": "Validation failed", + "path": "/api/v2/docker/hosts/add", + "timestamp": "2024-01-01T00:00:00.000Z" +} +``` -| Parameter type | Parameter name | Required? | -|----|----|----| -| Path | ==type==\* | **✅** | -| Path | ==containerId== | :white_check_mark: | +**Server Error (500):** -*Type\*: this is the notification service you are trying to test, for example: telegram, mail, pushbullet, …* +```json +{ + "error": "Response validation failed", + "message": "...", + "path": "/api/v2/...", + "timestamp": "..." +} +``` +## Authentication ---- +The current API implementation does not enforce authentication at the route level. For production deployments, add an authentication layer via reverse proxy or Elysia middleware. -## High Availability +## Source Files -WIP \ No newline at end of file +| File | Description | +|----|----| +| `apps/api/src/index.ts` | API entry point | +| `apps/api/src/routes/docker/index.ts` | Docker route aggregator | +| `apps/api/src/routes/docker/hosts.ts` | Host management routes | +| `apps/api/src/routes/docker/client.ts` | Client management routes | +| `apps/api/src/routes/docker/container.ts` | Container routes | +| `apps/api/src/routes/docker/manager.ts` | Manager routes | +| `apps/api/src/routes/plugins/index.ts` | Plugin routes | +| `apps/api/src/routes/db.ts` | Database configuration routes | +| `apps/api/src/routes/metrics/prometheus.ts` | Metrics endpoint | +| `apps/api/src/models/*.ts` | Request/response schemas | \ No newline at end of file diff --git a/apps/docs/dockstat/apps-overview/README.md b/apps/docs/dockstat/apps-overview/README.md new file mode 100644 index 00000000..8290019e --- /dev/null +++ b/apps/docs/dockstat/apps-overview/README.md @@ -0,0 +1,637 @@ +--- +id: fb89c77f-9f0a-497a-bb24-c41d21b37478 +title: Apps overview +collectionId: b4a5e48f-f103-480b-9f50-8f53f515cab9 +parentDocumentId: 7dddd764-6483-4f84-96a3-988304e772d3 +updatedAt: 2025-12-17T09:47:18.062Z +urlId: YM2LlgAuWf +--- + +> Complete guide to all applications in the DockStat monorepo. This document covers the purpose, architecture, and interaction patterns of each application. + +## Application Ecosystem + +```mermaidjs + +graph TB + subgraph "User Interface" + DS["dockstat
Frontend Application"] + end + + subgraph "Backend Services" + API["api
Backend API"] + DN["docknode
Remote Agent"] + end + + subgraph "Ecosystem" + DST["dockstore
Plugin Registry"] + DOCS["docs
Documentation"] + end + + subgraph "External" + DOCKER["Docker Daemons"] + BROWSER["Web Browser"] + end + + BROWSER --> DS + DS -->|"Eden Client"| API + API --> DOCKER + API --> DN + DN --> DOCKER + DST -->|"Plugin Bundles"| API + DOCS -->|"Outline Sync"| WIKI["Wiki"] +``` + +## Application Summary + +| Application | Type | Port | Purpose | +|----|----|----|----| +| `dockstat` | Frontend | 5173 (dev) / 3000 (prod) | Main user interface | +| `api` | Backend | 3000 | REST API and Docker management | +| `docknode` | Agent | 4000 | Remote Docker host management | +| `dockstore` | Registry | — | Plugin and template repository | +| `docs` | Documentation | — | Documentation and wiki sync | + +## dockstat (Frontend) + +### Overview + +The main DockStat frontend application built with React Router v7 for server-side rendering. Provides the user interface for container monitoring, management, and configuration. + +```mermaidjs + +graph TB + subgraph "Frontend Architecture" + ENTRY["entry.server.tsx"] + ROOT["root.tsx"] + ROUTES["routes/"] + API_CLIENT["api.ts (Eden)"] + end + + subgraph "Key Features" + MONITOR["Container Monitoring"] + MANAGE["Container Management"] + THEMES["Theme System"] + PLUGINS["Plugin UI"] + end + + subgraph "Technologies" + RR["React Router v7"] + TW["TailwindCSS"] + TS["TypeScript"] + BUN["Bun Runtime"] + end + + ENTRY --> ROOT + ROOT --> ROUTES + ROUTES --> API_CLIENT + API_CLIENT -->|"HTTP"| BACKEND["Backend API"] +``` + +### Directory Structure + +``` +apps/dockstat/ +├── app/ +│ ├── .server/ # Server-side utilities +│ ├── routes/ # React Router routes +│ ├── api.ts # Eden API client +│ ├── app.css # Global styles +│ ├── entry.client.tsx # Client entry point +│ ├── entry.server.tsx # Server entry point +│ ├── root.tsx # Root component +│ └── routes.ts # Route definitions +├── public/ # Static assets +├── build/ # Production build output +├── Dockerfile # Production container +├── react-router.config.ts +├── vite.config.ts +└── package.json +``` + +### Configuration + +```typescript +// react-router.config.ts + +import type { Config } from "@react-router/dev/config"; + +export default { + ssr: true, + future: { + // Enable future flags + } +} satisfies Config; +``` + +### Development + +```bash +cd apps/dockstat + +bun install + +bun run dev +# Available at http://localhost:5173 +``` + +### Production Build + +```bash +bun run build + +bun run start +# Available at http://localhost:3000 +``` + +### Key Features + +* **Server-Side Rendering**: Fast initial page loads with SSR +* **Type-Safe API Calls**: Eden client provides full type safety +* **Theme Support**: CSS variable-based theming system +* **Responsive Design**: TailwindCSS for responsive layouts +* **Hot Module Replacement**: Fast development iteration + + +--- + +## api (Backend) + +### Overview + +The DockStat backend API built with Elysia framework. Handles all Docker operations, plugin management, and data persistence. + +```mermaidjs + +graph TB + subgraph "API Architecture" + ENTRY["index.ts"] + PLUGINS["elysia-plugins.ts"] + ROUTES["routes/"] + HANDLERS["handlers/"] + end + + subgraph "Core Services" + DCM["DockerClientManager"] + PH["PluginHandler"] + DB["Database Layer"] + end + + subgraph "Route Groups" + DOCKER["/docker"] + PLUGIN["/plugins"] + METRICS["/metrics"] + CONFIG["/db"] + end + + ENTRY --> PLUGINS + ENTRY --> ROUTES + ROUTES --> DOCKER + ROUTES --> PLUGIN + ROUTES --> METRICS + ROUTES --> CONFIG + DOCKER --> DCM + PLUGIN --> PH + CONFIG --> DB +``` + +### Directory Structure + +``` +apps/api/ +├── src/ +│ ├── database/ # Database initialization +│ ├── docker/ # Docker client setup +│ ├── handlers/ # Request handlers +│ ├── middleware/ # Elysia middleware +│ ├── models/ # Typebox schemas +│ ├── plugins/ # Default plugins +│ ├── routes/ +│ │ ├── docker/ # Docker routes +│ │ ├── metrics/ # Prometheus metrics +│ │ └── plugins/ # Plugin routes +│ ├── utiles/ # Utility functions +│ ├── elysia-plugins.ts # Elysia plugin configuration +│ ├── index.ts # Main entry point +│ └── logger.ts # Logger setup +└── package.json +``` + +### Route Prefix + +All API routes are prefixed with `/api/v2`: + +| Route Group | Prefix | Purpose | +|----|----|----| +| Docker | `/api/v2/docker` | Container and host management | +| Plugins | `/api/v2/plugins` | Plugin administration | +| Metrics | `/api/v2/metrics` | Prometheus metrics endpoint | +| Database | `/api/v2/db` | Configuration management | + +### Configuration + +Environment variables: + +```bash +DOCKSTAT_MAX_WORKERS=200 # Max worker threads +DOCKSTATAPI_SHOW_TRACES=true # Enable server timing +DOCKSTATAPI_PORT=9876 # API port +``` + +### Development + +```bash +cd apps/api + +bun install + +bun run dev +# Available at http://localhost:9876 +``` + +### API Documentation + +OpenAPI documentation is available at `/api/v2/docs` using Scalar provider. + + +--- + +## docknode (Remote Agent) + +### Overview + +DockNode is a remote agent for managing Docker hosts that aren't directly accessible from the main DockStat instance. It provides secure stack deployment and management capabilities. + +```mermaidjs + +graph TB + subgraph "DockNode Architecture" + ENTRY["index.ts"] + DOCKSTACK["DockStackHandler"] + AUTH["Authentication"] + BUILDER["Stack Builder"] + end + + subgraph "Capabilities" + DEPLOY["Stack Deployment"] + DELETE["Stack Deletion"] + STATUS["Status Reporting"] + end + + subgraph "Security" + PSK["Pre-Shared Key"] + DEV["Dev Auth"] + end + + ENTRY --> DOCKSTACK + DOCKSTACK --> AUTH + DOCKSTACK --> BUILDER + AUTH --> PSK + AUTH --> DEV + BUILDER --> DEPLOY + BUILDER --> DELETE +``` + +### Directory Structure + +``` +apps/docknode/ +├── src/ +│ ├── handlers/ +│ │ ├── auth/ # Authentication handlers +│ │ └── dockstack/ # Stack management +│ ├── tests/ # Test files +│ ├── builder.ts # Docker Compose builder +│ └── index.ts # Main entry point +├── environment.d.ts # Type definitions +├── dockerfile # Production container +└── package.json +``` + +### API Endpoints + +| Method | Endpoint | Description | +|----|----|----| +| GET | `/api/status` | Health check | +| POST | `/api/dockstack/deploy` | Deploy a Docker Compose stack | +| DELETE | `/api/dockstack/delete` | Delete a deployed stack | +| GET | `/api/docs` | OpenAPI documentation | + +### Stack Deployment + +```typescript +// Deploy request body +{ + "id": 1, + "name": "my-stack", + "data": "version: '3.8'\nservices:\n ...", + "vars": { + "DOMAIN": "example.com", + "PORT": "8080" + } +} +``` + +### Authentication + +DockNode supports multiple authentication methods: + + +1. **Pre-Shared Key (PSK)**: For production environments +2. **Dev Auth**: For development and testing + +```bash +# Production + +DOCKNODE_DOCKSTACK_AUTH_PSK= +DOCKNODE_DOCKSTACK_AUTH_PRIORITY=psk + +# Development + +DOCKNODE_DOCKSTACK_DEV_AUTH=dev-key +DOCKNODE_DOCKSTACK_AUTH_PRIORITY=dev +``` + +### Development + +```bash +cd apps/docknode + +bun install + +bun run dev +# Available at http://localhost:4000 +``` + + +--- + +## dockstore (Plugin Registry) + +### Overview + +DockStore is the community hub for Docker Compose templates, themes, and plugins. It provides a curated repository of pre-built configurations. + +```mermaidjs + +graph TB + subgraph "DockStore Structure" + CONTENT["src/content/"] + PLUGINS["plugins/"] + UTILS[".utils/"] + end + + subgraph "Content Types" + TEMPLATES["Docker Compose Templates"] + THEME_FILES["Theme Files"] + PLUGIN_FILES["Plugin Bundles"] + end + + subgraph "Build Output" + DIST["dist/"] + SCHEMAS[".schemas/"] + end + + CONTENT --> PLUGINS + PLUGINS --> TEMPLATES + PLUGINS --> THEME_FILES + PLUGINS --> PLUGIN_FILES + PLUGINS -->|"Build"| DIST +``` + +### Directory Structure + +``` +apps/dockstore/ +├── src/ +│ ├── .utils/ # Build utilities +│ └── content/ +│ └── plugins/ # Plugin source files +├── .schemas/ # JSON schemas +├── dist/ # Built plugin bundles +├── bundler.ts # Plugin bundler +├── manifest.yml # DockStore manifest +└── package.json +``` + +### Available Plugins + +| Plugin | Description | Tags | +|----|----|----| +| `docknode-plugin` | DockNode connection handler | DockNode, Remote, fs | + +### Plugin Manifest Format + +```yaml +# manifest.yml + +name: my-plugin + +version: 1.0.0 + +description: Plugin description + +author: + name: Developer Name + email: dev@example.com + +repository: https://github.com/user/plugin + +tags: + - monitoring + - docker + +repoType: github +``` + +### Building Plugins + +```bash +cd apps/dockstore + +bun run build +# Output in dist/plugins/@/build.js +``` + + +--- + +## docs (Documentation) + +### Overview + +The documentation application handles documentation files and synchronization with Outline Wiki. It provides bi-directional sync between local markdown files and the online wiki. + +```mermaidjs + +graph LR + subgraph "Local" + MD["Markdown Files"] + CONFIG["outline-sync.config.json"] + end + + subgraph "Sync Process" + SYNC["outline-sync"] + end + + subgraph "Remote" + WIKI["Outline Wiki"] + end + + MD --> SYNC + CONFIG --> SYNC + SYNC <-->|"Bi-directional"| WIKI +``` + +### Directory Structure + +``` +apps/docs/ +├── dockstat/ +│ ├── api-reference/ # API documentation +│ ├── architecture/ # Architecture docs +│ ├── packages/ # Package documentation +│ │ ├── @dockstat-logger/ +│ │ ├── @dockstat-plugin-handler/ +│ │ └── @dockstat-typings/ +│ └── README.md # Main documentation +└── outline-sync.config.json +``` + +### Sync Configuration + +```json +{ + "apiUrl": "https://outline.itsnik.de", + "collectionId": "b4a5e48f-f103-480b-9f50-8f53f515cab9", + "docsPath": "./dockstat" +} +``` + +### Running Sync + +```bash +# Using @dockstat/outline-sync package + +bun run sync +``` + + +--- + +## Application Interactions + +### Data Flow + +```mermaidjs + +sequenceDiagram + participant User as "User" + participant FE as "dockstat (Frontend)" + participant API as "api (Backend)" + participant DN as "docknode (Agent)" + participant DST as "dockstore (Registry)" + participant Docker as "Docker" + + User->>FE: "Access UI" + FE->>API: "Fetch containers" + API->>Docker: "List containers" + Docker-->>API: "Container data" + API-->>FE: "JSON response" + FE-->>User: "Display containers" + + User->>FE: "Deploy stack to remote" + FE->>API: "Deploy request" + API->>DN: "Deploy stack" + DN->>Docker: "docker-compose up" + Docker-->>DN: "Success" + DN-->>API: "Deployment result" + API-->>FE: "Success response" + FE-->>User: "Stack deployed" + + User->>FE: "Install plugin" + FE->>API: "Install request" + API->>DST: "Fetch plugin bundle" + DST-->>API: "Plugin code" + API->>API: "Register plugin" + API-->>FE: "Plugin installed" + FE-->>User: "Plugin ready" +``` + +### Communication Protocols + +| From | To | Protocol | Purpose | +|----|----|----|----| +| Browser | dockstat | HTTP/HTTPS | UI serving | +| dockstat | api | HTTP (Eden) | API calls | +| api | Docker | Unix socket / TCP | Docker operations | +| api | docknode | HTTP | Remote management | +| api | dockstore | HTTP | Plugin fetching | + + +--- + +## Development Workflow + +### Starting All Applications + +```bash +# From monorepo root + +bun install + +# Start all in development mode + +bun run dev + +# Or start individually + +cd apps/dockstat && bun run dev + +cd apps/api && bun run dev + +cd apps/docknode && bun run dev +``` + +### Building for Production + +```bash +# Build all applications + +bun run build + +# Or build individually + +cd apps/dockstat && bun run build + +cd apps/api && bun run build + +cd apps/docknode && bun run build + +cd apps/dockstore && bun run build +``` + +### Testing + +```bash +# Run all tests + +bun run test + +# Run specific app tests + +cd apps/api && bun run test +``` + + +--- + +## Related Documentation + +| Section | Description | +|----|----| +| [Architecture](/doc/d56ca448-563a-4206-9585-c45f8f6be5cf) | System design and component relationships | +| [API Reference](/doc/b174143d-f906-4f8d-8cb5-9fc96512e575) | Complete API endpoint documentation | +| [Configuration](/doc/dec1cb2c-9a13-4e67-a31c-d3a685391208) | Environment variables and settings | +| [Integration guide](/doc/e4e04545-fd9f-4fbf-becb-94da81f48bc5) | How applications work together | +| [Packages](/doc/bbcefaa2-6bd4-46e8-ae4b-a6b823593e67) | Shared package documentation | \ No newline at end of file diff --git a/apps/docs/dockstat/architecture/README.md b/apps/docs/dockstat/architecture/README.md index ec6d3233..46f8acf5 100644 --- a/apps/docs/dockstat/architecture/README.md +++ b/apps/docs/dockstat/architecture/README.md @@ -1,17 +1,414 @@ --- -id: 81ab727f-9ea0-4214-a8e3-87b476d243d9 +id: d56ca448-563a-4206-9585-c45f8f6be5cf title: Architecture collectionId: b4a5e48f-f103-480b-9f50-8f53f515cab9 parentDocumentId: 7dddd764-6483-4f84-96a3-988304e772d3 -updatedAt: 2025-09-13T17:31:01.541Z -urlId: k2XSamXcBq +updatedAt: 2025-12-16T19:17:04.228Z +urlId: g3eBa2Z8rL --- -DockStat consists of multiple Abstraction Layers such as `Adapters` and `Plugins`. +> DockStat is a modular Docker monitoring platform built as a monorepo. The system follows a layered architecture with clear separation between the frontend, backend API, Docker integration, and persistence layers. +## System Overview -# Adapters +```mermaidjs -Adapters are connections to integrated services to manages, such as `Docker` and other Integrations like `Docker-Swarm`, `Kubernetes` and more are Planned. +graph TB + subgraph User Layer + Browser[Web Browser] + end -All Adapters are declared in \ No newline at end of file + subgraph Frontend ["Frontend (apps/dockstat)"] + RR[React Router SSR] + Eden[Eden Client] + UI["@dockstat/ui"] + end + + subgraph API ["Backend API (apps/api)"] + Elysia[Elysia Server] + Routes[Route Handlers] + Models[Typebox Models] + Middleware[Metrics Middleware] + end + + subgraph Core ["Core Services"] + DCM[DockerClientManager] + PH[PluginHandler] + DBL[Database Layer] + end + + subgraph Packages + DC["@dockstat/docker-client"] + PHPkg["@dockstat/plugin-handler"] + DB["@dockstat/db"] + SW["@dockstat/sqlite-wrapper"] + LOG["@dockstat/logger"] + end + + subgraph External + Docker[Docker Daemon] + SQLite[(SQLite DB)] + end + + Browser --> RR + RR --> Eden + Eden -->|HTTP| Elysia + Elysia --> Routes + Routes --> DCM + Routes --> PH + Routes --> DBL + DCM --> DC + PH --> PHPkg + DBL --> DB + DB --> SW + SW --> SQLite + DC -->|Docker API| Docker + LOG -.->|Logging| Elysia + LOG -.-> DCM + LOG -.-> PH +``` + +## Component Architecture + +### Frontend (`apps/dockstat`) + +The frontend is a server-rendered React application using React Router v7. It communicates with the backend via the Eden client (type-safe Elysia client). + +```mermaidjs + +graph LR + subgraph SSR + Entry[entry.server.tsx] + Root[root.tsx] + Routes[routes/] + end + + subgraph Client + EntryC[entry.client.tsx] + API[api.ts] + end + + Entry --> Root + Root --> Routes + EntryC --> Root + API -->|Eden| Backend[Backend API] +``` + +Key technologies: + +* React Router v7 with SSR +* TailwindCSS for styling +* Eden client for type-safe API calls +* Bun runtime + +### Backend API (`apps/api`) + +The API is an Elysia server with the prefix `/api/v2`. Routes are organized by domain. + +```mermaidjs + +graph TB + subgraph Elysia App + Main[index.ts] + Plugins[elysia-plugins.ts] + ErrorHandler[handlers/onError.ts] + end + + subgraph Routes + DockerR[routes/docker/] + PluginsR[routes/plugins/] + DBR[routes/db.ts] + MetricsR[routes/metrics/] + end + + subgraph Services + DCM[DockerClientManager] + PH[PluginHandler] + DockStatDB[Database] + end + + Main --> Plugins + Main --> ErrorHandler + Main --> DockerR + Main --> PluginsR + Main --> DBR + Main --> MetricsR + DockerR --> DCM + PluginsR --> PH + DBR --> DockStatDB +``` + +Route structure: + +* `/docker` — Container and host management +* `/plugins` — Plugin administration +* `/db` — Configuration persistence +* `/metrics` — Prometheus metrics + +### Docker Client Manager + +The `DockerClientManager` from `@dockstat/docker-client` manages connections to Docker daemons. It uses a worker pool architecture for scalability. + +```mermaidjs + +sequenceDiagram + participant API + participant DCM as DockerClientManager + participant Worker as Worker Thread + participant Docker as Docker Daemon + + API->>DCM: registerClient(name, options) + DCM->>DCM: Create worker + DCM-->>API: { clientId: 1 } + + API->>DCM: startMonitoring(clientId) + DCM->>Worker: Start monitoring loop + Worker->>Docker: GET /containers/json + Docker-->>Worker: Container list + Worker->>Docker: GET /containers/:id/stats + Docker-->>Worker: Stats stream + Worker-->>DCM: Emit events +``` + +Features: + +* Multi-host support +* Real-time container statistics +* Event-driven monitoring +* Connection pooling +* Automatic reconnection + +### Plugin System + +The `PluginHandler` from `@dockstat/plugin-handler` manages plugin lifecycle. + +```mermaidjs + +stateDiagram-v2 + [*] --> Installed: POST /plugins/install + Installed --> Loaded: POST /plugins/activate + Loaded --> Running: Plugin initialized + Running --> Loaded: Unload + Loaded --> Installed: Deactivate + Installed --> [*]: POST /plugins/delete +``` + +Plugin capabilities: + +* Custom API routes via Elysia instances +* Database tables via SQLite wrapper +* Event hooks for container lifecycle +* Action chains for request handling + +### Database Layer + +Data persistence uses SQLite via `@dockstat/sqlite-wrapper`. The `@dockstat/db` package provides a higher-level abstraction for configuration and themes. + +```mermaidjs + +erDiagram + config { + int id PK + string current_theme_name + } + + themes { + string name PK + string version + string creator + string license + json vars + } + + plugins { + int id PK + string name UK + string version + string repoType + string repository + string manifest + json author + json tags + text plugin + } + + hosts { + int id PK + int docker_client_id FK + string name + string host + int port + boolean secure + } + + docker_clients { + int id PK + string name UK + json options + boolean initialized + } +``` + +## Data Flow + +### Container Stats Request + +```mermaidjs + +sequenceDiagram + participant Browser + participant Frontend + participant API + participant DCM + participant Docker + + Browser->>Frontend: Load dashboard + Frontend->>API: GET /api/v2/docker/containers/all/1 + API->>DCM: getAllContainers(1) + DCM->>Docker: GET /containers/json + Docker-->>DCM: Container list + DCM->>Docker: GET /containers/:id/stats (per container) + Docker-->>DCM: Stats + DCM-->>API: Aggregated stats + API-->>Frontend: JSON response + Frontend-->>Browser: Render stats +``` + +### Plugin Route Proxy + +```mermaidjs + +sequenceDiagram + participant Client + participant API + participant PluginHandler + participant PluginElysia + + Client->>API: GET /api/v2/plugins/1/routes/custom + API->>PluginHandler: handleRoute(1, "/custom", request) + PluginHandler->>PluginHandler: Lookup loaded plugin + PluginHandler->>PluginElysia: Forward request + PluginElysia-->>PluginHandler: Response + PluginHandler-->>API: Response + API-->>Client: JSON response +``` + +## Package Dependencies + +```mermaidjs + +graph BT + subgraph Apps + API[apps/api] + DS[apps/dockstat] + DN[apps/docknode] + DST[apps/dockstore] + end + + subgraph Core Packages + DC["@dockstat/docker-client"] + DB["@dockstat/db"] + PH["@dockstat/plugin-handler"] + end + + subgraph Foundation + SW["@dockstat/sqlite-wrapper"] + LOG["@dockstat/logger"] + TYP["@dockstat/typings"] + UTILS["@dockstat/utils"] + end + + subgraph UI + UIPKG["@dockstat/ui"] + end + + API --> DC + API --> DB + API --> PH + API --> LOG + API --> TYP + + DS --> API + DS --> UIPKG + DS --> DB + DS --> DC + DS --> LOG + DS --> TYP + + DN --> LOG + + DST --> PH + DST --> TYP + DST --> LOG + + DC --> SW + DC --> PH + DC --> LOG + DC --> TYP + DC --> UTILS + + DB --> SW + DB --> TYP + + PH --> SW + PH --> LOG + PH --> TYP + + UIPKG --> TYP + UIPKG --> UTILS +``` + +## Deployment Architecture + +```mermaidjs + +graph TB + subgraph Production + + subgraph DockStat Container + API[DockStat API] + FE[Frontend SSR] + end + DB[(SQLite Volume)] + + end + + subgraph Docker Hosts + DH1[Docker Host 1] + DH2[Docker Host 2] + DH3[Docker Host 3] + end + + + FE --> API + API --> DB + API --> DH1 + API --> DH2 + API --> DH3 +``` + +For production deployments: + + +1. Run the frontend and API together or separately +2. Mount SQLite database as a persistent volume +3. Configure Docker socket access or TCP endpoints +4. Set environment variables for logging and worker limits + +## Security Considerations + +The API does not enforce authentication by default (yet). For production: + +* Add authentication middleware to Elysia +* Use a reverse proxy with auth (nginx, Traefik) +* Restrict Docker socket access +* Use TLS for Docker TCP connections (`secure: true`) + +## Extension Points + +| Extension Point | Description | +|----|----| +| Plugins | Custom routes, DB tables, event hooks | +| Themes | UI theming via `@dockstat/db` | +| Docker Adapters | Extend `@dockstat/docker-client` | +| UI Components | Add to `@dockstat/ui` | \ No newline at end of file diff --git a/apps/docs/dockstat/archive/README.md b/apps/docs/dockstat/archive/README.md deleted file mode 100644 index 55098bd8..00000000 --- a/apps/docs/dockstat/archive/README.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -id: 280f8e0a-92e7-4825-8dc0-9a8c886e3d17 -title: Archive -collectionId: b4a5e48f-f103-480b-9f50-8f53f515cab9 -parentDocumentId: null -updatedAt: 2025-08-19T16:53:07.418Z -urlId: kJ7y6DBDl1 ---- - -Here you can find all older, unsupported versions of DockStat. \ No newline at end of file diff --git a/apps/docs/dockstat/archive/contribute/README.md b/apps/docs/dockstat/archive/contribute/README.md deleted file mode 100644 index 87f06e5e..00000000 --- a/apps/docs/dockstat/archive/contribute/README.md +++ /dev/null @@ -1,233 +0,0 @@ ---- -id: af6ef73c-2e42-457f-bbf9-00ecb01a3833 -title: Contribute -collectionId: b4a5e48f-f103-480b-9f50-8f53f515cab9 -parentDocumentId: 280f8e0a-92e7-4825-8dc0-9a8c886e3d17 -updatedAt: 2025-08-18T22:50:57.027Z -urlId: 7yJnHJSw4D ---- - -Please see this [GitHub topic](https://github.com/topics/dockstat) to find all DockStat components. - -# :whale: DockStat - -## Summary - -DockStat is built using [Remix](https://remix.run), which is based on React. There is no UI Component library, everything is custom-made. - -## How to Contribute - - -1. **Fork the Repository**: - * Fork this repository to your GitHub account. - * Clone the forked repository to your local machine. -2. **Create a New Branch**: - * Name your branch descriptively, e.g., `add-new-route` or `fix-bug-in-logging`. - - - 1. Example: - - ```bash - git checkout -b my-change - ``` -3. Set up the environment - * Install [Bun](https://bun.sh) if you haven't already. - * Run: `bun install` - * Start dev environment: - * Frontend: `bun dev:client` - * Backend: `bun dev:server` -4. **Make Your Changes** -5. **Test Your Changes** - * Verify that your changes work as expected locally. - * Lint your files (using Biome): `bun lint` -6. **Commit Your Changes** - * Write clear and concise commit messages. - * Example: - - ```bash - git add . - git commit -m "Change grid layout" - ``` -7. **Push and Submit a Pull Request**: - * Push your changes to your fork: - - ```bash - git push origin change-this-and-that - ``` - * Open a pull request (PR) from your branch to the `dev` branch of this repository. - - ---- - -## Reporting Issues - -* Use the [GitHub Issues](https://github.com/its4nik/dockstak/issues) page to report bugs or suggest improvements. -* Provide a clear and detailed description: - * Steps to reproduce (if a bug). - * Expected behavior and actual behavior. - * Screenshots (if applicable). - - ---- - -# :gear: DockStatAPI - -## Summary - -DockStatAPI is the backbone of DockStat. Every functionality (except DockStacks) runs through here. The API is pretty complex by now, offering multiple functions and websocket router for metrics, Stack deployment and more. - -The API is built using [Bun](https://bun.sh). - -## How to Contribute - - -1. **Fork the Repository**: - * Fork this repository to your GitHub account. - * Clone the forked repository to your local machine. -2. **Create a New Branch**: - * Name your branch descriptively, e.g., `add-new-route` or `fix-bug-in-logging`. - - - 1. Example: - - ```bash - git checkout -b my-change - ``` -3. Set up the environment - * Install [Bun](https://bun.sh) if you haven't already. - * Run: `bun install` - * Start dev environment: `bun dev` -4. **Make Your Changes** -5. **Test Your Changes** - * Verify that your changes work as expected locally. - * Add Unit tests - * Run the unit tests: `bun test` - * Lint your files (using Biome): `bun lint` -6. **Commit Your Changes** - * Write clear and concise commit messages. - * Example: - - ```bash - git add . - git commit -m "Add template for Redis stack" - ``` -7. **Push and Submit a Pull Request**: - * Push your changes to your fork: - - ```bash - git push origin add-my-template - ``` - * Open a pull request (PR) from your branch to the `dev` branch of this repository. - - ---- - -## Reporting Issues - -* Use the [GitHub Issues](https://github.com/its4nik/dockstack/issues) page to report bugs or suggest improvements. -* Provide a clear and detailed description: - * Steps to reproduce (if a bug). - * Expected behavior and actual behavior. - * Screenshots (if applicable). - - ---- - -## Suggesting Features - -* Before suggesting a new feature, check if a similar suggestion already exists in the [Issues](https://github.com/its4nik/dockstack/issues). -* If not, open a new issue and provide: - * A detailed explanation of the feature. - * A brief example or use case. - - ---- - -# :shopping_trolley: DockStacks - -## How to Contribute - - -1. **Fork the Repository**: - * Fork this repository to your GitHub account. - * Clone the forked repository to your local machine. -2. **Create a New Branch**: - * Name your branch descriptively, e.g., `add-new-template` or `fix-bug-in-grid`. - * Example: - - ```bash - git checkout -b add-my-template - ``` -3. **Make Your Changes**: - * Ensure all new templates follow the folder structure: - - ```txt - template/{STACK_NAME}/ - ├── schema.json - ├── README.md - ├── icon.{svg|png} # Optional - └── DESCRIPTION.md - ``` - * Validate JSON schema files using online or CLI JSON schema validators (IDE integration works best for me!). -4. **Test Your Changes**: - * Verify that your changes work as expected locally. - * Check the appearance of the grid and modal (if applicable). -5. **Commit Your Changes**: - * Write clear and concise commit messages. - * Example: - - ```bash - git commit -m "Add template for Redis stack" - ``` -6. **Push and Submit a Pull Request**: - * Push your changes to your fork: - - ```bash - git push origin add-my-template - ``` - * Open a pull request (PR) from your branch to the `main` branch of this repository. - - ---- - -## Reporting Issues - -* Use the [GitHub Issues](https://github.com/its4nik/dockstacks/issues) page to report bugs or suggest improvements. -* Provide a clear and detailed description: - * Steps to reproduce (if a bug). - * Expected behavior and actual behavior. - * Screenshots (if applicable). - - ---- - -## Suggesting Features - -* Before suggesting a new feature, check if a similar suggestion already exists in the [Issues](https://github.com/its4nik/dockstacks/issues). -* If not, open a new issue and provide: - * A detailed explanation of the feature. - * A brief example or use case. - - ---- - -## Submitting Templates - -When submitting a new template: - - -1. Create a new folder in the `template/` directory named after the stack (e.g., `redis`, `nginx`), please only use lowercase letters. -2. Add the required files: - * `**schema.json**`: Follows the repository's JSON schema format. - * `**README.md**`: Describes the stack, its use, and configuration options. - * `**DESCRIPTION.md**`: A short description for the stack (max 50 characters). -3. Ensure your files are formatted correctly: - * Validate `schema.json` using a JSON schema validator. - * Ensure `README.md` is clear, concise, and well-structured. - - ---- - -## Need Help? - -Feel free to ask questions or seek help in the [Discussions](https://github.com/its4nik/dockstacks/discussions) section. \ No newline at end of file diff --git a/apps/docs/dockstat/archive/dockstatapi-v1/README.md b/apps/docs/dockstat/archive/dockstatapi-v1/README.md deleted file mode 100644 index 966bd2f2..00000000 --- a/apps/docs/dockstat/archive/dockstatapi-v1/README.md +++ /dev/null @@ -1,24 +0,0 @@ ---- -id: 88a86081-76f3-4056-985f-b0800ec2445f -title: DockStatAPI v1 -collectionId: b4a5e48f-f103-480b-9f50-8f53f515cab9 -parentDocumentId: 280f8e0a-92e7-4825-8dc0-9a8c886e3d17 -updatedAt: 2025-08-18T22:50:50.845Z -urlId: jLcVCfPNmS ---- - -# ! Deprecated when v2 is launched ! - -See the following documents: - - -:::info -DockStat v1 will only receive security updates from dependabot - -::: - - -:::warning -As soon as DockStat v2 and DockStatAPI v2 are finished the branches will be pushed into main and the old v1 branches will be archived. - -::: \ No newline at end of file diff --git a/apps/docs/dockstat/archive/dockstatapi-v1/backend-api-reference/README.md b/apps/docs/dockstat/archive/dockstatapi-v1/backend-api-reference/README.md deleted file mode 100644 index 8142b4ab..00000000 --- a/apps/docs/dockstat/archive/dockstatapi-v1/backend-api-reference/README.md +++ /dev/null @@ -1,289 +0,0 @@ ---- -id: 0adaab7d-0b53-4d3e-9667-0dd0a0f719fd -title: Backend API reference -collectionId: b4a5e48f-f103-480b-9f50-8f53f515cab9 -parentDocumentId: 88a86081-76f3-4056-985f-b0800ec2445f -updatedAt: 2025-08-18T22:50:51.287Z -urlId: YzcBbDvY33 ---- - -# Authentication - -The Authentication uses a token inside the header of the request, more examples here: - -## 1 React: - -```jsx -useEffect(() => { - if (!apihost || !apiKey) return; - const fetchData = async () => { - try { - const response = await fetch(`${apihost}/config`, { - method: 'GET', - headers: { - 'Authorization': `${apiKey}`, - }, - }); - if (!response.ok) throw new Error('Failed to fetch data'); - const data = await response.text(); - setResult(data); - } catch (error) { - console.error('Error fetching data:', error); - } - }; - fetchData(); -}, [apihost, apiKey]); -``` - -## 2 Bash (curl) - -```bash -curl -X GET "${apihost}/config" -H "Authorization: ${apiKey}" -``` - -## 3 JavaScript: - -```javascript -if (apihost && apiKey) { - (function fetchData() { - fetch(`${apihost}/config`, { - method: 'GET', - headers: { - 'Authorization': `${apiKey}`, - }, - }) - .then(response => { - if (!response.ok) throw new Error('Failed to fetch data'); - return response.text(); - }) - .then(data => { - setResult(data); - }) - .catch(error => { - console.error('Error fetching data:', error); - }); - })(); -} -``` - -## 4 TypeScript: - -```typescript -useEffect(() => { - if (!apihost || !apiKey) return; - - const fetchData = async (): Promise => { - try { - const response = await fetch(`${apihost}/config`, { - method: 'GET', - headers: { - 'Authorization': `${apiKey}`, - }, - }); - - if (!response.ok) throw new Error('Failed to fetch data'); - - const data = await response.text(); - setResult(data); - } catch (error) { - console.error('Error fetching data:', error); - } - }; - - fetchData(); -}, [apihost, apiKey]); -``` - -## 5. **XMLHttpRequest (Standard JavaScript without Fetch API):** - -```javascript -if (apihost && apiKey) { - const xhr = new XMLHttpRequest(); - xhr.open('GET', `${apihost}/config`, true); - xhr.setRequestHeader('Authorization', `${apiKey}`); - - xhr.onreadystatechange = function () { - if (xhr.readyState === 4) { - if (xhr.status === 200) { - setResult(xhr.responseText); - } else { - console.error('Error fetching data'); - } - } - }; - xhr.send(); -} -``` - -# Endpoints - -| Endpoints | Authentication | Method | Documentation | -|----|----|----|----| -| /stats | yes | GET | [🌐 Backend API reference](/doc/backend-api-reference-YzcBbDvY33#h-stats) | -| /hosts | yes | GET | [🌐 Backend API reference](/doc/backend-api-reference-YzcBbDvY33#h-hosts) | -| /config | yes | GET | [🌐 Backend API reference](/doc/backend-api-reference-YzcBbDvY33#h-config) | -| /status | no | GET | [🌐 Backend API reference](/doc/backend-api-reference-YzcBbDvY33#h-status) | - -# `/stats` - -## Description: - -The `/stats` Endpoint is used to provide all statistics regarding the docker containers of an host. - -## Example: - -```json -{ - "YourHost1": [ - { - "name": "dockstat-demo", - "id": "2ec35ef9d8789c09cb0bbe820d099f9794a525943e494991b3608f8aaee44466", - "hostName": "YourHost1", - "state": "running", - "cpu_usage": 49494987000, - "mem_usage": 31481856, - "mem_limit": 8123764736, - "net_rx": 224714, - "net_tx": 648853, - "current_net_rx": 224714, - "current_net_tx": 648853, - "networkMode": "docker-important_default", - "link": "", - "icon": "", - "tags": "" - }, - { - "name": "dockstat", - "id": "237ed865f1bfbe6675d29dff5d0aaed0873be87d7b513190144eb41ec6a69060", - "hostName": "YourHost1", - "state": "running", - "cpu_usage": 50673614000, - "mem_usage": 33038336, - "mem_limit": 8123764736, - "net_rx": 460148, - "net_tx": 441701, - "current_net_rx": 460148, - "current_net_tx": 441701, - "networkMode": "docker-important_default", - "link": "", - "icon": "", - "tags": "" - } - ], - "YourHost2": [ - { - "name": "traefik", - "id": "81cdf86c9db1576bc5e2a296db9a285b580d93a4407568e28ad6f071c70d389c", - "hostName": "YourHost2", - "state": "running", - "cpu_usage": 2478699136000, - "mem_usage": 270741504, - "mem_limit": 8127897600, - "net_rx": 2336032715, - "net_tx": 3340317255, - "current_net_rx": 2336032715, - "current_net_tx": 3340317255, - "networkMode": "container:cc8f93acd96aaf2045bfab2908f71084ac30cc1d62f850685bb805fd0df45e7f", - "link": "", - "icon": "", - "tags": "" - }, - { - "name": "nginx", - "id": "b77c049de4dc3a5aed6ab3be08cb75f6d9a1d26f7d6f41df1605a57498157b22", - "hostName": "YourHost2", - "state": "running", - "cpu_usage": 984765321000, - "mem_usage": 141975552, - "mem_limit": 8127897600, - "net_rx": 2336054780, - "net_tx": 3340344618, - "current_net_rx": 2336054780, - "current_net_tx": 3340344618, - "networkMode": "container:cc8f93acd96aaf2045bfab2908f71084ac30cc1d62f850685bb805fd0df45e7f", - "link": "", - "icon": "", - "tags": "" - } - ] -} -``` - - ---- - -# `/hosts` - -## Description: - -This endpoint provides general information for each host the DockStatAPI is targeted at. - -## Example: - -```json -{ - "YourHost1": { - "containerCount": 17, - "totalCPUs": 4, - "totalMemory": 8123764736, - "cpuUsage": 30746841089000, - "memoryUsage": "19.24" - }, - "YourHost2": { - "containerCount": 15, - "totalCPUs": 4, - "totalMemory": 8127897600, - "cpuUsage": 56829592024000, - "memoryUsage": "62.14" - } -} -``` - - ---- - -# `/config` - -## Description: - -The `/config` endpoint just provides the local config of the DockStatAPI endpoint. - -## Example: - -```yaml -mintimeout: 10000 # The minimum time to wait before querying the same server again, defaults to 5000 Ms - -log: - logsize: 10 # Specify the Size of the log files in MB, default is 1MB - LogCount: 1 # How many log files should be kept in rotation. Default is 5 - -tags: - raspberry: red-200 - private: violet-400 - -hosts: - YourHost1: - url: 1.1.1.1 - port: 2375 - - YourHost2: - url: 2.2.2.2 - port: 2375 - -container: - dozzle: # Container name - link: https://github.com - icon: minecraft.png - tags: private,raspberry -``` - - ---- - -# `/status` - -## Description - -This is a simple Endpoint used by the docker health-check to se if the container is up. - -This endpoint will just provide "UP" and a status code of 200 if the server is running. \ No newline at end of file diff --git a/apps/docs/dockstat/archive/dockstatapi-v1/integrations/README.md b/apps/docs/dockstat/archive/dockstatapi-v1/integrations/README.md deleted file mode 100644 index dc11f46f..00000000 --- a/apps/docs/dockstat/archive/dockstatapi-v1/integrations/README.md +++ /dev/null @@ -1,49 +0,0 @@ ---- -id: 112822b1-97bd-4086-a9ae-87b095bc7c7b -title: Integrations -collectionId: b4a5e48f-f103-480b-9f50-8f53f515cab9 -parentDocumentId: 88a86081-76f3-4056-985f-b0800ec2445f -updatedAt: 2025-08-18T22:50:51.764Z -urlId: Agq1oL6HxF ---- - -DockStat will provide some integrations like [cup](https://github.com/sergi0g/cup) and more. - -# [cup](https://github.com/sergi0g/cup) - -## Description: ![Example image](/api/attachments.redirect?id=964f512c-940a-4ee8-8ece-32c438d0ce45 "right-50 =460x231") - -Cup is the easiest way to check for container image updates. - -## Cup's features: - -* Extremely fast. Cup takes full advantage of your CPU and is highly optimized, resulting in lightning fast speed. On my test machine, it took \~12 seconds for \~95 images. -* Supports most registries, including Docker Hub, ghcr.io, Quay, lscr.io and even Gitea (or derivatives) -* Doesn't exhaust any rate limits. This is the original reason I created Cup. It was inspired by [What's up docker?](https://github.com/fmartinou/whats-up-docker) which would always use it up. -* Beautiful CLI and web interface for checking on your containers any time. -* The binary is tiny! At the time of writing it's just 5.1 MB. No more pulling 100+ MB docker images for a such a simple program. -* JSON output for both the CLI and web interface so you can connect Cup to integrations. It's easy to parse and makes webhooks and pretty dashboards simple to set up! - -## How to integrate? - -As with all configuration regarding data gathering we are going to rely on the DockStatAPI. - -We have to run the cup container, but don't very since it is written in #Rust you are not going to take a performance impact! - -To define the Cup Host I've added a new environment variable to the DockStatAPI container: - -```yaml - dockstatapi: - image: ghcr.io/its4nik/dockstatapi:latest - container_name: dockstatapi - environment: - - SECRET="CHANGEME" # This is required in the header 'Authorization': 'CHANGEME' - - CUP_URL="https://your-cup-host.com" - ports: - - "7070:7070" - volumes: - - ./dockstat/api:/api/config # Place your hosts.yaml file here - restart: always -``` - -That's it! Now you see all available updates at a glance \ No newline at end of file diff --git a/apps/docs/dockstat/archive/dockstatapi-v3/README.md b/apps/docs/dockstat/archive/dockstatapi-v3/README.md deleted file mode 100644 index 02ed70d1..00000000 --- a/apps/docs/dockstat/archive/dockstatapi-v3/README.md +++ /dev/null @@ -1,108 +0,0 @@ ---- -id: f33a7ed1-f6f9-48f9-a393-e150feb09d2f -title: DockStatAPI v3 -collectionId: b4a5e48f-f103-480b-9f50-8f53f515cab9 -parentDocumentId: 280f8e0a-92e7-4825-8dc0-9a8c886e3d17 -updatedAt: 2025-08-18T22:50:52.228Z -urlId: uHl1SoOiyt ---- - -![](/api/attachments.redirect?id=f0ef5b94-2e4c-47a2-9a01-3dd799acb5cf " =708.5x156.25") ![CC BY-NC 4.0 License](https://img.shields.io/badge/License-CC_BY--NC_4.0-lightgrey.svg " =238x34") - - ---- - -# DockStatAPI - -Docker monitoring API with real-time statistics, stack management, and plugin support. - -## Features - -* Real-time container metrics via WebSocket -* Multi-host Docker environment monitoring -* Compose stack deployment/management -* Plugin system for custom logic/notifications -* Historical stats storage (SQLite) -* Swagger API documentation -* Web dashboard (WIP) - -## Tech Stack - -* **Runtime**: [Bun.sh](http://Bun.sh) -* **Framework**: [Elysia.js](https://elysiajs.com/) -* **Database**: SQLite (WAL mode) -* **Docker**: dockerode + compose -* **Monitoring**: Custom metrics collection -* **Auth**: [Authentication](/doc/793112f8-b6d8-4e92-a20d-395995e84486) - -## Available Sub-documentations - -* [Database](/doc/9d7c53bf-b335-4567-a4cc-76388a903020) -* [Plugin Development](/doc/a2b23dbc-0f70-49ef-ad33-73e8421860c7) -* [WebSocket](/doc/5a552211-a8fa-44ce-b816-de587a5caa64) -* [Stacks](/doc/970cdf56-b108-4468-8d8a-4c9b7d71c2c3) -* [Contribute](/doc/19ddc854-ab6c-4d8b-93e7-9f8d2ced1a56) -* [Background Tasks](/doc/3e9d366d-9001-4448-bad8-30f5ff4eb784) -* [Authentication](/doc/793112f8-b6d8-4e92-a20d-395995e84486) - -## Quick Start - -```bash -# Clone the Repo -git clone git@github.com:Its4Nik/DockStatAPI.git - -# Install Dependencies -bun install - -# Start the Server -bun start - -# Access endpoints -curl http://localhost:3000/health -``` - -## Configuration - -Set via API endpoints or initial DB setup: - -* Data retention policies -* Docker host connections - -## API Documentation - -Available at `/swagger` when running: - - ![Swagger](/api/attachments.redirect?id=5cbf821a-7899-499f-9d1b-8bf938aa3107) - -## Development - -Please see [Contribute](/doc/19ddc854-ab6c-4d8b-93e7-9f8d2ced1a56) - - ---- - -## Screenshots - - ![Swagger](/api/attachments.redirect?id=966f40a2-55e5-44d0-a0aa-86fa656ff804) - - ---- - - ![Swagger - GET /docker-config/hosts](/api/attachments.redirect?id=6cb14d19-882e-4c96-8809-2709354216e0) - - ---- - - ![SQLite Web](/api/attachments.redirect?id=2254c3a7-7d9f-4cda-bab3-cf7dbca364e7) ![SQLite Web - Content Viewer](/api/attachments.redirect?id=a0bdd96c-5de8-4139-8b1a-679629344c0b) - - ---- - - ![Custom 404 Error page](/api/attachments.redirect?id=256f9147-4b4f-4e1d-a395-ca8303435986 " =1208x806") - - ---- - -## Project Structure Graph - - ![](https://raw.githubusercontent.com/Its4Nik/DockStatAPI/refs/heads/dev/dependency-graph.svg) \ No newline at end of file diff --git a/apps/docs/dockstat/archive/dockstatapi-v3/authentication/README.md b/apps/docs/dockstat/archive/dockstatapi-v3/authentication/README.md deleted file mode 100644 index 6df97e58..00000000 --- a/apps/docs/dockstat/archive/dockstatapi-v3/authentication/README.md +++ /dev/null @@ -1,62 +0,0 @@ ---- -id: 793112f8-b6d8-4e92-a20d-395995e84486 -title: Authentication -collectionId: b4a5e48f-f103-480b-9f50-8f53f515cab9 -parentDocumentId: f33a7ed1-f6f9-48f9-a393-e150feb09d2f -updatedAt: 2025-08-18T22:50:54.146Z -urlId: VSGhxqjtXf ---- - -# Overview - -* **API Key Header**: `x-api-key` -* **Security**: Keys are hashed using Bun's secure password hashing -* **Bypass**: Authentication is skipped in non-production environments (***NODE_ENV !== "production"***) - -# Getting Started - - -1. **Default Key**: - * Initial API key is `changeme` (change immediately in production) - * Update via `/config/update` endpoint -2. **Using the API**: - -```bash -curl -H "x-api-key: YOUR_API_KEY" http://localhost:3000/config -# or for websocat: -websocat -H='x-api-Key: YOUR_API_KEY' ws://localhost:3000/docker/stats -``` - -# Security Best Practices - -* 🔑 Rotate keys regularly using the config endpoint -* 🔒 Always use HTTPS in production -* 🗑️ Never commit actual API keys to version control -* 🛡️ Store keys securely using environment variables/secrets management - -# Development Notes - -* Authentication is disabled during development -* All routes are accessible without an API key -* Set `NODE_ENV=production` to enable auth validation - -## Swagger Documentation - -Access interactive API docs at `/swagger` with: - -* Built-in auth scheme configuration -* Endpoint-specific security requirements -* Testing capabilities with API key input - -# Error Handling - -Common responses include: - -* `401 Unauthorized`: Missing/invalid API key -* `500 Internal Server Error`: Authentication system failure - - -:::warning -Always change the default API key before deploying to production! - -::: \ No newline at end of file diff --git a/apps/docs/dockstat/archive/dockstatapi-v3/background-tasks/README.md b/apps/docs/dockstat/archive/dockstatapi-v3/background-tasks/README.md deleted file mode 100644 index 6dbaeca4..00000000 --- a/apps/docs/dockstat/archive/dockstatapi-v3/background-tasks/README.md +++ /dev/null @@ -1,34 +0,0 @@ ---- -id: 3e9d366d-9001-4448-bad8-30f5ff4eb784 -title: Background Tasks -collectionId: b4a5e48f-f103-480b-9f50-8f53f515cab9 -parentDocumentId: f33a7ed1-f6f9-48f9-a393-e150feb09d2f -updatedAt: 2025-08-18T22:50:56.512Z -urlId: 0WCNZPcXkn ---- - -## Background Tasks - -| Default Interval | Task | Config name in the Database | -|----|----|----| -| 5 min | Container and Host metrics collection | `fetching_interval` | -| 7 days | How long Database entries should be kept | `keep_data_for` | - -## Configure - -### 1. Through DockStat - -All configuration can be done through DockStat, please see the DockStat (v2) Documentation \[W.I.P\] for more information - -### 2. Manually - -You can also configure DockStatAPI through a simple `curl` command or similar commands. - -This is the expected Data structure: - -```none -POST /config/update { - keep_data_for: 7, - fetching_interval: 5 -} -``` \ No newline at end of file diff --git a/apps/docs/dockstat/archive/dockstatapi-v3/contributing/README.md b/apps/docs/dockstat/archive/dockstatapi-v3/contributing/README.md deleted file mode 100644 index 0181b99c..00000000 --- a/apps/docs/dockstat/archive/dockstatapi-v3/contributing/README.md +++ /dev/null @@ -1,129 +0,0 @@ ---- -id: 19ddc854-ab6c-4d8b-93e7-9f8d2ced1a56 -title: Contributing -collectionId: b4a5e48f-f103-480b-9f50-8f53f515cab9 -parentDocumentId: f33a7ed1-f6f9-48f9-a393-e150feb09d2f -updatedAt: 2025-08-18T22:50:52.752Z -urlId: cwmU68uCTn ---- - -# DockStatAPI Development Setup - -## Development Environment Overview - -### Prerequisites - -* [Bun](https://bun.sh/) (v1.1.4 or newer) -* Docker Desktop (with compose support) or Docker-engine -* Node.js 18+ (for cross-platform scripts) - -### Key Tools - -* **Bun**: Primary runtime & package manager -* **Socket Proxy**: Secure Docker API gateway -* **SQLite Web**: Database visualization -* **Elysia**: Web framework - -## Getting Started - - -1. **Clone Repository** - -```bash -git clone https://github.com/Its4Nik/DockStatAPI.git - -cd DockStatAPI -``` - - -2. **Install Dependencies** - -```bash -bun install -``` - - -3. **Start Development Environment** - -```bash -bun run dev -``` - -This will: - -* Start Docker Socket Proxy (port 2375) -* Launch SQLite Web UI (port 8080) -* Run API server with file watching (port 3000) - -## Development Commands - -| Command | Description | -|----|----| -| `bun dev` | Start full dev stack with hot reload | -| `bun dev:clean` | Clean the database files if the server crashes unexpectedly | -| `bun clean` | Remove database files (OS-aware) | -| `bun build` | Create production build in `/dist` | -| `bun build:docker` | Build a local Docker image (***dockstatapi:local***) | -| `bun knip` | Analyze for dead code/unused dependencies | - -## Environment Variables - -| Variable | Default | Description | -|----|----|----| -| `NODE_ENV` | dev | Runtime environment | -| `LOG_LEVEL` | debug (In dev mode) | Log verbosity (error, warn, info…) | -| `PAD_NEW_LINES` | true | Pads new log lines (only in the Console output) | - -## Docker Development Services - -### 1. Socket Proxy (Docker API) - -* **Port**: 2375 -* **Purpose**: Secure Docker socket access -* **Config**: Limited API endpoints enabled -* **Access**: `http://localhost:2375` - -### 2. SQLite Web - -* **Port**: 8080 -* **Purpose**: Database management UI -* **Credentials**: None (read-only access) -* **Access**: `http://localhost:8080` - -## Development Tips - -### Database Management - -* Database file: `dockstatapi.db` -* Web UI: -* Schema changes: Modify `~/core/database/database.ts` - -### Testing Docker Interactions - -```bash -curl http://localhost:2375/v1.41/containers/json -``` - -### Production Build - -```bash -bun run build && bun start -``` - -## Troubleshooting - -**Docker Permission Issues** - -```bash -sudo chmod 666 /var/run/docker.sock -``` - -**Clean Environment** - -```bash -bun clean && docker compose -f docker/docker-compose.dev.yaml down -``` - -**Windows Path Issues** - -Use WSL2 for full compatibility with shell commands \ No newline at end of file diff --git a/apps/docs/dockstat/archive/dockstatapi-v3/database/README.md b/apps/docs/dockstat/archive/dockstatapi-v3/database/README.md deleted file mode 100644 index fbaf3c91..00000000 --- a/apps/docs/dockstat/archive/dockstatapi-v3/database/README.md +++ /dev/null @@ -1,98 +0,0 @@ ---- -id: 9d7c53bf-b335-4567-a4cc-76388a903020 -title: Database -collectionId: b4a5e48f-f103-480b-9f50-8f53f515cab9 -parentDocumentId: f33a7ed1-f6f9-48f9-a393-e150feb09d2f -updatedAt: 2025-08-18T22:50:55.513Z -urlId: GuARMjt63A ---- - -## Database Schema - -```mermaidjs -erDiagram - BACKEND_LOG_ENTRIES { - STRING timestamp - TEXT level - TEXT message - TEXT file - NUMBER line - } - - STACKS_CONFIG { - INTEGER id PK - TEXT name - INTEGER version - BOOLEAN custom - TEXT source - INTEGER container_count - TEXT stack_prefix - BOOLEAN automatic_reboot_on_error - BOOLEAN image_updates - } - - DOCKER_HOSTS { - INTEGER id PK - TEXT name - TEXT hostAddress - BOOLEAN secure - } - - HOST_STATS { - INTEGER hostId PK - TEXT hostName - TEXT dockerVersion - TEXT apiVersion - TEXT os - TEXT architecture - INTEGER totalMemory - INTEGER totalCPU - TEXT labels - INTEGER containers - INTEGER containersRunning - INTEGER containersStopped - INTEGER containersPaused - INTEGER images - } - - CONTAINER_STATS { - TEXT id - TEXT hostId - TEXT name - TEXT image - TEXT status - TEXT state - FLOAT cpu_usage - FLOAT memory_usage - DATETIME timestamp - } - - CONFIG { - NUMBER keep_data_for - NUMBER fetching_interval - TEXT api_key - } -``` - -### Table Operations - -| **Table** | **Function** | **Description** | -|----|----|----| -| **backend_log_entries** | `addLogEntry(level, message, file_name, line)` | Adds a log entry. | -| | `getAllLogs()` | Retrieves all logs. | -| | `getLogsByLevel(level)` | Filters logs by severity level. | -| | `clearAllLogs()` | Clears all logs. | -| | `clearLogsByLevel(level)` | Clears logs by severity level. | -| **stacks_config** | `addStack(stack_config)` | Adds a stack configuration. | -| | `getStacks()` | Retrieves all stacks. | -| | `deleteStack(stack_id)` | Deletes a stack by name. | -| | `updateStack(stack_config)` | Updates a stack configuration. | -| **docker_hosts** | `addDockerHost(hostId, url, secure)` | Adds a Docker host. | -| | `getDockerHosts()` | Retrieves all Docker hosts. | -| | `updateDockerHost(name, url, secure)` | Updates a Docker host. | -| | `deleteDockerHost(name)` | Deletes a Docker host. | -| **host_stats** | `updateHostStats(stats)` | Updates host statistics. | -| **container_stats** | `addContainerStats(id, hostId, name, image, status, state, cpu_usage, memory_usage)` | Adds container statistics. | -| **config** | `updateConfig(fetching_interval, keep_data_for)` | Updates configuration settings. | -| | `getConfig()` | Retrieves configuration settings. | -| | `deleteOldData(days)` | Deletes old data based on retention policy. | \ No newline at end of file diff --git a/apps/docs/dockstat/archive/dockstatapi-v3/plugin-development/README.md b/apps/docs/dockstat/archive/dockstatapi-v3/plugin-development/README.md deleted file mode 100644 index 56f5af9e..00000000 --- a/apps/docs/dockstat/archive/dockstatapi-v3/plugin-development/README.md +++ /dev/null @@ -1,191 +0,0 @@ ---- -id: a2b23dbc-0f70-49ef-ad33-73e8421860c7 -title: Plugin Development -collectionId: b4a5e48f-f103-480b-9f50-8f53f515cab9 -parentDocumentId: f33a7ed1-f6f9-48f9-a393-e150feb09d2f -updatedAt: 2025-08-18T22:50:54.610Z -urlId: 3UBj9gNMKF ---- - -## Example plugin - -Create `My-Plugin.plugin.ts` files in `src/plugins/` - -```typescript -import type { Plugin } from "~/typings/plugin"; -import type { ContainerInfo } from "~/typings/docker"; -import type { HostStats } from "~/typings/docker"; - -const ExamplePlugin: Plugin = { - name: "Example Plugin", - async onContainerStart(containerInfo: ContainerInfo) { - console.log(`Container ${containerInfo.name} on ${containerInfo.hostId} started!` - }, -} satisfies Plugin; - -export default ExamplePlugin; - -// You can also use dbFunctions and all other functions like the default logger! -``` - -Available hooks: - -```typescript - async onContainerStart(containerInfo: ContainerInfo) {}, - async onContainerStop(containerInfo: ContainerInfo) {}, - async onContainerExit(containerInfo: ContainerInfo) {}, - async onContainerCreate(containerInfo: ContainerInfo) {}, - async onContainerDestroy(containerInfo: ContainerInfo) {}, - async onContainerPause(containerInfo: ContainerInfo) {}, - async onContainerUnpause(containerInfo: ContainerInfo) {}, - async onContainerRestart(containerInfo: ContainerInfo) {}, - async onContainerUpdate(containerInfo: ContainerInfo) {}, - async onContainerRename(containerInfo: ContainerInfo) {}, - async onContainerHealthStatus(containerInfo: ContainerInfo) {}, - async onHostUnreachable(HostStats: HostStats) {}, - async onHostReachableAgain(HostStats: HostStats) {}, -``` - -## Plugin Loader - -Scans plugins at startup: - -* Validates files don't contain any variation of a "change me" placeholder -* Registers with `PluginManager` - -## Hook usage - -To try it out when (almost) every hook fires please use the `.local-tests/test-container-changes.sh`script. - - ---- - -### **Example Plugin:** - -```typescript -import type { Plugin } from "~/typings/plugin"; -import type { ContainerInfo } from "~/typings/docker"; -import { logger } from "~/core/utils/logger"; - -const ExamplePlugin: Plugin = { - name: "Example Plugin", - - async onContainerStart(containerInfo: ContainerInfo) { - logger.info( - `Container ${containerInfo.name} started on ${containerInfo.hostId}`, - ); - }, - - async onContainerStop(containerInfo: ContainerInfo) { - logger.info( - `Container ${containerInfo.name} stopped on ${containerInfo.hostId}`, - ); - }, - - async onContainerExit(containerInfo: ContainerInfo) { - logger.info( - `Container ${containerInfo.name} exited on ${containerInfo.hostId}`, - ); - }, - - async onContainerCreate(containerInfo: ContainerInfo) { - logger.info( - `Container ${containerInfo.name} created on ${containerInfo.hostId}`, - ); - }, - - async onContainerDestroy(containerInfo: ContainerInfo) { - logger.info( - `Container ${containerInfo.name} destroyed on ${containerInfo.hostId}`, - ); - }, - - async onContainerPause(containerInfo: ContainerInfo) { - logger.info( - `Container ${containerInfo.name} pause on ${containerInfo.hostId}`, - ); - }, - - async onContainerUnpause(containerInfo: ContainerInfo) { - logger.info( - `Container ${containerInfo.name} resumed on ${containerInfo.hostId}`, - ); - }, - - async onContainerRestart(containerInfo: ContainerInfo) { - logger.info( - `Container ${containerInfo.name} restarted on ${containerInfo.hostId}`, - ); - }, - - async onContainerUpdate(containerInfo: ContainerInfo) { - logger.info( - `Container ${containerInfo.name} updated on ${containerInfo.hostId}`, - ); - }, - - async onContainerRename(containerInfo: ContainerInfo) { - logger.info( - `Container ${containerInfo.name} renamed on ${containerInfo.hostId}`, - ); - }, - - async onContainerHealthStatus(containerInfo: ContainerInfo) { - logger.info( - `Container ${containerInfo.name} changed status to ${containerInfo.status}`, - ); - }, - - async onHostUnreachable(host: string, err: string) { - logger.info(`Server ${host} unreachable - ${err}`); - }, - - async onHostReachableAgain(host: string) { - logger.info(`Server ${host} reachable`); - }, - - async handleContainerDie(containerInfo: ContainerInfo) { - logger.info( - `Container ${containerInfo.name} died on ${containerInfo.hostId}`, - ); - }, - - async onContainerKill(containerInfo: ContainerInfo) { - logger.info( - `Container ${containerInfo.name} killed on ${containerInfo.hostId}`, - ); - }, -} satisfies Plugin; - -export default ExamplePlugin; -``` - -### Flow and Commands - - -1. `docker kill SQLite-Web` - - > Logs: - > - > ```jsx - > // Directly after docker kill - > DEBUG [ 18/03 21:52:08 ] - Triggering Action [kill] - [ monitor.ts:76 ] - > INFO [ 18/03 21:52:08 ] - [ Plugin ] Container SQLite-web killed on Localhost - [ example.plugin.ts:89 ] - > - > // Short pause inbetween - > - > DEBUG [ 18/03 21:52:08 ] - Triggering Action [die] - [ monitor.ts:76 ] - > INFO [ 18/03 21:52:08 ] - [ Plugin ] Container SQLite-web died on Localhost - [ example.plugin.ts:83 ] - > // Done - > ``` -2. `docker start SQLite-Web` - - > Logs: - > - > ```jsx - > DEBUG [ 18/03 21:57:50 ] - Triggering Action [start] - [ monitor.ts:76 ] - > INFO [ 18/03 21:57:50 ] - [ Plugin ] Container SQLite-web started on Localhost - [ example.plugin.ts:9 ] - > ``` -3. `docker restart SQLite-Web` - - > Logs: \ No newline at end of file diff --git a/apps/docs/dockstat/archive/dockstatapi-v3/plugin-development/docker-events-endpoint/README.md b/apps/docs/dockstat/archive/dockstatapi-v3/plugin-development/docker-events-endpoint/README.md deleted file mode 100644 index c0deb75d..00000000 --- a/apps/docs/dockstat/archive/dockstatapi-v3/plugin-development/docker-events-endpoint/README.md +++ /dev/null @@ -1,222 +0,0 @@ ---- -id: b0c6295e-8a6b-4404-92a3-09c91d50d6fb -title: Docker Events Endpoint -collectionId: b4a5e48f-f103-480b-9f50-8f53f515cab9 -parentDocumentId: a2b23dbc-0f70-49ef-ad33-73e8421860c7 -updatedAt: 2025-08-18T22:50:55.064Z -urlId: ys0Q9EzfpO ---- - -# Actions - -## Restart - -```json -{ - "status": "kill", - "id": "6d573c3e2de8cdb5ee69ce9b9227a08721b4527c891e1059f7284824d5042729", - "from": "ghcr.io/coleifer/sqlite-web:latest", - "Type": "container", - "Action": "kill", - "Actor": { - "ID": "6d573c3e2de8cdb5ee69ce9b9227a08721b4527c891e1059f7284824d5042729", - "Attributes": { - "com.docker.compose.config-hash": "4f75fd922fd319506dbfaacd72bdb421e90d1b14a52ab0e924450102a9668299", - "com.docker.compose.container-number": "1", - "com.docker.compose.depends_on": "", - "com.docker.compose.image": "sha256:52279e390de0e0cf1126cdd9b64d5c78c68409bd2430072d787f2591a056df99", - "com.docker.compose.oneoff": "False", - "com.docker.compose.project": "dockstatapi-dev", - "com.docker.compose.project.config_files": "DockStatAPI/docker/docker-compose.dev.yaml", - "com.docker.compose.project.working_dir": "DockStatAPI/docker", - "com.docker.compose.service": "sqlite-web", - "com.docker.compose.version": "2.34.0", - "image": "ghcr.io/coleifer/sqlite-web:latest", - "name": "SQLite-web", - "signal": "15" - } - }, - "scope": "local", - "time": 1742332562, - "timeNano": 1742332562851341000 -} - -{ - "status": "kill", - "id": "6d573c3e2de8cdb5ee69ce9b9227a08721b4527c891e1059f7284824d5042729", - "from": "ghcr.io/coleifer/sqlite-web:latest", - "Type": "container", - "Action": "kill", - "Actor": { - "ID": "6d573c3e2de8cdb5ee69ce9b9227a08721b4527c891e1059f7284824d5042729", - "Attributes": { - "com.docker.compose.config-hash": "4f75fd922fd319506dbfaacd72bdb421e90d1b14a52ab0e924450102a9668299", - "com.docker.compose.container-number": "1", - "com.docker.compose.depends_on": "", - "com.docker.compose.image": "sha256:52279e390de0e0cf1126cdd9b64d5c78c68409bd2430072d787f2591a056df99", - "com.docker.compose.oneoff": "False", - "com.docker.compose.project": "dockstatapi-dev", - "com.docker.compose.project.config_files": "DockStatAPI/docker/docker-compose.dev.yaml", - "com.docker.compose.project.working_dir": "DockStatAPI/docker", - "com.docker.compose.service": "sqlite-web", - "com.docker.compose.version": "2.34.0", - "image": "ghcr.io/coleifer/sqlite-web:latest", - "name": "SQLite-web", - "signal": "9" - } - }, - "scope": "local", - "time": 1742332572, - "timeNano": 1742332572881627600 -} - -{ - "status": "stop", - "id": "6d573c3e2de8cdb5ee69ce9b9227a08721b4527c891e1059f7284824d5042729", - "from": "ghcr.io/coleifer/sqlite-web:latest", - "Type": "container", - "Action": "stop", - "Actor": { - "ID": "6d573c3e2de8cdb5ee69ce9b9227a08721b4527c891e1059f7284824d5042729", - "Attributes": { - "com.docker.compose.config-hash": "4f75fd922fd319506dbfaacd72bdb421e90d1b14a52ab0e924450102a9668299", - "com.docker.compose.container-number": "1", - "com.docker.compose.depends_on": "", - "com.docker.compose.image": "sha256:52279e390de0e0cf1126cdd9b64d5c78c68409bd2430072d787f2591a056df99", - "com.docker.compose.oneoff": "False", - "com.docker.compose.project": "dockstatapi-dev", - "com.docker.compose.project.config_files": "DockStatAPI/docker/docker-compose.dev.yaml", - "com.docker.compose.project.working_dir": "DockStatAPI/docker", - "com.docker.compose.service": "sqlite-web", - "com.docker.compose.version": "2.34.0", - "image": "ghcr.io/coleifer/sqlite-web:latest", - "name": "SQLite-web" - } - }, - "scope": "local", - "time": 1742332572, - "timeNano": 1742332572978213400 -} - -{ - "status": "die", - "id": "6d573c3e2de8cdb5ee69ce9b9227a08721b4527c891e1059f7284824d5042729", - "from": "ghcr.io/coleifer/sqlite-web:latest", - "Type": "container", - "Action": "die", - "Actor": { - "ID": "6d573c3e2de8cdb5ee69ce9b9227a08721b4527c891e1059f7284824d5042729", - "Attributes": { - "com.docker.compose.config-hash": "4f75fd922fd319506dbfaacd72bdb421e90d1b14a52ab0e924450102a9668299", - "com.docker.compose.container-number": "1", - "com.docker.compose.depends_on": "", - "com.docker.compose.image": "sha256:52279e390de0e0cf1126cdd9b64d5c78c68409bd2430072d787f2591a056df99", - "com.docker.compose.oneoff": "False", - "com.docker.compose.project": "dockstatapi-dev", - "com.docker.compose.project.config_files": "DockStatAPI/docker/docker-compose.dev.yaml", - "com.docker.compose.project.working_dir": "DockStatAPI/docker", - "com.docker.compose.service": "sqlite-web", - "com.docker.compose.version": "2.34.0", - "execDuration": "611", - "exitCode": "137", - "image": "ghcr.io/coleifer/sqlite-web:latest", - "name": "SQLite-web" - } - }, - "scope": "local", - "time": 1742332572, - "timeNano": 1742332572984070700 -} - -{ - "status": "start", - "id": "6d573c3e2de8cdb5ee69ce9b9227a08721b4527c891e1059f7284824d5042729", - "from": "ghcr.io/coleifer/sqlite-web:latest", - "Type": "container", - "Action": "start", - "Actor": { - "ID": "6d573c3e2de8cdb5ee69ce9b9227a08721b4527c891e1059f7284824d5042729", - "Attributes": { - "com.docker.compose.config-hash": "4f75fd922fd319506dbfaacd72bdb421e90d1b14a52ab0e924450102a9668299", - "com.docker.compose.container-number": "1", - "com.docker.compose.depends_on": "", - "com.docker.compose.image": "sha256:52279e390de0e0cf1126cdd9b64d5c78c68409bd2430072d787f2591a056df99", - "com.docker.compose.oneoff": "False", - "com.docker.compose.project": "dockstatapi-dev", - "com.docker.compose.project.config_files": "DockStatAPI/docker/docker-compose.dev.yaml", - "com.docker.compose.project.working_dir": "DockStatAPI/docker", - "com.docker.compose.service": "sqlite-web", - "com.docker.compose.version": "2.34.0", - "image": "ghcr.io/coleifer/sqlite-web:latest", - "name": "SQLite-web" - } - }, - "scope": "local", - "time": 1742332573, - "timeNano": 1742332573121227800 -} - -{ - "status": "restart", - "id": "6d573c3e2de8cdb5ee69ce9b9227a08721b4527c891e1059f7284824d5042729", - "from": "ghcr.io/coleifer/sqlite-web:latest", - "Type": "container", - "Action": "restart", - "Actor": { - "ID": "6d573c3e2de8cdb5ee69ce9b9227a08721b4527c891e1059f7284824d5042729", - "Attributes": { - "com.docker.compose.config-hash": "4f75fd922fd319506dbfaacd72bdb421e90d1b14a52ab0e924450102a9668299", - "com.docker.compose.container-number": "1", - "com.docker.compose.depends_on": "", - "com.docker.compose.image": "sha256:52279e390de0e0cf1126cdd9b64d5c78c68409bd2430072d787f2591a056df99", - "com.docker.compose.oneoff": "False", - "com.docker.compose.project": "dockstatapi-dev", - "com.docker.compose.project.config_files": "DockStatAPI/docker/docker-compose.dev.yaml", - "com.docker.compose.project.working_dir": "DockStatAPI/docker", - "com.docker.compose.service": "sqlite-web", - "com.docker.compose.version": "2.34.0", - "image": "ghcr.io/coleifer/sqlite-web:latest", - "name": "SQLite-web" - } - }, - "scope": "local", - "time": 1742332573, - "timeNano": 1742332573121259500 -} -``` - -There is also some network messages: - -```json -{ - "Type": "network", - "Action": "disconnect", - "Actor": { - "ID": "065c15b76c16d1ce87587f6767246129fd41af2b6f82db621c2993cbaf3b2e7a", - "Attributes": { - "container": "6d573c3e2de8cdb5ee69ce9b9227a08721b4527c891e1059f7284824d5042729", - "name": "dockstatapi-dev_default", - "type": "bridge" - } - }, - "scope": "local", - "time": 1742331961, - "timeNano": 1742331961807890000 -} - -{ - "Type": "network", - "Action": "connect", - "Actor": { - "ID": "065c15b76c16d1ce87587f6767246129fd41af2b6f82db621c2993cbaf3b2e7a", - "Attributes": { - "container": "6d573c3e2de8cdb5ee69ce9b9227a08721b4527c891e1059f7284824d5042729", - "name": "dockstatapi-dev_default", - "type": "bridge" - } - }, - "scope": "local", - "time": 1742332573, - "timeNano": 1742332573108775200 -} -``` \ No newline at end of file diff --git a/apps/docs/dockstat/archive/dockstatapi-v3/stacks/README.md b/apps/docs/dockstat/archive/dockstatapi-v3/stacks/README.md deleted file mode 100644 index 22cd3271..00000000 --- a/apps/docs/dockstat/archive/dockstatapi-v3/stacks/README.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -id: 970cdf56-b108-4468-8d8a-4c9b7d71c2c3 -title: Stacks -collectionId: b4a5e48f-f103-480b-9f50-8f53f515cab9 -parentDocumentId: f33a7ed1-f6f9-48f9-a393-e150feb09d2f -updatedAt: 2025-08-18T22:50:53.197Z -urlId: mlJ2fQWpdT ---- - -## Compose Stack Lifecycle - -```mermaidjs -graph LR - A[Deploy] --> B[Create YAML] - B --> C[Store Config] - C --> D[Compose Up] - - D --> E[Start/Stop] - E --> F[Status Monitoring] -``` - -## API Endpoints - -| Method | Path | Description | -|----|----|----| -| POST | `/stacks/deploy` | Deploy new Stack=> writes yaml and `docker compose up` | -| POST | `/stacks/start` | Start a Stack
=> `docker compose up` | -| POST | `/stacks/stop` | Puts a Stack down
=> `docker compose down` | -| POST | `/stacks/restart` | Restarts a Stack=> `docker compose restart` | -| POST | `/stacks/pull-images` | Pulls all images for a Stack=> `docker compose pull` | -| GET | `/stacks/status` | Gets custom Stack status=> Adjusted: `docker compose ps` | -| GET | `/stacks` | Lists all available Stacks | - -## Storage - -Stored in `stacks/` directory as: - -``` -/stacks - /my_stack - docker-compose.yaml - my-folder-for-a-service/... -``` \ No newline at end of file diff --git a/apps/docs/dockstat/archive/dockstatapi-v3/stacks/usage/README.md b/apps/docs/dockstat/archive/dockstatapi-v3/stacks/usage/README.md deleted file mode 100644 index 259f7559..00000000 --- a/apps/docs/dockstat/archive/dockstatapi-v3/stacks/usage/README.md +++ /dev/null @@ -1,159 +0,0 @@ ---- -id: 8e733b65-5df2-4d8f-90e1-da8b127b2b27 -title: Usage -collectionId: b4a5e48f-f103-480b-9f50-8f53f515cab9 -parentDocumentId: 970cdf56-b108-4468-8d8a-4c9b7d71c2c3 -updatedAt: 2025-08-18T22:50:53.676Z -urlId: OaH8UQ4BB3 ---- - -## Endpoint - -``` -POST /stacks/deploy -``` - -### Description - -Deploys a new Docker stack using a provided Compose specification, allowing custom configurations and image updates. - - ---- - -## Request Structure - -| Field | Type | Required | Description | -|----|----|----|----| -| `name` | `string` | yes | Name of the stack to deploy. | -| `version` | `number` | yes | Version number of the stack configuration. | -| `custom` | `boolean` | yes | Whether to use a custom deployment process. | -| `source` | `string` | yes | Source identifier (e.g., repository or internal reference). | -| `compose_spec` | `object` | yes | JSON representation of a Docker Compose file. | - -The `compose_spec` object must conform to the `ComposeSpec` interface: - -```typescript -export interface ComposeSpec { - version?: string; - name?: string; - include?: Include[]; - services?: { [key: string]: Service }; - networks?: { [key: string]: Network }; - volumes?: { [key: string]: Volume }; - secrets?: { [key: string]: Secret }; - configs?: { [key: string]: Config }; - - // Allows custom extensions - [key: `x-${string}`]: any; -} -``` - - ---- - -## Responses - -* **200 OK** - * **Body**: `{ "message": "Stack deployed successfully" }` -* **400 Bad Request** - * **Body**: `{ "error": "Error deploying stack" }` - - ---- - -## Examples - -### 1. Single-Container Stack - -Deploy a simple NGINX web server: - -```json -{ - "name": "nginx-simple", - "version": 1, - "custom": false, - "source": "internal", - "compose_spec": { - "services": { - "web": { - "image": "nginx:latest", - "ports": [ - "80:80" - ] - } - } - } -} -``` - -**Result**: - -* The API will deploy an NGINX container exposing port 80. - - ---- - -### 2. Lightweight Multi-Service Stack - -Deploy a simple pair of services that communicate over a shared internal network, using no external files or volumes: - -```json -{ - "name": "echo-ping", - "version": 1, - "custom": false, - "source": "internal", - "compose_spec": { - "services": { - "ping": { - "image": "alpine", - "command": ["sh", "-c", "apk add curl && sleep 2 && watch -n 5 curl echo:5678"], - "depends_on": ["echo"], - "networks": ["testnet"] - }, - "echo": { - "image": "ealen/echo-server", - "ports": ["5678:5678"], - "networks": ["testnet"] - } - }, - "networks": { - "testnet": { - "driver": "bridge" - } - } - } -} -``` - -**Result**: - -* `echo` is a lightweight HTTP server that responds to any request. -* `ping` waits a bit, then makes a request to `echo` over the shared `testnet` network. - - ---- - -## Tips and Best Practices - - -1. **Versioning**: Use the `version` field to handle version tracking. -2. **Source Control:** Tracks the source of each version *(e.g: "[https://github.com/Its4Nik/DockStacks](https://github.com/Its4Nik/DockStackshttps://github.com/Its4Nik/DockStacks)")* -3. **Service Isolation**: Use custom networks to securely connect services and reduce unwanted exposure. - - ---- - -For further details on the Compose file format, refer to the [Docker Compose documentation](https://docs.docker.com/compose/compose-file/). - - -:::info -YAML is full JSON compiled, just use a trustable online converter. - -::: - - -:::tip -Better yet, use DockStat! - -::: \ No newline at end of file diff --git a/apps/docs/dockstat/archive/dockstatapi-v3/web-sockets/README.md b/apps/docs/dockstat/archive/dockstatapi-v3/web-sockets/README.md deleted file mode 100644 index b95fe8be..00000000 --- a/apps/docs/dockstat/archive/dockstatapi-v3/web-sockets/README.md +++ /dev/null @@ -1,172 +0,0 @@ ---- -id: 5a552211-a8fa-44ce-b816-de587a5caa64 -title: Web Sockets -collectionId: b4a5e48f-f103-480b-9f50-8f53f515cab9 -parentDocumentId: f33a7ed1-f6f9-48f9-a393-e150feb09d2f -updatedAt: 2025-08-18T22:50:56.004Z -urlId: XuqaF3W1HH ---- - -:::info -Use either `ws://` or `wss://` depending on your setup. - -::: - -# Docker Stats socket - - -:::info -A Web Socket endpoint for Live Statistics of Docker Containers - -::: - -## Usage - -Connect to `:/docker/stats` - -## Example - -```json -{ - "message": "Connection established" -} -{ - "id": "XXX", - "hostId": "Localhost", - "name": "SQLite-web", - "image": "ghcr.io/coleifer/sqlite-web:latest", - "status": "Up 2 hours", - "state": "running", - "cpuUsage": 0.001024106400665004, - "memoryUsage": 0.1788478730664797 -} -{ - "id": "YYY", - "hostId": "Localhost", - "name": "Socket-Proxy", - "image": "lscr.io/linuxserver/socket-proxy:latest", - "status": "Up 2 hours", - "state": "running", - "cpuUsage": 0.002458021612635079, - "memoryUsage": 0.03701313770551838 -} -``` - - ---- - -# Logging Socket - -## Usage - -Connect to `:/logs/ws` - -## Example Data - -```json -{ - "message": "Connection established" -} -{ - level: "INFO" - timestamp: "...", - message: "Starting DockStatAPI", - file: "index.ts", - line: 5 -} -``` - - ---- - -# Stack Socket - -## Usage - -Connect to `:/stacks` - -## Example Data - - ---- - -### ✅ Stack Status Update - -```json -{ - "type": "stack-status", - "data": { - "stack_id": 12, - "status": "pending", - "message": "Creating stack configuration" - } -} -``` - - ---- - -### 🚀 Stack Deployment Success - -```json -{ - "type": "stack-status", - "data": { - "stack_id": 12, - "status": "deployed", - "message": "Stack deployed successfully" - } -} -``` - - ---- - -### ⚙️ Stack Progress Log (During Start/Deploy/etc.) - -```json -{ - "type": "stack-progress", - "data": { - "stack_id": 12, - "action": "deploying", - "message": "Creating network my_stack_default", - "timestamp": "2025-04-16T18:25:43.511Z" - } -} -``` - - ---- - -### ❌ Stack Error - -```json -{ - "type": "stack-error", - "data": { - "stack_id": 12, - "action": "deploying", - "message": "Error while deploying stack \"12\": Docker daemon not reachable", - "timestamp": "2025-04-16T18:26:10.115Z" - } -} -``` - - ---- - -### 🗑️ Stack Removed - -```json -{ - "type": "stack-removed", - "data": { - "stack_id": 12, - "message": "Stack removed successfully" - } -} -``` - - ---- \ No newline at end of file diff --git a/apps/docs/dockstat/configuration/README.md b/apps/docs/dockstat/configuration/README.md new file mode 100644 index 00000000..1b63b32c --- /dev/null +++ b/apps/docs/dockstat/configuration/README.md @@ -0,0 +1,607 @@ +--- +id: dec1cb2c-9a13-4e67-a31c-d3a685391208 +title: Configuration +collectionId: b4a5e48f-f103-480b-9f50-8f53f515cab9 +parentDocumentId: 7dddd764-6483-4f84-96a3-988304e772d3 +updatedAt: 2025-12-17T09:46:31.595Z +urlId: 3dLW6L8pzR +--- + +> Complete configuration reference for all DockStat applications and packages. This guide covers environment variables, runtime settings, and configuration files. + +## Configuration Overview + +```mermaidjs + +graph TB + subgraph "Configuration Sources" + ENV["Environment Variables"] + FILES["Configuration Files"] + DB["Database Config"] + RUNTIME["Runtime Settings"] + end + + subgraph "Applications" + API["apps/api"] + DS["apps/dockstat"] + DN["apps/docknode"] + DST["apps/dockstore"] + end + + subgraph "Packages" + LOG["@dockstat/logger"] + DC["@dockstat/docker-client"] + DBP["@dockstat/db"] + PH["@dockstat/plugin-handler"] + end + + ENV --> API + ENV --> DS + ENV --> DN + ENV --> LOG + FILES --> API + FILES --> DS + DB --> DBP + DB --> DC + RUNTIME --> PH +``` + +## Environment Variables + +### API Application (`apps/api`) + +| Variable | Type | Default | Description | +|----|----|----|----| +| `DOCKSTAT_MAX_WORKERS` | `number` | `200` | Maximum worker threads for DockerClientManager | +| `DOCKSTATAPI_SHOW_TRACES` | `boolean` | `true` | Enable server timing traces in responses | +| `DOCKSTATAPI_DEFAULT_PLUGIN_DIR` | `string` | `src/plugins/default-plugins` | Default directory for plugin discovery | +| `DOCKSTATAPI_PORT` | `number` | `9876` | API server port | +| `DOCKSTATAPI_HOST` | `string` | `0.0.0.0` | API server host binding | + +**Example Configuration:** + +```bash +# .env file for apps/api + +DOCKSTAT_MAX_WORKERS=100 +DOCKSTATAPI_SHOW_TRACES=true +DOCKSTATAPI_DEFAULT_PLUGIN_DIR=./plugins +DOCKSTATAPI_PORT=9876 +DOCKSTATAPI_HOST=0.0.0.0 +``` + +### DockNode Application (`apps/docknode`) + +| Variable | Type | Default | Description | +|----|----|----|----| +| `DOCKNODE_DOCKSTACK_AUTH_PSK` | `string` | — | Production pre-shared key for authentication | +| `DOCKNODE_DOCKSTACK_DEV_AUTH` | `string` | — | Development authentication key | +| `DOCKNODE_DOCKSTACK_AUTH_PRIORITY` | `string` | `psk` | Authentication method priority (`psk`, `dev`, `none`) | +| `PORT` | `number` | `4000` | DockNode server port | + +**Example Configuration:** + +```bash +# .env file for apps/docknode + +DOCKNODE_DOCKSTACK_AUTH_PSK=your-secure-production-key +DOCKNODE_DOCKSTACK_DEV_AUTH=dev-key-for-testing +DOCKNODE_DOCKSTACK_AUTH_PRIORITY=psk + +PORT=4000 +``` + +### Logger Package (`@dockstat/logger`) + +| Variable | Type | Default | Description | +|----|----|----|----| +| `DOCKSTAT_LOGGER_FULL_FILE_PATH` | `boolean` | `false` | Show full file paths in log output | +| `DOCKSTAT_LOGGER_IGNORE_MESSAGES` | `string` | — | Comma-separated messages to filter out | +| `DOCKSTAT_LOGGER_DISABLED_LOGGERS` | `string` | — | Comma-separated logger names to disable | +| `DOCKSTAT_LOGGER_ONLY_SHOW` | `string` | — | Only show these loggers (comma-separated) | +| `DOCKSTAT_LOGGER_SEPERATOR` | `string` | `:` | Separator between logger name segments | + +**Example Configuration:** + +```bash +# Development - verbose logging +DOCKSTAT_LOGGER_FULL_FILE_PATH=true + +# Production - filtered logging + +DOCKSTAT_LOGGER_DISABLED_LOGGERS=Debug,Trace +DOCKSTAT_LOGGER_ONLY_SHOW=API,Database,Docker +DOCKSTAT_LOGGER_IGNORE_MESSAGES=health check,heartbeat +``` + +## Configuration Flow + +```mermaidjs + +sequenceDiagram + participant App as "Application" + participant Env as "Environment" + participant Config as "Config Loader" + participant DB as "Database" + participant Runtime as "Runtime Config" + + App->>Env: "Read environment variables" + Env-->>Config: "ENV values" + Config->>DB: "Load stored configuration" + DB-->>Config: "Database settings" + Config->>Config: "Merge configurations" + Config->>Runtime: "Apply runtime settings" + Runtime-->>App: "Initialized configuration" +``` + +## Database Configuration + +### Config Table Schema + +The `config` table stores application-wide settings: + +```typescript +interface ConfigTable { + id: number; // Always 1 (singleton) + current_theme_name: string; // Active theme name +} +``` + +### Accessing Configuration + +```typescript +import DockStatDB from "@dockstat/db"; + +const db = new DockStatDB(); + +// Get current theme name + +const themeName = db.getCurrentThemeName(); + +// Set theme + +db.setTheme("dark-theme"); + +// Get full theme configuration + +const theme = db.getCurrentTheme(); +``` + +## Docker Client Configuration + +### Host Configuration + +```typescript +import type { DOCKER } from "@dockstat/typings"; + +const hostConfig: DOCKER.HostConfig = { + id: 1, + host: "192.168.1.100", + port: 2375, + secure: false, + name: "Production Docker" +}; +``` + +### Client Options + +```typescript +import DockerClient from "@dockstat/docker-client"; + +const client = new DockerClient(db.getDB(), { + enableMonitoring: true, + monitoringInterval: 5000, // 5 seconds + maxRetries: 3, + retryDelay: 1000 +}); +``` + +### Monitoring Configuration + +```mermaidjs + +graph LR + subgraph "Monitoring Options" + INT["Interval: 5000ms"] + RET["Retries: 3"] + DEL["Delay: 1000ms"] + STREAM["Streaming: enabled"] + end + + subgraph "Output Channels" + WS["WebSocket"] + POLL["HTTP Polling"] + EVT["Event Emitter"] + end + + INT --> WS + INT --> POLL + STREAM --> EVT +``` + +## Plugin Configuration + +### Plugin Manifest + +Plugins are configured via `manifest.yml`: + +```yaml +name: my-plugin + +version: 1.0.0 + +description: Plugin description + +author: + name: Developer Name + email: dev@example.com + +repository: https://github.com/user/plugin + +tags: + - monitoring + - docker + +repoType: github +``` + +### Plugin Runtime Configuration + +```typescript +const pluginConfig = { + table: { + name: "plugin_data", + columns: { + id: column.id(), + data: column.json(), + created_at: column.createdAt() + }, + jsonColumns: ["data"] + }, + apiRoutes: { + "/status": { + method: "GET", + actions: ["getStatus"] + } + }, + actions: { + getStatus: ({ table, logger }) => { + return { status: "active" }; + } + } +}; +``` + +## Theme Configuration + +### Theme Structure + +```mermaidjs + +graph TB + subgraph "Theme Configuration" + META["Metadata"] + VARS["Variables"] + end + + subgraph "Metadata" + NAME["name"] + VER["version"] + CREATOR["creator"] + LIC["license"] + end + + subgraph "Variables" + BG["background_effect"] + COMP["components"] + end + + subgraph "Background Effects" + SOLID["Solid"] + GRAD["Gradient"] + AURORA["Aurora"] + end + + subgraph "Components" + CARD["Card"] + BTN["Button"] + NAV["Navbar"] + end + + META --> NAME + META --> VER + META --> CREATOR + META --> LIC + VARS --> BG + VARS --> COMP + BG --> SOLID + BG --> GRAD + BG --> AURORA + COMP --> CARD + COMP --> BTN + COMP --> NAV +``` + +### Theme Example + +```typescript +import type { THEME } from "@dockstat/typings"; + +const customTheme: THEME.THEME_config = { + name: "custom-dark", + version: "1.0.0", + creator: "Your Name", + license: "MIT", + description: "A custom dark theme", + active: true, + vars: { + background_effect: { + Gradient: { + from: "#1a1a2e", + to: "#16213e", + direction: "to bottom right" + } + }, + components: { + Card: { + accent: "#0f3460", + border: "1px solid #e94560", + border_color: "#e94560", + border_size: "1px", + title: { + font: "Inter", + color: "#ffffff", + font_size: "18px", + font_weight: "600" + }, + sub_title: { + font: "Inter", + color: "#cccccc", + font_size: "14px", + font_weight: "400" + }, + content: { + font: "Inter", + color: "#e0e0e0", + font_size: "14px", + font_weight: "400" + } + } + } + } +}; +``` + +## Production Configuration + +### Recommended Settings + +```mermaidjs + +graph TB + subgraph "Production Config" + direction TB + API_PROD["API Settings"] + LOG_PROD["Logger Settings"] + DB_PROD["Database Settings"] + SEC_PROD["Security Settings"] + end + + API_PROD --> W["Workers: CPU cores × 2"] + API_PROD --> T["Traces: disabled"] + + LOG_PROD --> L1["Disabled: Debug, Trace"] + LOG_PROD --> L2["Only Show: API, Database"] + + DB_PROD --> D1["WAL mode: enabled"] + DB_PROD --> D2["Foreign keys: ON"] + + SEC_PROD --> S1["TLS: enabled"] + SEC_PROD --> S2["Auth: PSK"] +``` + +### Production Environment File + +```bash +# Production .env + +# API Configuration + +DOCKSTAT_MAX_WORKERS=16 +DOCKSTATAPI_SHOW_TRACES=false +DOCKSTATAPI_PORT=9876 + +# Logger Configuration + +DOCKSTAT_LOGGER_DISABLED_LOGGERS=Debug,Trace,Verbose +DOCKSTAT_LOGGER_FULL_FILE_PATH=false + +# DockNode Configuration + +DOCKNODE_DOCKSTACK_AUTH_PRIORITY=psk +DOCKNODE_DOCKSTACK_AUTH_PSK= + +# Database + +NODE_ENV=production +``` + +## Development Configuration + +### Development Environment File + +```bash +# Development .env + +# API Configuration +DOCKSTAT_MAX_WORKERS=4 +DOCKSTATAPI_SHOW_TRACES=true +DOCKSTATAPI_PORT=9876 + +# Logger Configuration - verbose +DOCKSTAT_LOGGER_FULL_FILE_PATH=true + +# DockNode Configuration +DOCKNODE_DOCKSTACK_AUTH_PRIORITY=dev +DOCKNODE_DOCKSTACK_DEV_AUTH=dev-key + +# Database +NODE_ENV=development +``` + +## TypeScript Configuration + +### Base Configuration (`tsconfig.base.json`) + +All packages extend the base TypeScript configuration: + +```json +{ + "compilerOptions": { + "target": "ESNext", + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "types": ["bun-types"] + } +} +``` + +### Package-Specific Configuration + +Each package extends the base config: + +```json +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} +``` + +## Turborepo Configuration + +### Pipeline Configuration (`turbo.json`) + +```json +{ + "$schema": "https://turbo.build/schema.json", + "pipeline": { + "build": { + "dependsOn": ["^build"], + "outputs": ["dist/**", "build/**"] + }, + "dev": { + "cache": false, + "persistent": true + }, + "lint": {}, + "check-types": { + "dependsOn": ["^build"] + } + } +} +``` + +## Docker Configuration + +### Development Compose (`docker-compose.dev.yaml`) + +```yaml +version: '3.8' +services: + dockstat: + build: + context: ./apps/dockstat + dockerfile: Dockerfile + ports: + - "5173:5173" + volumes: + - ./apps/dockstat:/app + environment: + - NODE_ENV=development + + api: + build: + context: ./apps/api + ports: + - "9876:9876" + environment: + - DOCKSTAT_MAX_WORKERS=4 + - DOCKSTATAPI_SHOW_TRACES=true +``` + +## Configuration Precedence + +Configuration values are resolved in the following order (highest to lowest priority): + +```mermaidjs + +graph TB + subgraph "Configuration Precedence" + direction TB + CLI["1. Command Line Arguments"] + ENV["2. Environment Variables"] + LOCAL["3. Local Config Files"] + DB["4. Database Settings"] + DEFAULT["5. Default Values"] + end + + CLI --> ENV + ENV --> LOCAL + LOCAL --> DB + DB --> DEFAULT +``` + +## Validation + +### Runtime Validation with Typebox + +```typescript +import { Value } from "@sinclair/typebox/value"; +import { schemas } from "@dockstat/typings/schemas"; + +// Validate configuration at runtime + +const config = loadConfig(); + +if (!Value.Check(schemas.HostConfigSchema, config.host)) { + throw new Error("Invalid host configuration"); +} +``` + +### Schema Validation + +```typescript +import { t } from "elysia"; + +// Elysia route with validation + +app.post("/config", ({ body }) => { + return updateConfig(body); +}, { + body: t.Object({ + maxWorkers: t.Number({ minimum: 1, maximum: 1000 }), + enableTraces: t.Boolean(), + pluginDir: t.String() + }) +}); +``` + +## Related Documentation + +| Section | Description | +|----|----| +| [Architecture](/doc/d56ca448-563a-4206-9585-c45f8f6be5cf) | System design and component relationships | +| [API Reference](/doc/b174143d-f906-4f8d-8cb5-9fc96512e575) | Complete API endpoint documentation | +| [Packages](/doc/bbcefaa2-6bd4-46e8-ae4b-a6b823593e67) | Package-specific configuration details | +| [Troubleshooting](/doc/88a5f959-3f89-4266-9d8e-eb50193425b0) | Configuration-related issues and solutions | \ No newline at end of file diff --git a/apps/docs/dockstat/integration-guide/README.md b/apps/docs/dockstat/integration-guide/README.md new file mode 100644 index 00000000..70ae4bb8 --- /dev/null +++ b/apps/docs/dockstat/integration-guide/README.md @@ -0,0 +1,1046 @@ +--- +id: e4e04545-fd9f-4fbf-becb-94da81f48bc5 +title: Integration guide +collectionId: b4a5e48f-f103-480b-9f50-8f53f515cab9 +parentDocumentId: 7dddd764-6483-4f84-96a3-988304e772d3 +updatedAt: 2025-12-17T09:45:45.019Z +urlId: SWA5Nd4lzW +--- + +> Comprehensive guide for integrating DockStat components, packages, and external services. This document covers package interoperability, API integration, plugin development, and third-party service connections. + +## Integration Overview + +```mermaidjs + +graph TB + subgraph "External Services" + DOCKER["Docker Daemon"] + PROM["Prometheus"] + WEBHOOK["Webhooks"] + NOTIFY["Notification Services"] + end + + subgraph "DockStat Core" + API["DockStat API"] + FE["Frontend (React Router)"] + DN["DockNode"] + DST["DockStore"] + end + + subgraph "Shared Packages" + DC["@dockstat/docker-client"] + DB["@dockstat/db"] + PH["@dockstat/plugin-handler"] + LOG["@dockstat/logger"] + TYP["@dockstat/typings"] + end + + DOCKER --> DC + DC --> API + API --> FE + API --> PROM + PH --> WEBHOOK + PH --> NOTIFY + DN --> API + DST --> PH + DB --> API + LOG --> API + LOG --> DC + TYP --> API + TYP --> FE +``` + +## Package Integration + +### Core Package Dependencies + +```mermaidjs + +graph BT + subgraph "Foundation Layer" + SW["@dockstat/sqlite-wrapper"] + LOG["@dockstat/logger"] + TYP["@dockstat/typings"] + UTILS["@dockstat/utils"] + end + + subgraph "Service Layer" + DB["@dockstat/db"] + DC["@dockstat/docker-client"] + PH["@dockstat/plugin-handler"] + end + + subgraph "Presentation Layer" + UI["@dockstat/ui"] + end + + subgraph "Applications" + API["apps/api"] + FE["apps/dockstat"] + end + + DB --> SW + DB --> TYP + DC --> SW + DC --> LOG + DC --> TYP + DC --> UTILS + PH --> SW + PH --> LOG + PH --> TYP + UI --> TYP + UI --> UTILS + API --> DB + API --> DC + API --> PH + API --> LOG + FE --> UI + FE --> TYP +``` + +### Database Integration + +The database layer provides the foundation for all data persistence: + +```typescript +import DockStatDB from "@dockstat/db"; +import DockerClient from "@dockstat/docker-client"; +import PluginHandler from "@dockstat/plugin-handler"; + +// Initialize the database + +const db = new DockStatDB(); + +// Share database instance with Docker client + +const dockerClient = new DockerClient(db.getDB(), { + enableMonitoring: true +}); + +// Share database instance with plugin handler + +const pluginHandler = new PluginHandler(db.getDB()); + +// All components now share the same SQLite database +``` + +### Logger Integration + +Integrate the logger across all services: + +```typescript +import Logger from "@dockstat/logger"; + +// Create service-specific loggers + +const apiLogger = new Logger("API"); +const dockerLogger = new Logger("Docker"); +const pluginLogger = new Logger("Plugins"); + +// Spawn child loggers for sub-components + +const routeLogger = apiLogger.spawn("Routes"); +const containerLogger = dockerLogger.spawn("Container"); + +// Use request ID tracking for distributed tracing + +function handleRequest(req: Request) { + const reqId = req.headers.get("x-request-id") || crypto.randomUUID(); + + apiLogger.info("Request received", reqId); + routeLogger.debug("Processing route", reqId); + + return processRequest(req, reqId); +} +``` + +### Type Safety Integration + +Use shared types across all packages: + +```typescript +import type { DOCKER, PLUGIN, THEME, DATABASE } from "@dockstat/typings"; +import { schemas } from "@dockstat/typings/schemas"; +import { Value } from "@sinclair/typebox/value"; + +// Type-safe Docker host configuration + +const host: DOCKER.HostConfig = { + id: 1, + host: "192.168.1.100", + port: 2375, + secure: false, + name: "Production Host" +}; + +// Runtime validation + +if (Value.Check(schemas.HostConfigSchema, host)) { + // host is validated + await dockerClient.addHost(host); +} + +// Type-safe plugin definition + +const plugin: PLUGIN.Plugin = { + id: 1, + name: "my-plugin", + version: "1.0.0", + config: { + // Type-checked configuration + } +}; +``` + +## API Integration + +### Frontend to Backend Integration + +```mermaidjs + +sequenceDiagram + participant Browser as "Browser" + participant FE as "Frontend (React Router)" + participant Eden as "Eden Client" + participant API as "API (Elysia)" + participant DC as "DockerClient" + participant Docker as "Docker Daemon" + + Browser->>FE: "User Action" + FE->>Eden: "Type-safe API call" + Eden->>API: "HTTP Request" + API->>DC: "Docker operation" + DC->>Docker: "Docker API call" + Docker-->>DC: "Response" + DC-->>API: "Processed data" + API-->>Eden: "JSON Response" + Eden-->>FE: "Typed response" + FE-->>Browser: "Updated UI" +``` + +### Eden Client Setup + +```typescript +// apps/dockstat/app/api.ts + +import { treaty } from "@elysiajs/eden"; +import type { TreatyType } from "../../api/src/index"; + +// Create type-safe API client + +export const api = treaty("http://localhost:9876"); + +// Usage in React components + +async function fetchContainers(clientId: number) { + const { data, error } = await api.api.v2.docker.containers.all[clientId].get(); + + if (error) { + throw new Error(error.message); + } + + return data; +} +``` + +### Direct API Integration + +For external services integrating with DockStat: + +```typescript +// External service integration + +const DOCKSTAT_API = "http://localhost:9876/api/v2"; + +// Register a new Docker client + +async function registerClient(name: string) { + const response = await fetch(`${DOCKSTAT_API}/docker/client/register`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ clientName: name }) + }); + + const result = await response.json(); + return result.clientId; +} + +// Add a Docker host + +async function addHost(clientId: number, config: HostConfig) { + const response = await fetch(`${DOCKSTAT_API}/docker/hosts/add`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ clientId, ...config }) + }); + + return response.json(); +} + +// Get container stats + +async function getContainerStats(clientId: number) { + const response = await fetch(`${DOCKSTAT_API}/docker/containers/all/${clientId}`); + return response.json(); +} +``` + +## Docker Integration + +### Docker Daemon Connection + +```mermaidjs + +graph TB + subgraph "Connection Types" + SOCKET["Unix Socket"] + TCP["TCP Connection"] + TLS["TLS/SSL"] + SSH["SSH Tunnel"] + end + + subgraph "DockerClient" + DCM["DockerClientManager"] + WORKER["Worker Pool"] + MONITOR["Monitoring Manager"] + end + + subgraph "Docker Hosts" + LOCAL["Local Docker"] + REMOTE1["Remote Host 1"] + REMOTE2["Remote Host 2"] + end + + SOCKET --> LOCAL + TCP --> REMOTE1 + TLS --> REMOTE2 + DCM --> WORKER + WORKER --> SOCKET + WORKER --> TCP + WORKER --> TLS + MONITOR --> WORKER +``` + +### Local Docker Socket + +```typescript +import DockerClient from "@dockstat/docker-client"; + +const client = new DockerClient(db.getDB(), { + enableMonitoring: true +}); + +// Register local client using Unix socket + +const clientId = await client.registerClient("local"); + +// Add local Docker host + +await client.addHost({ + id: 1, + clientId, + host: "/var/run/docker.sock", + name: "Local Docker", + secure: false, + port: 0 // Not used for socket connections +}); +``` + +### Remote Docker Host (TCP) + +```typescript +// Remote Docker host over TCP + +await client.addHost({ + id: 2, + clientId, + host: "192.168.1.100", + port: 2375, + name: "Remote Docker", + secure: false +}); +``` + +### Secure Docker Host (TLS) + +```typescript +// Remote Docker host with TLS + +await client.addHost({ + id: 3, + clientId, + host: "production.docker.local", + port: 2376, + name: "Production Docker", + secure: true, + // TLS certificates would be configured separately +}); +``` + +### Container Event Streaming + +```mermaidjs + +sequenceDiagram + participant Client as "DockerClient" + participant Docker as "Docker Daemon" + participant Stream as "StreamManager" + participant WS as "WebSocket" + participant UI as "Frontend" + + Client->>Docker: "Subscribe to events" + Docker-->>Stream: "Event stream" + + loop "Container Events" + Docker->>Stream: "Container event" + Stream->>WS: "Broadcast event" + WS->>UI: "Real-time update" + end +``` + +```typescript +import { StreamManager, STREAM_CHANNELS } from "@dockstat/docker-client"; + +// Subscribe to container events + +const streamManager = new StreamManager(); + +streamManager.subscribe(STREAM_CHANNELS.CONTAINER_STATS, (stats) => { + console.log("Container stats:", stats); +}); + +streamManager.subscribe(STREAM_CHANNELS.CONTAINER_EVENTS, (event) => { + console.log("Container event:", event.Action, event.Actor.ID); +}); +``` + +## Plugin Integration + +### Plugin System Architecture + +```mermaidjs + +graph TB + subgraph "Plugin Lifecycle" + INSTALL["Install"] + LOAD["Load"] + ACTIVATE["Activate"] + RUN["Running"] + DEACTIVATE["Deactivate"] + UNLOAD["Unload"] + DELETE["Delete"] + end + + subgraph "Plugin Capabilities" + ROUTES["API Routes"] + TABLES["Database Tables"] + HOOKS["Event Hooks"] + ACTIONS["Action Chains"] + end + + INSTALL --> LOAD + LOAD --> ACTIVATE + ACTIVATE --> RUN + RUN --> DEACTIVATE + DEACTIVATE --> UNLOAD + UNLOAD --> DELETE + + RUN --> ROUTES + RUN --> TABLES + RUN --> HOOKS + RUN --> ACTIONS +``` + +### Installing Plugins + +```typescript +import PluginHandler from "@dockstat/plugin-handler"; + +const handler = new PluginHandler(db.getDB()); + +// Install from GitHub manifest URL + +await handler.installFromManifestLink( + "https://raw.githubusercontent.com/user/plugin/main/manifest.yml" +); + +// Or install directly + +const result = await handler.savePlugin({ + name: "my-plugin", + version: "1.0.0", + repository: "https://github.com/user/plugin", + manifest: "https://github.com/user/plugin/manifest.yml", + author: { name: "Developer", email: "dev@example.com" }, + tags: ["monitoring"], + repoType: "github", + plugin: pluginCode +}); +``` + +### Plugin Route Integration + +```typescript +// Plugin with custom API routes + +const plugin = { + name: "metrics-plugin", + version: "1.0.0", + config: { + table: { + name: "metrics_data", + columns: { + id: column.id(), + metric_name: column.text({ notNull: true }), + value: column.real(), + timestamp: column.createdAt() + } + }, + apiRoutes: { + "/metrics": { + method: "GET", + actions: ["getMetrics"] + }, + "/metrics/:name": { + method: "GET", + actions: ["getMetricByName"] + }, + "/metrics": { + method: "POST", + actions: ["validateMetric", "saveMetric"] + } + }, + actions: { + getMetrics: ({ table }) => { + return table.select(["*"]).orderBy("timestamp").desc().all(); + }, + getMetricByName: ({ table, params }) => { + return table.select(["*"]).where({ metric_name: params.name }).all(); + }, + validateMetric: ({ body }) => { + if (!body.name || body.value === undefined) { + throw new Error("Invalid metric data"); + } + return { valid: true, data: body }; + }, + saveMetric: ({ table, previousResult }) => { + const { data } = previousResult; + return table.insert({ + metric_name: data.name, + value: data.value + }); + } + } + } +}; +``` + +### Event Hook Integration + +```typescript +// Plugin with Docker event hooks + +const eventPlugin = { + name: "container-logger", + version: "1.0.0", + config: { + table: { + name: "container_events", + columns: { + id: column.id(), + container_id: column.text(), + event_type: column.text(), + timestamp: column.createdAt() + } + } + }, + events: { + onContainerStart: async ({ container, logger, table }) => { + logger.info(`Container started: ${container.id}`); + await table.insert({ + container_id: container.id, + event_type: "start" + }); + }, + onContainerStop: async ({ container, logger, table }) => { + logger.info(`Container stopped: ${container.id}`); + await table.insert({ + container_id: container.id, + event_type: "stop" + }); + }, + onContainerRestart: async ({ container, logger, table }) => { + logger.info(`Container restarted: ${container.id}`); + await table.insert({ + container_id: container.id, + event_type: "restart" + }); + } + } +}; +``` + +## DockNode Integration + +### DockNode Architecture + +```mermaidjs + +graph TB + subgraph "DockStat Main" + API["DockStat API"] + UI["Frontend"] + end + + subgraph "Remote Nodes" + DN1["DockNode 1"] + DN2["DockNode 2"] + DN3["DockNode 3"] + end + + subgraph "Docker Hosts" + DH1["Docker Host A"] + DH2["Docker Host B"] + DH3["Docker Host C"] + end + + API --> DN1 + API --> DN2 + API --> DN3 + DN1 --> DH1 + DN2 --> DH2 + DN3 --> DH3 + UI --> API +``` + +### DockNode Connection + +```typescript +// DockNode client integration + +const DOCKNODE_URL = "http://remote-node:4000/api"; + +// Deploy a stack to remote node +async function deployStack(nodeUrl: string, stack: StackConfig) { + const response = await fetch(`${nodeUrl}/dockstack/deploy`, { + method: "POST", + headers: { + "Content-Type": "application/json", + "Authorization": `Bearer ${AUTH_TOKEN}` + }, + body: JSON.stringify({ + id: stack.id, + name: stack.name, + data: stack.composeFile, + vars: stack.variables + }) + }); + + return response.json(); +} + +// Delete a stack from remote node +async function deleteStack(nodeUrl: string, stackId: number, name: string) { + const response = await fetch(`${nodeUrl}/dockstack/delete`, { + method: "DELETE", + headers: { + "Content-Type": "application/json", + "Authorization": `Bearer ${AUTH_TOKEN}` + }, + body: JSON.stringify({ id: stackId, name }) + }); + + return response.json(); +} +``` + +### Authentication + +```typescript +// DockNode authentication configuration + +const authConfig = { + // Production: Pre-shared key + psk: process.env.DOCKNODE_DOCKSTACK_AUTH_PSK, + + // Development: Dev auth key + devAuth: process.env.DOCKNODE_DOCKSTACK_DEV_AUTH, + + // Priority: 'psk' | 'dev' | 'none' + priority: process.env.DOCKNODE_DOCKSTACK_AUTH_PRIORITY || 'psk' +}; +``` + +## DockStore Integration + +### Template Installation + +```mermaidjs + +sequenceDiagram + participant User as "User" + participant UI as "DockStat UI" + participant API as "DockStat API" + participant DST as "DockStore" + participant PH as "PluginHandler" + + User->>UI: "Browse templates" + UI->>DST: "Fetch template list" + DST-->>UI: "Available templates" + User->>UI: "Select template" + UI->>API: "Install template" + API->>DST: "Download template" + DST-->>API: "Template files" + API->>PH: "Register plugin" + PH-->>API: "Plugin installed" + API-->>UI: "Installation complete" +``` + +### Plugin Installation from DockStore + +```typescript +// Install a plugin from DockStore + +async function installFromDockStore(pluginName: string) { + const manifestUrl = `https://raw.githubusercontent.com/Its4Nik/DockStat/main/apps/dockstore/src/content/plugins/${pluginName}/manifest.yml`; + + const result = await pluginHandler.installFromManifestLink(manifestUrl); + + if (result.success) { + // Activate the plugin + await pluginHandler.loadPlugins([result.id]); + } + + return result; +} +``` + +## Prometheus Integration + +### Metrics Endpoint + +```mermaidjs + +graph LR + subgraph "DockStat" + API["API Server"] + METRICS["/api/v2/metrics"] + end + + subgraph "Prometheus" + SCRAPER["Scraper"] + STORAGE["Time Series DB"] + end + + subgraph "Visualization" + GRAFANA["Grafana"] + end + + API --> METRICS + SCRAPER --> METRICS + SCRAPER --> STORAGE + STORAGE --> GRAFANA +``` + +### Prometheus Configuration + +```yaml +# prometheus.yml + +scrape_configs: + - job_name: 'dockstat' + scrape_interval: 15s + static_configs: + - targets: ['localhost:9876'] + metrics_path: '/api/v2/metrics' +``` + +### Available Metrics + +```prometheus +# HTTP request metrics +http_requests_total{method="GET", path="/api/v2/docker/containers/all", status="200"} 1234 +http_request_duration_seconds_bucket{method="GET", path="/api/v2/docker/containers/all", le="0.1"} 1000 + +# Database metrics +dockstat_db_size_bytes 1048576 +dockstat_db_table_count 5 + +# Docker metrics +dockstat_containers_total{host="local"} 15 +dockstat_containers_running{host="local"} 12 + +# Memory metrics +process_resident_memory_bytes 52428800 +``` + +## Webhook Integration + +### Outgoing Webhooks + +```typescript +// Plugin with webhook notifications + +const webhookPlugin = { + name: "webhook-notifier", + version: "1.0.0", + config: { + table: { + name: "webhook_config", + columns: { + id: column.id(), + url: column.text({ notNull: true }), + events: column.json(), + active: column.boolean() + }, + jsonColumns: ["events"] + } + }, + events: { + onContainerStart: async ({ container, table }) => { + const webhooks = await table + .select(["*"]) + .where({ active: true }) + .all(); + + for (const webhook of webhooks) { + if (webhook.events.includes("container.start")) { + await fetch(webhook.url, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + event: "container.start", + container: { + id: container.id, + name: container.Names[0], + image: container.Image + }, + timestamp: new Date().toISOString() + }) + }); + } + } + } + } +}; +``` + +### Incoming Webhooks + +```typescript +// Elysia route for incoming webhooks +app.post("/api/v2/webhooks/:source", async ({ params, body }) => { + const { source } = params; + + switch (source) { + case "github": + return handleGitHubWebhook(body); + case "docker-hub": + return handleDockerHubWebhook(body); + default: + return { error: "Unknown webhook source" }; + } +}); +``` + +## React Component Integration + +### Using DockStat UI Components + +```typescript +import { Card, Button, Badge, Table } from "@dockstat/ui"; + +function ContainerList({ containers }) { + return ( + + ({ + name: c.Names[0], + image: c.Image, + status: ( + + {c.State} + + ) + }))} + /> + + + ); +} +``` + +### Theme Integration + +```typescript +import DockStatDB from "@dockstat/db"; +import type { THEME } from "@dockstat/typings"; + +// Load current theme +const db = new DockStatDB(); +const theme = db.getCurrentTheme(); + +// Apply theme variables to CSS +function applyTheme(theme: THEME.THEME_config) { + const root = document.documentElement; + + // Apply background + const bg = theme.vars.background_effect; + if ("Gradient" in bg) { + root.style.setProperty("--bg-from", bg.Gradient.from); + root.style.setProperty("--bg-to", bg.Gradient.to); + root.style.setProperty("--bg-direction", bg.Gradient.direction); + } + + // Apply component styles + const card = theme.vars.components.Card; + root.style.setProperty("--card-accent", card.accent); + root.style.setProperty("--card-border", card.border); +} +``` + +## Testing Integration + +### Integration Test Setup + +```typescript +import { describe, it, expect, beforeAll, afterAll } from "bun:test"; +import DockStatDB from "@dockstat/db"; +import DockerClient from "@dockstat/docker-client"; +import PluginHandler from "@dockstat/plugin-handler"; + +describe("Integration Tests", () => { + let db: DockStatDB; + let dockerClient: DockerClient; + let pluginHandler: PluginHandler; + + beforeAll(() => { + db = new DockStatDB(); + dockerClient = new DockerClient(db.getDB(), { enableMonitoring: false }); + pluginHandler = new PluginHandler(db.getDB()); + }); + + afterAll(() => { + db.close(); + }); + + it("should share database between components", () => { + const dbPath1 = db.getDatabasePath(); + // Docker client and plugin handler use the same DB + expect(dbPath1).toBeDefined(); + }); + + it("should install and load plugins", async () => { + const result = await pluginHandler.savePlugin({ + name: "test-plugin", + version: "1.0.0", + // ... plugin config + }); + + expect(result.success).toBe(true); + + const loaded = await pluginHandler.loadPlugins([result.id]); + expect(loaded.successes).toContain(result.id); + }); +}); +``` + +## Best Practices + +### Error Handling + +```typescript +import Logger from "@dockstat/logger"; + +const log = new Logger("Integration"); + +async function safeApiCall( + operation: () => Promise, + context: string +): Promise { + try { + return await operation(); + } catch (error) { + log.error(`${context}: ${error.message}`); + return null; + } +} + +// Usage +const containers = await safeApiCall( + () => dockerClient.getAllContainers(clientId), + "Fetching containers" +); +``` + +### Resource Cleanup + +```typescript +// Proper cleanup on shutdown +process.on("SIGTERM", async () => { + log.info("Shutting down gracefully..."); + + // Stop monitoring + await dockerClient.stopAllMonitoring(); + + // Unload plugins + await pluginHandler.unloadAllPlugins(); + + // Close database + db.close(); + + process.exit(0); +}); +``` + +### Connection Pooling + +```typescript +// Reuse database and client instances +class ServiceContainer { + private static db: DockStatDB; + private static dockerClient: DockerClient; + private static pluginHandler: PluginHandler; + + static getDB(): DockStatDB { + if (!this.db) { + this.db = new DockStatDB(); + } + return this.db; + } + + static getDockerClient(): DockerClient { + if (!this.dockerClient) { + this.dockerClient = new DockerClient(this.getDB().getDB(), { + enableMonitoring: true + }); + } + return this.dockerClient; + } + + static getPluginHandler(): PluginHandler { + if (!this.pluginHandler) { + this.pluginHandler = new PluginHandler(this.getDB().getDB()); + } + return this.pluginHandler; + } +} +``` + +## Related Documentation + +| Section | Description | +|----|----| +| [Architecture](/doc/d56ca448-563a-4206-9585-c45f8f6be5cf) | System design and component relationships | +| [API Reference](/doc/b174143d-f906-4f8d-8cb5-9fc96512e575) | Complete API endpoint documentation | +| [Configuration](/doc/dec1cb2c-9a13-4e67-a31c-d3a685391208) | Configuration options and settings | +| [Packages](./packages/) | Individual package documentation | +| [Troubleshooting](/doc/88a5f959-3f89-4266-9d8e-eb50193425b0) | Common issues and solutions | \ No newline at end of file diff --git a/apps/docs/dockstat/maintaining-functions/README.md b/apps/docs/dockstat/maintaining-functions/README.md deleted file mode 100644 index 4cb2fdac..00000000 --- a/apps/docs/dockstat/maintaining-functions/README.md +++ /dev/null @@ -1,368 +0,0 @@ ---- -id: 16c1541b-03ec-4444-924b-d585c300b9fd -title: Maintaining functions -collectionId: b4a5e48f-f103-480b-9f50-8f53f515cab9 -parentDocumentId: 9550ca5f-3b09-4e4d-9ea9-634b4cc1553d -updatedAt: 2025-08-18T22:50:49.294Z -urlId: svuZbEHH9g ---- - -# Creating dependency graphs - -```bash -#!/bin/bash -cd src || exit 1 -TMP=$(mktemp) - -cat ./server.ts | grep "./routes" | awk '{print $2,$4}' > $TMP - -spawn_worker(){ - local line="$1" - local target_route="$(echo "$line" | cut -d '"' -f2).ts" - local route=$(echo "$line" | awk '{print $1}') - - echo - echo "Route: $route" - echo ${target_route} - - sleep 0.5 - - npx depcruise \ - -p cli-feedback \ - -T mermaid \ - -x "../node_modules|logger|.dependency-cruiser|path|fs" \ - -f ./misc/dependencyGraphs/mermaid-${route}.txt \ - ${target_route} || exit 1 -} - -while read line; do - spawn_worker "$line" & -done < <(cat $TMP) - -npx depcruise \ - -p cli-feedback \ - -T mermaid \ - -x "../node_modules|logger|.dependency-cruiser|path|fs" \ - -f ./misc/dependencyGraphs/mermaid-all.txt \ - ./server.ts || exit 1 - -wait - -sleep 0.5 - -echo -e "\n========\n\n DONE\n\n========" - -exit 0 -``` - -The script initializes a temp file (`$TMP`), where we dump all available routes which are initialized in the main server.js file. - -Then we loop through that temp file and execute the depcruiser npm package via npx and save the output to `/misc/dependencyGraphs/${FILE}.txt`. - -Since the *mermaid diagram renderer* is a bit outdated here in Outline we are going to use the [mermaid.live](https://mermaid.live) preview and embed it that way. - - ---- - -# Removing unused dependencies - -```bash -#!/bin/bash - -TMP="$(npx depcheck --ignores dependency-cruiser,tsx,@types/bcrypt,@types/express,@types/express-handlebars,@types/node,ts-node --quiet --oneline | tail -n 1 | tr -d '\n')" - -lines=$(echo "$TMP" | tr -s ' ' '\n' | wc -l) - -if ((lines == 0)); then - echo "No unused dependencies." -else - echo - echo "Removing these unused dependencies:" - for entry in $TMP; do - echo "$entry" - done - echo -fi - - -read -n 1 -p "Delete unused dependencies? (y/n) " input -echo - -case $input in - Y|y) - COMMAND=$(echo "npm remove $TMP") - $COMMAND - exit 0 - ;; - *) - echo "Aborting" - exit 1 - ;; -esac - -exit 2 -``` - - ---- - -# Automated testing – In VS Code - -When opening the Project in VS Code you can see the testing tab in the sidebar (typically on the left). - -When opening said "Testing" tab you can see all 2 currently available tests: ![Test tab](/api/attachments.redirect?id=4fb23db8-1789-4749-a400-fd033bc42ead) - -The test take about 2 minutes since we have a timeout of 2 seconds in between request due to making sure that the previous section is done. - - ![Output of a running test](/api/attachments.redirect?id=ae630b90-2fad-42f5-90e6-ec55d5b32418) - - ---- - -# Minifying compiled JavaScript - -This executes when running `npm run mini`. - -```bash -#!/bin/bash - -dist="$(pwd)/dist" - -run_script() { - echo -ne "\r⏳ Minifying : $(basename "$1")" - npx uglifyjs --no-annotations --in-situ "$1" > /dev/null - echo -ne "\r✔️ Minified : $(basename "$1")\n" -} - -if [ -d "$dist" ]; then - echo "::: Dist directory exists." -else - echo "::: Dist does not exist... Running npx tsc" - npx tsc -fi - -export -f run_script - -find "$dist" -type f -exec bash -c 'run_script "$0"' {} \; - -echo - -if [[ $1 == "--build-only" ]]; then - exit 0 -fi - -node dist/server.js -``` - -The goal of the script is to minify and compress the compiled JavaScript as much as possible.\nThat's why `find` traverses all directories and files inside `./dist` and runs `uglifyjs` on all found files. - -## Example Data - -```bash -"use strict";var __importDefault=this&&this.__importDefault||function(mod){return mod&&mod.__esModule?mod:{default:mod}};Object.defineProperty(exports,"__esModule",{value:true});const express_1=__importDefault(require("express"));const swaggerDocs_1=__importDefault(require("./swagger/swaggerDocs"));const logger_1=__importDefault(require("./utils/logger"));const routes_1=__importDefault(require("./routes/auth/routes"));const routes_2=__importDefault(require("./routes/data/routes"));const routes_3=__importDefault(require("./routes/frontendController/routes"));const routes_4=__importDefault(require("./routes/getter/routes"));const routes_5=__importDefault(require("./routes/notifications/routes"));const routes_6=__importDefault(require("./routes/setter/routes"));const authMiddleware_1=__importDefault(require("./middleware/authMiddleware"));const routes_7=__importDefault(require("./routes/highavailability/routes"));const proxy_1=__importDefault(require("./controllers/proxy"));const rateLimiter_1=require("./middleware/rateLimiter");const scheduler_1=require("./controllers/scheduler");const highAvailability_1=require("./controllers/highAvailability");const cors_1=__importDefault(require("cors"));const app=(0,express_1.default)();const PORT=9876;app.use((0,cors_1.default)());app.use(express_1.default.json());app.use("/api-docs",(req,res,next)=>next());(0,swaggerDocs_1.default)(app);(0,proxy_1.default)(app);(0,scheduler_1.scheduleFetch)();app.use("/api",rateLimiter_1.limiter,authMiddleware_1.default,routes_4.default);app.use("/conf",rateLimiter_1.limiter,authMiddleware_1.default,routes_6.default);app.use("/auth",rateLimiter_1.limiter,authMiddleware_1.default,routes_1.default);app.use("/data",rateLimiter_1.limiter,authMiddleware_1.default,routes_2.default);app.use("/frontend",rateLimiter_1.limiter,authMiddleware_1.default,routes_3.default);app.use("/notification-service",rateLimiter_1.limiter,authMiddleware_1.default,routes_5.default);app.use("/ha",rateLimiter_1.limiter,authMiddleware_1.default,routes_7.default);app.get("/",(req,res)=>{res.redirect("/api-docs")});app.listen(PORT,()=>{logger_1.default.info(`Server is running on http://localhost:${PORT}`);logger_1.default.info(`Swagger docs available at http://localhost:${PORT}/api-docs`);(0,highAvailability_1.startMasterNode)()}); -``` - -This brings down the image size even more so that updates and installs are faster on a slower internet connection :sunglasses: - - ---- - -# credits.sh - -Since I don't want any legal actions against me, I am going to credit every dependency I am using (which requires it according to the license), that's why this script exists: - -```bash -#!/bin/bash - -if ! command -v jq 2>&1 >/dev/null -then - echo "ERROR: jq could not be found" - exit 1 -fi - - -LICENSE_JSON=$(npx license-checker \ - --exclude 'MIT, MIT-0, MIT OR X11, BSD, ISC, Unlicense, CC0-1.0, Python-2.0: 1' \ - --json) - -{ - echo -e "# CREDITS\n" - echo -e "This file shows all npm packages used in DockStatAPI (also Dev packages)\n" -} > CREDITS.md - -jq -r ' - to_entries | - group_by(.value.licenses)[] | - "### License: \(.[0].value.licenses)\n\n" + - "| Name | Repository | Publisher |\n|------|-------------|-----------|\n" + - (map( - "| \(.key) | \(.value.repository // "N/A") | \(.value.publisher // "N/A") |" - ) | join("\n")) + "\n\n" -' <<< "$LICENSE_JSON" >> CREDITS.md - -echo "Markdown file with license information has been created: CREDITS.md" -``` - -This will run a dependency (*ironic isn't it?*) which will check all licenses of all dependencies and puts them inside `./CREDITS.md` - - ---- - -# Creating a local environment file - -Path: `./src/misc/createEnvDev.sh` - -```bash -#!/bin/bash - -# Version -VERSION="$(cat ./package.json | grep version | cut -d '"' -f 4)" - -# Docker -if grep -q '/docker' /proc/1/cgroup 2>/dev/null || [ -f /.dockerenv ]; then - RUNNING_IN_DOCKER="true" -else - RUNNING_IN_DOCKER="false" -fi - -echo -n "\ -{ - \"VERSION\": \"${VERSION}\", - \"RUNNING_IN_DOCKER\": \"${RUNNING_IN_DOCKER}\", - \"TRUSTED_PROXYS\": \"${TRUSTED_PROXYS}\", - \"HA_MASTER\": \"${HA_MASTER}\", - \"HA_MASTER_IP\": \"${HA_MASTER_IP}\", - \"HA_NODE\": \"${HA_NODE}\", - \"HA_UNSAFE\": \"${HA_UNSAFE}\", - \"DISCORD_WEBHOOK_URL\": \"${DISCORD_WEBHOOK_URL}\", - \"EMAIL_SENDER\": \"${EMAIL_SENDER}\", - \"EMAIL_RECIPIENT\": \"${EMAIL_RECIPIENT}\", - \"EMAIL_PASSWORD\": \"${EMAIL_PASSWORD}\", - \"EMAIL_SERVICE\": \"${EMAIL_SERVICE}\", - \"PUSHBULLET_ACCESS_TOKEN\": \"${PUSHBULLET_ACCESS_TOKEN}\", - \"PUSHOVER_USER_KEY\": \"${PUSHOVER_USER_KEY}\", - \"PUSHOVER_API_TOKEN\": \"${PUSHOVER_API_TOKEN}\", - \"SLACK_WEBHOOK_URL\": \"${SLACK_WEBHOOK_URL}\", - \"TELEGRAM_BOT_TOKEN\": \"${TELEGRAM_BOT_TOKEN}\", - \"TELEGRAM_CHAT_ID\": \"${TELEGRAM_CHAT_ID}\", - \"WHATSAPP_API_URL\": \"${WHATSAPP_API_URL}\", - \"WHATSAPP_RECIPIENT\": \"${WHATSAPP_RECIPIENT}\" -} \ -" > ./src/data/variables.json -``` - -This file will create a JSON file (`./src/data/variables.json`) which the backend will read (based on user configuration). - -For the keen eyed: There is also a `createEnvFile.sh`, this does the same (just an adjusted path) for use inside the docker image. - - ---- - -# npm run functions: - -## npm run docker:full - -### Code - -```bash -docker compose up -d && \ -[ -z \"$TMUX\" ] && \ -tmux new-session -d -s docker 'docker compose logs -f master' \\; \ -split-window -v 'docker compose logs -f slave' \\; \ -attach-session || echo 'Already inside a tmux session. Exiting.'; \ -docker compose down -``` - -### Explanation - - -1. `docker compose up -d`: starts the docker-compose.yaml -2. `[ -z \"$TMUX\" ]`: tests if $TMUX is set -3. `tmux new-session -d -s docker 'docker compose logs -f master'`: Creates a TMUX session, named docker, with the default window of: `docker compose logs -f master` -4. `split-window -v 'docker compose logs -f slave'`: splits the TMUX window -5. `attach-session`: Attach to the new TMUX session -6. `|| echo 'Already inside a tmux session. Exiting.'`: aborts if attaching to session fails -7. `docker compose down`: Runs after the tmux session is closed, will shut down the docker compose stack - - ---- - -## npm run docker:build - -### Code - -```bash -docker build . -t \"dockstatapi:local\" -f ./Dockerfile-dev && \ -docker compose up -d -``` - -### Explanation - -Builds the local docker image using the Dockerfile-dev and starts the docker compose stack - - -:::info -Differences between Dockerfile and Dockerfile-dev: - -Line 27: - -* Dockerfile: - - `RUN npm run build:mini` -* Dockerfile-dev: - - `RUN npm run build` - -The difference is that `npm run build:mini` will remove swagger documentation since it is based on comments. - -::: - - ---- - -## npm run docker:build:full - -### Code - -```bash -npm run docker:build && -[ -z \"$TMUX\" ] && -tmux new-session -d -s docker 'docker compose up -d && -docker compose logs -f master' \\; -split-window -v 'docker compose logs -f slave' \\; -attach-session || echo 'Already inside a tmux session. Exiting.'; -docker compose down" -``` - -### Explanation - - -1. Runs the default docker:build run command -2. Checks if TMUX is already active -3. Creates a new tmux session and starts the docker-compose file (detached) -4. Follows the logs of the master container -5. Splits TMUX window and follows the logs of the slave container -6. Attaches to the session (fails if already inside a session) -7. Stops the entire docker compose stack - - ---- - -## npm run prettier - -### Code - -```bash -npx prettier -c ./src/**/*.ts --parser typescript --write && \ -npx prettier -c ./.github/workflows/*.{yaml,yml} --parser yaml --write && \ -npx prettier -c ./**/*.md --parser markdown --write && \ -npx prettier -c ./**/*.json --parser json --write -``` - -### Explanation - - -1. "prettifies" all typescript files -2. "prettifies" all GitHub workflows -3. "prettifies" all markdown files -4. "prettifies" all JSON files \ No newline at end of file diff --git a/apps/docs/dockstat/notifications/README.md b/apps/docs/dockstat/notifications/README.md deleted file mode 100644 index 692a2055..00000000 --- a/apps/docs/dockstat/notifications/README.md +++ /dev/null @@ -1,205 +0,0 @@ ---- -id: fc21da6e-6893-48a8-af83-b308012d6ac9 -title: Notifications -collectionId: b4a5e48f-f103-480b-9f50-8f53f515cab9 -parentDocumentId: 9550ca5f-3b09-4e4d-9ea9-634b4cc1553d -updatedAt: 2025-08-18T22:50:50.358Z -urlId: cBAMg6Z7so ---- - -DockStatAPI comes with 7 default notification providers: - - -1. Discord (Webhooks) -2. E-Mail -3. Pushbullet -4. Pushover -5. Slack -6. Telegram -7. WhatsApp (Official WhatsApp API token needed) - - ---- - -# Configure Notification template - -We use a templating functionality for the notification messages, this is the default template: - -```json -{ - "text": "{{name}} ({{id}}) on {{hostName}} is {{state}}" -} -``` - -Usable variables: - -| Identifier | Example data | -|----|----| -| `name` | "My-Awesome-Container" | -| `id` | "a6b6c34350b40f310fa24b0a3564b0e8897ca604acd483713cad8a27b7284cef" | -| `state` | "running" | -| `hostName` | "Host-1" | - -The data for those notifications is kept here: `./src/data/states.json` and the template is kept here: `./src/data/template.json`. - -# Configuring Notification providers - - -:::info -All notification providers are configured using environment variables - -::: - -## :three_button_mouse: Discord - -In your docker-compose.yaml: - -```yaml -services: - dockStatAPI: - image: ghcr.io/its4nik/dockstatapi - container_name: "DockStatAPI" - ports: - - 9876:9876 - environment: - - DISCORD_WEBHOOK_URL: "..." -``` - -Just add the URL of the discord Webhook to said variable. - -## :email: E-Mail - -```yaml -services: - dockStatAPI: - image: ghcr.io/its4nik/dockstatapi - container_name: "DockStatAPI" - ports: - - 9876:9876 - environment: - - EMAIL_SENDER: "..." - - EMAIL_RECIPIENT: "..." - - EMAIL_PASSWORD: "..." - - EMAIL_SERVICE: "..." -``` - -Please see [nodemailer/well-known-services](https://community.nodemailer.com/2-0-0-beta/setup-smtp/well-known-services/). - -## :bullettrain_front: Pushbullet - -```yaml -services: - dockStatAPI: - image: ghcr.io/its4nik/dockstatapi - container_name: "DockStatAPI" - ports: - - 9876:9876 - environment: - - PUSHBULLET_ACCESS_TOKEN: "..." -``` - -> To access your Pushbullet token, **navigate to Pushbullet's My Account page.** **It will appear under the Access Token heading**. This is confidential information that your server sends via a secure channel. - -## :bullettrain_side: Pushover - -```yaml -services: - dockStatAPI: - image: ghcr.io/its4nik/dockstatapi - container_name: "DockStatAPI" - ports: - - 9876:9876 - environment: - - PUSHOVER_USER_KEY: "..." - - PUSHOVER_API_TOKEN: "..." -``` - -:link: - -## :wavy_dash: Slack - -```yaml -services: - dockStatAPI: - image: ghcr.io/its4nik/dockstatapi - container_name: "DockStatAPI" - ports: - - 9876:9876 - environment: - - SLACK_WEBHOOK_URL: "..." -``` - -:link: - -## :airplane: Telegram - -```yaml -services: - dockStatAPI: - image: ghcr.io/its4nik/dockstatapi - container_name: "DockStatAPI" - ports: - - 9876:9876 - environment: - - TELEGRAM_BOT_TOKEN: "..." - - TELEGRAM_CHAT_ID: "..." -``` - -:link: - -:link: - -## :telephone_receiver: WhatsApp - - -:::warning -Needs a WhatsApp business plan - -::: - -```yaml -services: - dockStatAPI: - image: ghcr.io/its4nik/dockstatapi - container_name: "DockStatAPI" - ports: - - 9876:9876 - environment: - - WHATSAPP_API_URL: "..." - - WHATSAPP_RECIPIENT: "..." -``` - -:link: - -# Custom notifications - -To add custom notifications you can specify them in a JavaScript file, you can just place them here: `notifications/custom/myCustomNotification.js` - -And specify all custom Notification types with a List inside `CUSTOM_NOTIFICATIONS` - -## Use them with docker - -```yaml -services: - dockStatAPI: - image: ghcr.io/its4nik/dockstatapi - container_name: "DockStatAPI" - ports: - - 9876:9876 - volumes: - - "./dockstatapi/notifications:/api/utils/notifications/custom" - environment: - - CUSTOM_NOTIFICATIONS="myCustomNotification.js,mySecondCustomNotification.js,..." -``` - -## How to write custom notification modules - -```javascript -import { renderTemplate } from "./../_template"; - -export async function myNotification(containerId) { - const message = renderTemplate(containerId); - - // Your custom logic here -} -``` \ No newline at end of file diff --git a/apps/docs/dockstat/packages/@dockstat-db/README.md b/apps/docs/dockstat/packages/@dockstat-db/README.md new file mode 100644 index 00000000..300245d7 --- /dev/null +++ b/apps/docs/dockstat/packages/@dockstat-db/README.md @@ -0,0 +1,670 @@ +--- +id: 5176f3ba-1242-4c85-8290-491dcc0f9963 +title: "@dockstat/db" +collectionId: b4a5e48f-f103-480b-9f50-8f53f515cab9 +parentDocumentId: bbcefaa2-6bd4-46e8-ae4b-a6b823593e67 +updatedAt: 2025-12-17T09:12:31.841Z +urlId: LgnEA0nOUp +--- + +> A TypeScript database layer focused on theme management and database access for Docker container monitoring. Built on top of `@dockstat/sqlite-wrapper` with predefined models for themes and seamless integration with `@dockstat/docker-client`. + +## Overview + +`@dockstat/db` provides a high-level database abstraction for DockStat applications. It manages theme configurations, application settings, and serves as the central database coordinator for all DockStat packages. + +```mermaidjs + +graph TB + subgraph "Application Layer" + API["DockStat API"] + FE["DockStat Frontend"] + end + + subgraph "@dockstat/db" + DOCKSTATDB["DockStatDB Class"] + THEMES["Theme Manager"] + CONFIG["Config Manager"] + DEFAULTS["Default Data"] + end + + subgraph "Dependencies" + SW["@dockstat/sqlite-wrapper"] + TYP["@dockstat/typings"] + end + + subgraph "Consumers" + DC["@dockstat/docker-client"] + PH["@dockstat/plugin-handler"] + end + + subgraph "Storage" + SQLITE["SQLite Database"] + end + + API --> DOCKSTATDB + FE --> DOCKSTATDB + DOCKSTATDB --> THEMES + DOCKSTATDB --> CONFIG + DOCKSTATDB --> DEFAULTS + DOCKSTATDB --> SW + DOCKSTATDB --> TYP + DC --> DOCKSTATDB + PH --> DOCKSTATDB + SW --> SQLITE +``` + +## Installation + +```bash +bun add @dockstat/db +``` + +## Quick Start + +```typescript +import DockStatDB from "@dockstat/db"; + +// Initialize database (auto-creates with default theme) +const db = new DockStatDB(); + +// Get current theme +const theme = db.getCurrentTheme(); +console.log(`Current theme: ${theme.name}`); + +// Change theme +db.setTheme("dark-theme"); + +// Get underlying DB for other packages +const sqliteDb = db.getDB(); +``` + +## Architecture + +### Database Schema + +```mermaidjs + +erDiagram + config { + int id PK "Always 1 (singleton)" + string current_theme_name FK "Active theme" + } + + themes { + string name PK "Theme identifier" + string version "Semantic version" + string creator "Theme author" + string license "License type" + json vars "Theme variables (JSON)" + } + + config ||--o| themes : "references" +``` + +### Initialization Flow + +```mermaidjs + +sequenceDiagram + participant App as "Application" + participant DB as "DockStatDB" + participant SW as "sqlite-wrapper" + participant File as "SQLite File" + + App->>DB: "new DockStatDB()" + DB->>SW: "new DB('dockstat.db')" + SW->>File: "Open/Create database" + + DB->>DB: "Create config table" + DB->>DB: "Create themes table" + + DB->>DB: "Check for default theme" + alt "No default theme" + DB->>DB: "Insert default theme" + DB->>DB: "Set config to default" + end + + DB-->>App: "DockStatDB instance" +``` + +## Core Features + +### Theme Management + +```mermaidjs + +graph LR + subgraph "Theme Operations" + ADD["Add Theme"] + GET["Get Theme"] + SET["Set Active"] + LIST["List Themes"] + UPDATE["Update Theme"] + end + + subgraph "Theme Structure" + META["Metadata"] + VARS["Variables"] + end + + subgraph "Variables" + BG["Background Effects"] + COMP["Component Styles"] + FONT["Font Config"] + end + + ADD --> META + ADD --> VARS + VARS --> BG + VARS --> COMP + VARS --> FONT +``` + +#### Adding Themes + +```typescript +import DockStatDB from "@dockstat/db"; +import type { THEME } from "@dockstat/typings"; + +const db = new DockStatDB(); + +const customTheme: THEME.THEME_config = { + name: "ocean-dark", + version: "1.0.0", + creator: "Developer", + license: "MIT", + description: "Ocean-inspired dark theme", + active: false, + vars: { + background_effect: { + Gradient: { + from: "#0a1628", + to: "#1a365d", + direction: "to bottom right" + } + }, + components: { + Card: { + accent: "#3182ce", + border: "1px solid #2c5282", + border_color: "#2c5282", + border_size: "1px", + title: { + font: "Inter", + color: "#ffffff", + font_size: "18px", + font_weight: "600" + }, + sub_title: { + font: "Inter", + color: "#a0aec0", + font_size: "14px", + font_weight: "400" + }, + content: { + font: "Inter", + color: "#e2e8f0", + font_size: "14px", + font_weight: "400" + } + } + } + } +}; + +// Add or update theme +db.addOrUpdateTheme(customTheme); +``` + +#### Getting Themes + +```typescript +// Get specific theme by name +const theme = db.getTheme("ocean-dark"); + +// Get all available themes +const allThemes = db.getThemes(); +console.log(`Available themes: ${allThemes.map(t => t.name).join(", ")}`); + +// Get currently active theme +const currentTheme = db.getCurrentTheme(); + +// Get just the current theme name +const themeName = db.getCurrentThemeName(); +``` + +#### Setting Active Theme + +```typescript +// Set theme by name +db.setTheme("ocean-dark"); + +// Verify the change +const current = db.getCurrentThemeName(); +console.log(`Active theme is now: ${current}`); +``` + +### Database Access + +The primary use case for `@dockstat/db` is providing database access to other DockStat packages: + +```typescript +import DockStatDB from "@dockstat/db"; +import DockerClient from "@dockstat/docker-client"; +import PluginHandler from "@dockstat/plugin-handler"; + +// Create central database instance +const db = new DockStatDB(); + +// Share with Docker client +const dockerClient = new DockerClient(db.getDB(), { + enableMonitoring: true +}); + +// Share with Plugin handler +const pluginHandler = new PluginHandler(db.getDB()); + +// All packages now use the same SQLite database +// - DockStatDB manages themes and config +// - DockerClient manages hosts and containers +// - PluginHandler manages plugins +``` + +### Configuration Management + +```typescript +// The config table stores application-wide settings +// Currently tracks the active theme + +// Internal structure (accessed via theme methods) +interface Config { + id: number; // Always 1 + current_theme_name: string; // References themes.name +} +``` + +## Theme Structure + +### Background Effects + +Three types of background effects are supported: + +```typescript +import type { THEME } from "@dockstat/typings"; + +// Solid color background +const solidBg: THEME.THEME_background_effects = { + Solid: { color: "#1a1a2e" } +}; + +// Gradient background +const gradientBg: THEME.THEME_background_effects = { + Gradient: { + from: "#1a1a2e", + to: "#16213e", + direction: "to bottom right" + } +}; + +// Aurora effect background +const auroraBg: THEME.THEME_background_effects = { + Aurora: { + colors: ["#1a1a2e", "#16213e", "#0f3460"], + speed: "slow" + } +}; +``` + +### Component Styles + +```typescript +import type { THEME } from "@dockstat/typings"; + +const componentStyles: THEME.THEME_components = { + Card: { + accent: "#0f3460", + border: "1px solid #e94560", + border_color: "#e94560", + border_size: "1px", + title: { + font: "Inter", + color: "#ffffff", + font_size: "18px", + font_weight: "600" + }, + sub_title: { + font: "Inter", + color: "#cccccc", + font_size: "14px", + font_weight: "400" + }, + content: { + font: "Inter", + color: "#e0e0e0", + font_size: "14px", + font_weight: "400" + } + } + // Additional components can be added +}; +``` + +### Font Configuration + +```typescript +import type { THEME } from "@dockstat/typings"; + +const fontConfig: THEME.THEME_font_config = { + font: "Inter", // Font family + color: "#ffffff", // Text color + font_size: "16px", // Font size + font_weight: "400" // Font weight +}; +``` + +## API Reference + +### Constructor + +```typescript +new DockStatDB(path?: string) +``` + +Creates a new DockStatDB instance. If no path is provided, defaults to `dockstat.db` in the current directory. + +**Parameters:** + +* `path` (optional): Path to the SQLite database file + +**Example:** + +```typescript +// Default path +const db = new DockStatDB(); + +// Custom path +const db = new DockStatDB("./data/myapp.db"); +``` + +### Database Access Methods + +| Method | Return Type | Description | +|----|----|----| +| `getDB()` | `DB` | Returns the underlying sqlite-wrapper DB instance | +| `close()` | `void` | Closes the database connection | +| `exec(sql)` | `any` | Execute raw SQL query | +| `getSchema()` | `object` | Get database schema information | +| `getDatabasePath()` | `string` | Returns the database file path | + +### Theme Management Methods + +| Method | Return Type | Description | +|----|----|----| +| `addOrUpdateTheme(theme)` | `void` | Add or update a theme | +| `getTheme(name)` | `THEME_config \| null` | Get theme by name | +| `getThemes()` | `THEME_config[]` | Get all themes | +| `setTheme(name)` | `void` | Set the active theme | +| `getCurrentTheme()` | `THEME_config` | Get the currently active theme | +| `getCurrentThemeName()` | `string` | Get the current theme name | + +## Default Theme + +The package includes a default theme that is automatically created on first initialization: + +```typescript +const defaultTheme = { + name: "default", + version: "1.0.0", + creator: "DockStat", + license: "MIT", + description: "Default DockStat theme", + active: true, + vars: { + background_effect: { + Solid: { color: "#1a1a1a" } + }, + components: { + Card: { + accent: "#3b82f6", + border: "1px solid #374151", + border_color: "#374151", + border_size: "1px", + title: { + font: "Inter", + color: "#f9fafb", + font_size: "18px", + font_weight: "600" + }, + sub_title: { + font: "Inter", + color: "#9ca3af", + font_size: "14px", + font_weight: "400" + }, + content: { + font: "Inter", + color: "#e5e7eb", + font_size: "14px", + font_weight: "400" + } + } + } + } +}; +``` + +## Usage Patterns + +### Singleton Pattern + +```typescript +// Recommended: Use a singleton for the entire application + +class DatabaseService { + private static instance: DockStatDB; + + static getInstance(): DockStatDB { + if (!this.instance) { + this.instance = new DockStatDB(); + } + return this.instance; + } +} + +// Usage + +const db = DatabaseService.getInstance(); +``` + +### Theme Switching in Frontend + +```typescript +import DockStatDB from "@dockstat/db"; + +const db = new DockStatDB(); + +// React/frontend integration +function useTheme() { + const [theme, setTheme] = useState(db.getCurrentTheme()); + + const changeTheme = (themeName: string) => { + db.setTheme(themeName); + setTheme(db.getCurrentTheme()); + applyThemeToDOM(db.getCurrentTheme()); + }; + + return { theme, changeTheme }; +} + +function applyThemeToDOM(theme: THEME.THEME_config) { + const root = document.documentElement; + + // Apply background + const bg = theme.vars.background_effect; + if ("Solid" in bg) { + root.style.setProperty("--bg-color", bg.Solid.color); + } else if ("Gradient" in bg) { + root.style.setProperty("--bg-from", bg.Gradient.from); + root.style.setProperty("--bg-to", bg.Gradient.to); + } + + // Apply component styles + const card = theme.vars.components.Card; + root.style.setProperty("--card-accent", card.accent); + root.style.setProperty("--card-border", card.border); + root.style.setProperty("--card-title-color", card.title.color); +} +``` + +### Integration with Docker Client + +```typescript +import DockStatDB from "@dockstat/db"; +import DockerClient from "@dockstat/docker-client"; + +class AppServices { + private db: DockStatDB; + private dockerClient: DockerClient; + + constructor() { + // DockStatDB creates and manages the database + this.db = new DockStatDB(); + + // DockerClient uses the same database for host storage + this.dockerClient = new DockerClient(this.db.getDB(), { + enableMonitoring: true + }); + } + + getTheme() { + return this.db.getCurrentTheme(); + } + + async getContainers(clientId: number) { + return await this.dockerClient.getAllContainers(clientId); + } + + cleanup() { + this.db.close(); + } +} +``` + +## Error Handling + +```typescript +import DockStatDB from "@dockstat/db"; +import Logger from "@dockstat/logger"; + +const log = new Logger("Database"); + +try { + const db = new DockStatDB(); + + // Theme operations + const theme = db.getTheme("nonexistent"); + if (!theme) { + log.warn("Theme not found, using default"); + db.setTheme("default"); + } + +} catch (error) { + if (error.code === "SQLITE_CANTOPEN") { + log.error("Cannot open database file"); + } else if (error.code === "SQLITE_CORRUPT") { + log.error("Database is corrupted"); + } else { + log.error(`Database error: ${error.message}`); + } +} +``` + +## Testing + +```typescript +import { describe, it, expect, beforeEach, afterEach } from "bun:test"; +import DockStatDB from "@dockstat/db"; +import { unlinkSync } from "fs"; + +describe("DockStatDB", () => { + let db: DockStatDB; + const testDbPath = "./test.db"; + + beforeEach(() => { + db = new DockStatDB(testDbPath); + }); + + afterEach(() => { + db.close(); + try { + unlinkSync(testDbPath); + unlinkSync(`${testDbPath}-wal`); + unlinkSync(`${testDbPath}-shm`); + } catch {} + }); + + it("should initialize with default theme", () => { + const theme = db.getCurrentTheme(); + expect(theme.name).toBe("default"); + }); + + it("should add and retrieve custom theme", () => { + db.addOrUpdateTheme({ + name: "test-theme", + version: "1.0.0", + creator: "Test", + license: "MIT", + vars: { /* ... */ } + }); + + const theme = db.getTheme("test-theme"); + expect(theme).not.toBeNull(); + expect(theme.name).toBe("test-theme"); + }); + + it("should change active theme", () => { + db.addOrUpdateTheme({ + name: "new-theme", + version: "1.0.0", + creator: "Test", + license: "MIT", + vars: { /* ... */ } + }); + + db.setTheme("new-theme"); + expect(db.getCurrentThemeName()).toBe("new-theme"); + }); + + it("should persist data across instances", () => { + db.addOrUpdateTheme({ + name: "persistent-theme", + version: "1.0.0", + creator: "Test", + license: "MIT", + vars: { /* ... */ } + }); + db.setTheme("persistent-theme"); + db.close(); + + const db2 = new DockStatDB(testDbPath); + expect(db2.getCurrentThemeName()).toBe("persistent-theme"); + db2.close(); + }); +}); +``` + +## Related Packages + +* `@dockstat/sqlite-wrapper` - Underlying SQLite operations +* `@dockstat/typings` - Theme and database type definitions +* `@dockstat/docker-client` - Uses db for host persistence +* `@dockstat/plugin-handler` - Uses db for plugin storage + +## License + +Part of the DockStat project - MIT License. + +## Contributing + +Issues and PRs welcome at [github.com/Its4Nik/DockStat](https://github.com/Its4Nik/DockStat) \ No newline at end of file diff --git a/apps/docs/dockstat/packages/@dockstat-docker-client/README.md b/apps/docs/dockstat/packages/@dockstat-docker-client/README.md new file mode 100644 index 00000000..55197484 --- /dev/null +++ b/apps/docs/dockstat/packages/@dockstat-docker-client/README.md @@ -0,0 +1,822 @@ +--- +id: ef12194c-404e-4bcd-a5b0-31aaf7b1b798 +title: "@dockstat/docker-client" +collectionId: b4a5e48f-f103-480b-9f50-8f53f515cab9 +parentDocumentId: bbcefaa2-6bd4-46e8-ae4b-a6b823593e67 +updatedAt: 2025-12-17T09:16:08.678Z +urlId: X0z5b9Ra01 +--- + +> A comprehensive Docker client library for DockStat built on Dockerode. Provides real-time monitoring, multi-host management, streaming capabilities, and a worker pool architecture for scalable Docker operations. + +## Overview + +`@dockstat/docker-client` is the core Docker integration package for DockStat. It abstracts Docker operations, provides real-time container statistics, manages multiple Docker hosts, and offers event-driven monitoring capabilities. + +```mermaidjs + +graph TB + subgraph "Application" + API["DockStat API"] + end + + subgraph "@dockstat/docker-client" + DCM["DockerClientManager"] + HH["HostHandler"] + MM["MonitoringManager"] + SM["StreamManager"] + WP["Worker Pool"] + end + + subgraph "Dependencies" + DOCKERODE["Dockerode"] + SW["@dockstat/sqlite-wrapper"] + LOG["@dockstat/logger"] + end + + subgraph "Docker Hosts" + LOCAL["Local Docker"] + REMOTE1["Remote Host 1"] + REMOTE2["Remote Host 2"] + end + + API --> DCM + DCM --> HH + DCM --> MM + DCM --> WP + MM --> SM + HH --> SW + DCM --> DOCKERODE + WP --> LOCAL + WP --> REMOTE1 + WP --> REMOTE2 +``` + +## Installation + +```bash +bun add @dockstat/docker-client +``` + +> **Note**: This is an internal package. For external use, ensure all peer dependencies are installed. + +## Quick Start + +```typescript +import DockerClient from "@dockstat/docker-client"; +import DockStatDB from "@dockstat/db"; + +// Initialize database + +const db = new DockStatDB(); + +// Create Docker client with monitoring + +const client = new DockerClient(db.getDB(), { + enableMonitoring: true, + monitoringInterval: 5000 +}); + +// Register a client + +const clientId = await client.registerClient("production"); + +// Add a Docker host + +await client.addHost({ + id: 1, + clientId, + host: "/var/run/docker.sock", + name: "Local Docker", + secure: false, + port: 0 +}); + +// Start monitoring + +await client.startMonitoring(clientId); + +// Get all containers + +const containers = await client.getAllContainers(clientId); +console.log(`Found ${containers.length} containers`); +``` + +## Architecture + +### Component Overview + +```mermaidjs + +graph TB + subgraph "DockerClient" + direction TB + MAIN["Main Controller"] + CONFIG["Configuration"] + STATE["State Management"] + end + + subgraph "HostHandler" + direction TB + HOSTS["Host Registry"] + PERSIST["Persistence Layer"] + HEALTH["Health Checks"] + end + + subgraph "MonitoringManager" + direction TB + STATS["Stats Collection"] + EVENTS["Event Handling"] + INTERVALS["Interval Management"] + end + + subgraph "StreamManager" + direction TB + WS["WebSocket Support"] + CHANNELS["Channel Management"] + BROADCAST["Broadcasting"] + end + + subgraph "Worker Pool" + direction TB + WORKERS["Worker Threads"] + QUEUE["Task Queue"] + BALANCE["Load Balancing"] + end + + MAIN --> CONFIG + MAIN --> STATE + MAIN --> HOSTS + MAIN --> STATS + MAIN --> WS + MAIN --> WORKERS + HOSTS --> PERSIST + HOSTS --> HEALTH + STATS --> EVENTS + STATS --> INTERVALS + WS --> CHANNELS + WS --> BROADCAST + WORKERS --> QUEUE + WORKERS --> BALANCE +``` + +### Data Flow + +```mermaidjs + +sequenceDiagram + participant App as "Application" + participant DC as "DockerClient" + participant HH as "HostHandler" + participant MM as "MonitoringManager" + participant WP as "Worker Pool" + participant Docker as "Docker Daemon" + + App->>DC: "registerClient(name)" + DC->>HH: "Create client entry" + HH-->>DC: "clientId" + DC-->>App: "clientId" + + App->>DC: "addHost(config)" + DC->>HH: "Register host" + DC->>WP: "Create worker for host" + WP->>Docker: "Test connection" + Docker-->>WP: "Connection OK" + WP-->>DC: "Host ready" + DC-->>App: "Host added" + + App->>DC: "startMonitoring(clientId)" + DC->>MM: "Start monitoring loop" + + loop "Every interval" + MM->>WP: "Request stats" + WP->>Docker: "GET /containers/json" + Docker-->>WP: "Container list" + WP->>Docker: "GET /containers/:id/stats" + Docker-->>WP: "Stats stream" + WP-->>MM: "Aggregated stats" + MM-->>App: "Emit stats event" + end +``` + +## Core Features + +### Multi-Host Management + +Manage multiple Docker hosts from a single client instance: + +```typescript +// Register the client + +const clientId = await client.registerClient("multi-host"); + +// Add local Docker + +await client.addHost({ + id: 1, + clientId, + host: "/var/run/docker.sock", + name: "Local", + secure: false, + port: 0 +}); + +// Add remote Docker (TCP) +await client.addHost({ + id: 2, + clientId, + host: "192.168.1.100", + name: "Remote 1", + secure: false, + port: 2375 +}); + +// Add remote Docker (TLS) +await client.addHost({ + id: 3, + clientId, + host: "production.docker.local", + name: "Production", + secure: true, + port: 2376 +}); +``` + +### Real-Time Monitoring + +```mermaidjs + +graph LR + subgraph "Monitoring Flow" + INTERVAL["Interval Timer"] + COLLECT["Stats Collection"] + PROCESS["Data Processing"] + EMIT["Event Emission"] + end + + subgraph "Data Types" + CPU["CPU Usage"] + MEM["Memory Usage"] + NET["Network I/O"] + DISK["Disk I/O"] + end + + INTERVAL --> COLLECT + COLLECT --> PROCESS + PROCESS --> EMIT + EMIT --> CPU + EMIT --> MEM + EMIT --> NET + EMIT --> DISK +``` + +```typescript +import DockerClient, { MonitoringManager } from "@dockstat/docker-client"; + +const client = new DockerClient(db.getDB(), { + enableMonitoring: true, + monitoringInterval: 5000 // 5 seconds +}); + +// Start monitoring + +await client.startMonitoring(clientId); + +// Listen for stats updates + +client.on("stats", (stats) => { + console.log("Container stats:", stats); +}); + +// Listen for container events + +client.on("container:start", (container) => { + console.log("Container started:", container.Id); +}); + +client.on("container:stop", (container) => { + console.log("Container stopped:", container.Id); +}); + +// Stop monitoring when done + +await client.stopMonitoring(clientId); +``` + +### Container Statistics + +```typescript +// Get all containers with stats + +const containers = await client.getAllContainers(clientId); + +for (const container of containers) { + console.log(` + Container: ${container.Names[0]} + Image: ${container.Image} + State: ${container.State} + Status: ${container.Status} + CPU: ${container.stats?.cpu_percent}% + Memory: ${container.stats?.memory_usage}MB / ${container.stats?.memory_limit}MB + Network RX: ${container.stats?.network_rx} + Network TX: ${container.stats?.network_tx} + `); +} +``` + +### Streaming + +```mermaidjs + +sequenceDiagram + participant Client as "Client" + participant SM as "StreamManager" + participant WS as "WebSocket" + participant Subscriber as "Subscriber" + + Client->>SM: "subscribe(channel, handler)" + SM->>SM: "Register handler" + SM-->>Client: "Subscription ID" + + loop "Data Available" + WS->>SM: "Incoming data" + SM->>SM: "Route to channel" + SM->>Subscriber: "Call handler(data)" + end + + Client->>SM: "unsubscribe(channel, handler)" + SM->>SM: "Remove handler" +``` + +```typescript +import { StreamManager, STREAM_CHANNELS } from "@dockstat/docker-client"; + +const streamManager = new StreamManager(); + +// Subscribe to container stats + +const unsubscribe = streamManager.subscribe( + STREAM_CHANNELS.CONTAINER_STATS, + (stats) => { + console.log("Real-time stats:", stats); + } +); + +// Subscribe to container events + +streamManager.subscribe( + STREAM_CHANNELS.CONTAINER_EVENTS, + (event) => { + console.log("Container event:", event.Action, event.Actor.ID); + } +); + +// Subscribe to Docker daemon events + +streamManager.subscribe( + STREAM_CHANNELS.DOCKER_EVENTS, + (event) => { + console.log("Docker event:", event); + } +); + +// Unsubscribe when done + +unsubscribe(); +``` + +### Stream Channels + +| Channel | Description | +|----|----| +| `CONTAINER_STATS` | Real-time container statistics | +| `CONTAINER_EVENTS` | Container lifecycle events | +| `DOCKER_EVENTS` | All Docker daemon events | +| `IMAGE_EVENTS` | Image-related events | +| `NETWORK_EVENTS` | Network-related events | +| `VOLUME_EVENTS` | Volume-related events | + +## Worker Pool + +The worker pool architecture enables scalable multi-host management: + +```mermaidjs + +graph TB + subgraph "Worker Pool" + MANAGER["Pool Manager"] + W1["Worker 1"] + W2["Worker 2"] + W3["Worker 3"] + WN["Worker N"] + end + + subgraph "Hosts" + H1["Host A"] + H2["Host B"] + H3["Host C"] + HN["Host N"] + end + + MANAGER --> W1 + MANAGER --> W2 + MANAGER --> W3 + MANAGER --> WN + W1 --> H1 + W2 --> H2 + W3 --> H3 + WN --> HN +``` + +```typescript +// Get worker pool statistics +const status = await client.getStatus(); + +console.log(` + Total Workers: ${status.totalWorkers} + Active Workers: ${status.activeWorkers} + Total Hosts: ${status.totalHosts} + Average Hosts/Worker: ${status.averageHostsPerWorker} +`); + +for (const worker of status.workers) { + console.log(` + Worker ${worker.workerId}: + Client: ${worker.clientName} + Hosts: ${worker.hostsManaged} + Active Streams: ${worker.activeStreams} + Monitoring: ${worker.isMonitoring} + Uptime: ${worker.uptime}s + Memory: ${Math.round(worker.memoryUsage.heapUsed / 1024 / 1024)}MB + `); +} +``` + +### Worker Configuration + +```typescript +// Configure max workers via environment variable +// DOCKSTAT_MAX_WORKERS=200 + +const client = new DockerClient(db.getDB(), { + enableMonitoring: true, + maxWorkers: parseInt(process.env.DOCKSTAT_MAX_WORKERS || "200") +}); +``` + +## Host Handler + +The HostHandler manages Docker host registration and persistence: + +```typescript +import { HostHandler } from "@dockstat/docker-client"; + +const hostHandler = new HostHandler(db); + +// Get all hosts +const hosts = hostHandler.getAll(); + +// Get hosts for a specific client +const clientHosts = hostHandler.getByClient(clientId); + +// Update host configuration +hostHandler.update({ + id: 1, + clientId, + host: "192.168.1.101", + name: "Updated Host", + secure: true, + port: 2376 +}); + +// Remove host +hostHandler.remove(hostId); +``` + +### Host Health Checks + +```typescript +// Check host connectivity +const isHealthy = await client.checkHostHealth(hostId); + +// Get host with health status +const hostsWithHealth = await client.getHostsWithHealth(clientId); + +for (const host of hostsWithHealth) { + console.log(` + Host: ${host.name} + Status: ${host.healthy ? "Healthy" : "Unhealthy"} + Last Check: ${host.lastHealthCheck} + Error: ${host.healthError || "None"} + `); +} +``` + +## Container Operations + +### List Containers + +```typescript +// All containers (including stopped) +const all = await client.getAllContainers(clientId); + +// Running containers only +const running = await client.getRunningContainers(clientId); + +// Filter by label +const filtered = await client.getContainersByLabel( + clientId, + "com.docker.compose.project", + "myproject" +); +``` + +### Container Actions + +```typescript +// Start container +await client.startContainer(clientId, containerId); + +// Stop container +await client.stopContainer(clientId, containerId); + +// Restart container +await client.restartContainer(clientId, containerId); + +// Remove container +await client.removeContainer(clientId, containerId, { force: true }); + +// Get container logs +const logs = await client.getContainerLogs(clientId, containerId, { + tail: 100, + since: Date.now() - 3600000 // Last hour +}); +``` + +### Container Inspection + +```typescript +// Get detailed container info +const inspect = await client.inspectContainer(clientId, containerId); + +console.log(` + ID: ${inspect.Id} + Name: ${inspect.Name} + Image: ${inspect.Config.Image} + Created: ${inspect.Created} + State: ${inspect.State.Status} + Running: ${inspect.State.Running} + Ports: ${JSON.stringify(inspect.NetworkSettings.Ports)} + Mounts: ${inspect.Mounts.map(m => `${m.Source}:${m.Destination}`).join(", ")} +`); +``` + +## API Reference + +### DockerClient Constructor + +```typescript +new DockerClient(db: DB, options?: DockerClientOptions) +``` + +**Options:** + +| Option | Type | Default | Description | +|----|----|----|----| +| `enableMonitoring` | `boolean` | `false` | Enable real-time monitoring | +| `monitoringInterval` | `number` | `5000` | Stats collection interval (ms) | +| `maxWorkers` | `number` | `200` | Maximum worker threads | +| `maxRetries` | `number` | `3` | Connection retry attempts | +| `retryDelay` | `number` | `1000` | Delay between retries (ms) | + +### Client Management + +| Method | Description | +|----|----| +| `registerClient(name, options?)` | Register a new Docker client | +| `removeClient(clientId)` | Remove a client and all hosts | +| `getClients()` | Get all registered clients | +| `getClient(clientId)` | Get specific client | + +### Host Management + +| Method | Description | +|----|----| +| `addHost(config)` | Add a Docker host | +| `updateHost(config)` | Update host configuration | +| `removeHost(hostId)` | Remove a host | +| `getHosts(clientId)` | Get hosts for a client | +| `getHostsWithHealth(clientId)` | Get hosts with health status | +| `checkHostHealth(hostId)` | Check host connectivity | + +### Monitoring + +| Method | Description | +|----|----| +| `startMonitoring(clientId)` | Start monitoring for a client | +| `stopMonitoring(clientId)` | Stop monitoring for a client | +| `stopAllMonitoring()` | Stop all monitoring | +| `isMonitoring(clientId)` | Check if monitoring is active | + +### Container Operations + +| Method | Description | +|----|----| +| `getAllContainers(clientId)` | Get all containers | +| `getRunningContainers(clientId)` | Get running containers | +| `inspectContainer(clientId, containerId)` | Get container details | +| `startContainer(clientId, containerId)` | Start a container | +| `stopContainer(clientId, containerId)` | Stop a container | +| `restartContainer(clientId, containerId)` | Restart a container | +| `removeContainer(clientId, containerId, opts?)` | Remove a container | +| `getContainerLogs(clientId, containerId, opts?)` | Get container logs | + +### Status + +| Method | Description | +|----|----| +| `getStatus()` | Get overall client status | +| `getPoolStats()` | Get worker pool statistics | + +## Events + +The DockerClient emits various events: + +```typescript +// Container events +client.on("container:start", (container) => { }); +client.on("container:stop", (container) => { }); +client.on("container:die", (container) => { }); +client.on("container:restart", (container) => { }); +client.on("container:create", (container) => { }); +client.on("container:destroy", (container) => { }); + +// Image events +client.on("image:pull", (image) => { }); +client.on("image:delete", (image) => { }); + +// Stats events +client.on("stats", (stats) => { }); +client.on("stats:error", (error) => { }); + +// Connection events +client.on("host:connected", (host) => { }); +client.on("host:disconnected", (host) => { }); +client.on("host:error", (host, error) => { }); +``` + +## Type Definitions + +```typescript +import type { DOCKER } from "@dockstat/typings"; + +// Host configuration + +type HostConfig = DOCKER.HostConfig; +/* +{ + id: number; + clientId?: number; + host: string; + port: number; + secure: boolean; + name: string; +} +*/ + +// Container with stats + +type Container = DOCKER.Container; +/* +{ + Id: string; + Names: string[]; + Image: string; + ImageID: string; + Command: string; + Created: number; + State: string; + Status: string; + Ports: Port[]; + Labels: Record; + stats?: ContainerStats; +} +*/ + +// Container statistics + +type ContainerStats = DOCKER.ContainerStats; +/* +{ + cpu_percent: number; + memory_usage: number; + memory_limit: number; + memory_percent: number; + network_rx: number; + network_tx: number; + block_read: number; + block_write: number; +} +*/ +``` + +## Error Handling + +```typescript +import DockerClient from "@dockstat/docker-client"; +import Logger from "@dockstat/logger"; + +const log = new Logger("Docker"); + +try { + const containers = await client.getAllContainers(clientId); +} catch (error) { + if (error.code === "ENOENT") { + log.error("Docker socket not found"); + } else if (error.code === "ECONNREFUSED") { + log.error("Docker daemon not running"); + } else if (error.code === "ETIMEDOUT") { + log.error("Connection to Docker host timed out"); + } else { + log.error(`Docker error: ${error.message}`); + } +} + +// Handle monitoring errors +client.on("stats:error", (error) => { + log.error(`Monitoring error: ${error.message}`); +}); + +client.on("host:error", (host, error) => { + log.error(`Host ${host.name} error: ${error.message}`); +}); +``` + +## Integration Examples + +### With DockStat API + +```typescript +import { Elysia } from "elysia"; +import DockerClient from "@dockstat/docker-client"; +import DockStatDB from "@dockstat/db"; + +const db = new DockStatDB(); +const dockerClient = new DockerClient(db.getDB(), { + enableMonitoring: true +}); + +const app = new Elysia() + .get("/api/containers/:clientId", async ({ params }) => { + return await dockerClient.getAllContainers(parseInt(params.clientId)); + }) + .post("/api/containers/:clientId/:containerId/start", async ({ params }) => { + await dockerClient.startContainer( + parseInt(params.clientId), + params.containerId + ); + return { success: true }; + }) + .get("/api/status", async () => { + return await dockerClient.getStatus(); + }); +``` + +### With WebSocket Streaming + +```typescript +import { Elysia } from "elysia"; +import { websocket } from "@elysiajs/websocket"; +import { StreamManager, STREAM_CHANNELS } from "@dockstat/docker-client"; + +const streamManager = new StreamManager(); + +const app = new Elysia() + .use(websocket()) + .ws("/ws/stats", { + open(ws) { + streamManager.subscribe(STREAM_CHANNELS.CONTAINER_STATS, (stats) => { + ws.send(JSON.stringify(stats)); + }); + }, + close(ws) { + // Cleanup handled automatically + } + }); +``` + +## Related Packages + +* `@dockstat/db` - Database layer for persistence +* `@dockstat/sqlite-wrapper` - SQLite operations +* `@dockstat/logger` - Logging utilities +* `@dockstat/typings` - Type definitions +* `@dockstat/plugin-handler` - Plugin system integration + +## License + +Part of the DockStat project. See main repository for license information. + +## Contributing + +Issues and PRs welcome at [github.com/Its4Nik/DockStat](https://github.com/Its4Nik/DockStat) \ No newline at end of file diff --git a/apps/docs/dockstat/packages/@dockstat-logger/README.md b/apps/docs/dockstat/packages/@dockstat-logger/README.md new file mode 100644 index 00000000..0022d5d9 --- /dev/null +++ b/apps/docs/dockstat/packages/@dockstat-logger/README.md @@ -0,0 +1,523 @@ +--- +id: e913e6bd-3f7c-485f-812d-3e626b4f6b5b +title: "@dockstat/logger" +collectionId: b4a5e48f-f103-480b-9f50-8f53f515cab9 +parentDocumentId: bbcefaa2-6bd4-46e8-ae4b-a6b823593e67 +updatedAt: 2025-12-17T09:49:02.729Z +urlId: zb1s366Lti +--- + +> A lightweight, colorized logging utility for Bun with source mapping support, request tracking, and hierarchical logger spawning. + +## Overview + +`@dockstat/logger` provides a simple yet powerful logging system with: + +* **Colorized Output**: Automatic color coding by log level and request ID +* **Source Mapping**: Shows file name and line number for each log +* **Request Tracking**: Track logs across async operations with request IDs +* **Hierarchical Loggers**: Spawn child loggers with parent context +* **Environment Controls**: Fine-grained control via environment variables +* **Bun-Optimized**: Built specifically for Bun runtime + +## Installation + +```bash +bun add @dockstat/logger +``` + +## Quick Start + +```typescript +import { Logger } from "@dockstat/logger" + +const log = new Logger("MyApp") + +log.info("Application started") +log.warn("This is a warning") +log.error("An error occurred") +log.debug("Debug information") +``` + +**Output:** + +```php +12:34:56 INFO [MyApp] index.ts:5 — Application started +12:34:56 WARN [MyApp] index.ts:6 — This is a warning +12:34:56 ERROR [MyApp] index.ts:7 — An error occurred +12:34:56 DEBUG [MyApp] index.ts:8 — Debug information +``` + +## Core Features + +### Log Levels + +Four standard log levels with automatic color coding: + +* **ERROR** (red) - Critical errors +* **WARN** (yellow) - Warning messages +* **INFO** (green) - Informational messages +* **DEBUG** (blue) - Debug information + +### Request ID Tracking + +Track logs across async operations by passing a request ID: + +```typescript +const log = new Logger("API") + +function handleRequest(req: Request) { + const reqId = crypto.randomUUID() + + log.info("Request received", reqId) + await processRequest(req, reqId) + log.info("Request completed", reqId) +} + +async function processRequest(req: Request, reqId: string) { + log.debug("Processing data", reqId) + // ... +} +``` + +**Output:** + +```php +12:34:56 INFO (a1b2c3d4) [API] handler.ts:5 — Request received +12:34:56 DEBUG (a1b2c3d4) [API] handler.ts:12 — Processing data +12:34:56 INFO (a1b2c3d4) [API] handler.ts:7 — Request completed +``` + +Each request ID is automatically colorized with a consistent color for easy tracking. + +### Request Origin Tracking + +Set and track where requests originate from: + +```typescript +const log = new Logger("Router") +const reqId = "abc123" + +log.setReqFrom(reqId, "192.168.1.100") +log.info("Handling request", reqId) + +// Output: 12:34:56 INFO (abc123@192.168.1.100) [Router] — Handling request + +log.clearReqFrom(reqId) // Clean up when done +``` + +### Hierarchical Loggers + +Spawn child loggers that maintain parent context: + +```typescript +const mainLog = new Logger("App") +const dbLog = mainLog.spawn("Database") +const queryLog = dbLog.spawn("Query") + +mainLog.info("Application started") +dbLog.info("Connected to database") +queryLog.debug("Executing SELECT query") +``` + +**Output:** + +```php +12:34:56 INFO [App] — Application started +12:34:56 INFO [Database:App] — Connected to database +12:34:56 DEBUG [Query:Database:App] — Executing SELECT query +``` + +### Additional Parents + +Add extra context when spawning loggers: + +```typescript +const log = new Logger("Service") +const userLog = log.spawn("UserHandler", ["User:123"]) + +userLog.info("Processing user action") +// Output: [UserHandler:User:123:Service] — Processing user action +``` + +### Dynamic Logger Control + +Enable or disable loggers at runtime: + +```typescript +const log = new Logger("Debug") + +log.setDisabled(true) +log.debug("This won't be logged") + +log.setDisabled(false) +log.debug("This will be logged") +``` + +## Environment Variables + +### `DOCKSTAT_LOGGER_DISABLED_LOGGERS` + +Disable specific loggers by name (comma-separated): + +```bash +DOCKSTAT_LOGGER_DISABLED_LOGGERS="Database,Cache" +``` + +```typescript +const dbLog = new Logger("Database") // disabled +const apiLog = new Logger("API") // enabled + +dbLog.info("This won't show") +apiLog.info("This will show") +``` + +### `DOCKSTAT_LOGGER_ONLY_SHOW` + +Only show logs from specific loggers: + +```bash +DOCKSTAT_LOGGER_ONLY_SHOW="API,Router" +``` + +All loggers except `API` and `Router` will be disabled. + +### `DOCKSTAT_LOGGER_IGNORE_MESSAGES` + +Filter out logs containing specific text (comma-separated, case-insensitive): + +```bash +DOCKSTAT_LOGGER_IGNORE_MESSAGES="health check,ping" +``` + +```typescript +log.info("Health check passed") // ignored +log.info("User logged in") // shown +``` + +### `DOCKSTAT_LOGGER_SEPERATOR` + +Customize the separator between logger names in hierarchies: + +```bash +DOCKSTAT_LOGGER_SEPERATOR=" → " +``` + +```typescript +const parent = new Logger("Parent") +const child = parent.spawn("Child") +child.info("Message") +// Output: [Child → Parent] — Message +``` + +Default: `:` + +### `DOCKSTAT_LOGGER_FULL_FILE_PATH` + +Show full file paths instead of just filename: + +```bash +DOCKSTAT_LOGGER_FULL_FILE_PATH="true" +``` + +```typescript +log.info("Message") +// Default: index.ts:5 +// Full: /home/user/project/src/index.ts:5 +``` + +## API Reference + +### Constructor + +```typescript +new Logger(name: string, parents?: string[]) +``` + +Create a new logger instance. + +**Parameters:** + +* `name` - Logger identifier +* `parents` - Optional array of parent logger names + +### Methods + +#### `spawn(prefix: string, additionalParents?: string[]): Logger` + +Create a child logger with parent context. + +```typescript +const parent = new Logger("Parent") +const child = parent.spawn("Child", ["Context"]) +``` + +#### `error(msg: string, requestid?: string): void` + +Log an error message. + +```typescript +log.error("Database connection failed", reqId) +``` + +#### `warn(msg: string, requestid?: string): void` + +Log a warning message. + +```typescript +log.warn("Rate limit approaching", reqId) +``` + +#### `info(msg: string, requestid?: string): void` + +Log an informational message. + +```typescript +log.info("User authenticated", reqId) +``` + +#### `debug(msg: string, requestid?: string): void` + +Log a debug message. + +```typescript +log.debug("Cache hit for key: user:123", reqId) +``` + +#### `setDisabled(to: boolean): void` + +Enable or disable the logger. + +```typescript +log.setDisabled(true) // disable +log.setDisabled(false) // enable +``` + +#### `setReqFrom(reqId: string, from: string): void` + +Set the origin for a request ID. + +```typescript +log.setReqFrom("abc123", "192.168.1.100") +``` + +#### `clearReqFrom(reqId: string): void` + +Clear the origin for a request ID. + +```typescript +log.clearReqFrom("abc123") +``` + +#### `getParents(): string[]` + +Get array of parent logger names. + +```typescript +const parents = log.getParents() +``` + +#### `getParentsForLoggerChaining(): string[]` + +Get array including current logger and all parents. + +```typescript +const chain = log.getParentsForLoggerChaining() +``` + +#### `addParent(prefix: string): string[]` + +Add a single parent to the logger. + +```typescript +log.addParent("NewParent") +``` + +#### `addParents(parents: string[]): void` + +Replace all parents with a new array. + +```typescript +log.addParents(["Parent1", "Parent2"]) +``` + +## Usage Patterns + +### API Request Logging + +```typescript +import { Logger } from "@dockstat/logger" + +const log = new Logger("API") + +app.use((req, res, next) => { + const reqId = req.headers["x-request-id"] || crypto.randomUUID() + log.setReqFrom(reqId, req.ip) + + log.info(`${req.method} ${req.path}`, reqId) + + res.on("finish", () => { + log.info(`Response ${res.statusCode}`, reqId) + log.clearReqFrom(reqId) + }) + + next() +}) +``` + +### Service Architecture + +```typescript +// Create service-specific loggers + +const apiLog = new Logger("API") +const dbLog = new Logger("Database") +const cacheLog = new Logger("Cache") + +// Docker operations with hierarchical context + +const dockerLog = new Logger("Docker") +const containerLog = dockerLog.spawn("Container") +const imageLog = dockerLog.spawn("Image") + +containerLog.info("Starting container abc123") +imageLog.info("Pulling image nginx:latest") +``` + +### Plugin System + +```typescript +class PluginManager { + private log: Logger + + constructor() { + this.log = new Logger("PluginManager") + } + + loadPlugin(name: string) { + const pluginLog = this.log.spawn(name) + + pluginLog.info("Loading plugin") + // ... load plugin + pluginLog.info("Plugin loaded successfully") + } +} +``` + +### Development vs Production + +```bash +# Development - show everything + +DOCKSTAT_LOGGER_DISABLED_LOGGERS="" + +# Production - only errors and warnings + +DOCKSTAT_LOGGER_ONLY_SHOW="API,Database" +DOCKSTAT_LOGGER_IGNORE_MESSAGES="debug,trace" +``` + +## Color Coding + +The logger uses consistent color schemes: + +* **Timestamp**: Magenta +* **Log Level**: Colored by level (red/yellow/green/blue) +* **Logger Name**: Cyan +* **Parent Chain**: Yellow +* **Request ID**: Hashed to consistent color per ID +* **Request Origin**: Green +* **File Location**: Blue +* **Message**: Gray + +Request IDs are colored using a hash function, ensuring the same request ID always gets the same color across all logs. + +## Performance + +* **Minimal Overhead**: Simple formatting with no async operations +* **Conditional Logging**: Messages are not processed if logger is disabled +* **Source Maps**: Efficient stack trace parsing with Bun's source-map-support +* **No File I/O**: All output to stdout/stderr for performance + +## Integration Examples + +### With Elysia + +```typescript + +import { Elysia } from "elysia" +import { Logger } from "@dockstat/logger" + +const log = new Logger("Elysia") + +new Elysia() + .onRequest(({ request }) => { + const reqId = request.headers.get("x-request-id") + log.info(`${request.method} ${new URL(request.url).pathname}`, reqId) + }) + .onError(({ error, request }) => { + const reqId = request.headers.get("x-request-id") + log.error(`Error: ${error.message}`, reqId) + }) + .listen(3000) +``` + +### With Docker Client + +```typescript +import { Logger } from "@dockstat/logger" +import { DockerClient } from "@dockstat/docker-client" + +const log = new Logger("Docker") +const client = new DockerClient(log) + +// Logger is passed to docker client for internal logging +``` + +### With Plugin Handler + +```typescript +import { Logger } from "@dockstat/logger" +import { PluginHandler } from "@dockstat/plugin-handler" + +const baseLog = new Logger("Plugins") +const handler = new PluginHandler(db, baseLog) + +// Each plugin gets its own spawned logger +``` + +## Best Practices + + +1. **Use Hierarchical Loggers**: Spawn child loggers for different components +2. **Pass Request IDs**: Track operations across async boundaries +3. **Clean Up Request Context**: Call `clearReqFrom()` when requests complete +4. **Descriptive Logger Names**: Use clear, hierarchical naming (e.g., "API:Routes:Users") +5. **Environment Controls**: Use env vars for production log filtering +6. **Consistent Levels**: Use appropriate log levels (error for errors, info for significant events) + +## Comparison with Other Loggers + +| Feature | @dockstat/logger | winston | pino | bunyan | +|----|----|----|----|----| +| Bun-Native | ✅ | ❌ | ❌ | ❌ | +| Request Tracking | ✅ | Manual | Manual | Manual | +| Hierarchical Spawning | ✅ | ❌ | ❌ | ✅ | +| Zero Config | ✅ | ❌ | ❌ | ❌ | +| Source Maps | ✅ | ❌ | ❌ | ❌ | +| Color Coding | ✅ | Manual | Manual | ❌ | +| File Size | <5KB | \~500KB | \~50KB | \~100KB | + +## License + +Part of the DockStat project. See main repository for license information. + +## Contributing + +Issues and PRs welcome at [github.com/Its4Nik/DockStat](https://github.com/Its4Nik/DockStat) + +## Related Packages + +* `@dockstat/docker-client` - Docker client that uses this logger +* `@dockstat/plugin-handler` - Plugin system with logger integration +* `@dockstat/outline-sync` - Uses this logger for sync operations \ No newline at end of file diff --git a/apps/docs/dockstat/packages/@dockstat-outline-sync/README.md b/apps/docs/dockstat/packages/@dockstat-outline-sync/README.md deleted file mode 100644 index 8a28171b..00000000 --- a/apps/docs/dockstat/packages/@dockstat-outline-sync/README.md +++ /dev/null @@ -1,790 +0,0 @@ ---- -id: b931ba3f-2f39-4414-9c80-fb1ebbe92771 -title: "@dockstat/outline-sync" -collectionId: b4a5e48f-f103-480b-9f50-8f53f515cab9 -parentDocumentId: 75d80211-7262-4064-aaa6-2ead20e17f43 -updatedAt: 2025-08-19T18:44:19.206Z -urlId: QdSHhQ3ZXI ---- - -# Technical Documentation - -This document provides a comprehensive technical overview of the outline-sync tool, including architecture, data flow, algorithms, and implementation details. - - ---- - -## Architecture Overview - -The outline-sync tool follows a modular architecture with clear separation of concerns: - -```mermaidjs -graph LR - subgraph "CLI Layer" - A[CLI Parser] --> B[Command Router] - B --> C[Flag Processor] - end - - subgraph "Core Engine" - D[Sync Engine] --> E[Conflict Resolver] - D --> F[File Manager] - D --> G[Manifest Manager] - end - - subgraph "Data Layer" - H[Config Loader] --> I[Collection Config] - H --> J[Top Config] - K[Outline API] --> L[Document Fetcher] - K --> M[Document Updater] - end - - subgraph "Utilities" - N[Logger] --> O[Timestamp Utils] - N --> P[Content Normalizer] - N --> Q[Path Resolver] - end - - B --> D - D --> H - D --> K - D --> N - - style A fill:#00537C - style D fill:#9D009F - style H fill:#207E00 - style N fill:#680900 -``` - - ---- - -## Core Components - -### 1. CLI Entry Point (`bin/cli.ts`) - -The CLI acts as the main entry point and handles: - -```mermaidjs -flowchart TD - A[Process Arguments] --> B{Flag Parsing} - B --> C[API Key Setup] - B --> D[Collection Resolution] - C --> E[Dynamic Module Import] - D --> E - E --> F[Command Execution] - - subgraph "Commands" - G[setup] --> H[Interactive Collection Setup] - I[init] --> J[Bootstrap Collection] - K[sync/pull/push] --> L[Run Sync Engine] - M[list-collections] --> N[API Collection List] - end - - F --> G - F --> I - F --> K - F --> M -``` - -**Key Features:** - -* Repeatable `--collection` flag support -* Early API key injection for module imports -* Comprehensive error handling -* Debug logging control - -### 2. Configuration System (`lib/config.ts`) - -Manages the hierarchical configuration structure: - -```mermaidjs -graph LR - subgraph "Configuration Hierarchy" - A[Environment Variables] --> B[CLI Flags] - B --> C[outline-sync.json] - C --> D[collection.config.json] - D --> E[collection.pages.json] - end - - subgraph "File Structure" - F[.config/] --> G[outline-sync.json] - F --> H[collection-id.config.json] - F --> I[collection-id.pages.json] - end - - subgraph "Config Types" - J[TopConfig] --> K[Collections Array] - L[CollectionConfig] --> M[Mappings Rules] - N[Manifest] --> O[Page Tree] - end -``` - -**Configuration Resolution Order:** - - -1. CLI flags (highest priority) -2. Environment variables -3. Configuration files -4. Defaults (lowest priority) - -### 3. Sync Engine (`lib/syncEngine.ts`) - -The core synchronization logic: - -```mermaidjs -flowchart TD - A[Load Configuration] --> B[Load Page Manifest] - B --> C[Apply Path Mappings] - C --> D[Normalize File Paths] - D --> E[Process Each Page] - - subgraph "Page Processing" - F[Check Local File] --> G[Fetch Remote Document] - G --> H[Compare Timestamps] - H --> I[Compare Content] - I --> J{Sync Decision} - - J -->|Pull| K[Remote → Local] - J -->|Push| L[Local → Remote] - J -->|Skip| M[No Changes] - end - - E --> F - K --> N[Process Children] - L --> N - M --> N - N --> O[Update Manifest] -``` - - ---- - -## Data Flow - -### Complete Synchronization Flow - -```mermaidjs -sequenceDiagram - participant CLI as CLI Interface - participant Config as Config Manager - participant Sync as Sync Engine - participant API as Outline API - participant FS as File System - participant Git as Git Repository - - CLI->>Config: Load configurations - Config->>Config: Resolve collection settings - Config-->>CLI: Configuration data - - CLI->>Sync: Initialize sync process - Sync->>Config: Load page manifest - Sync->>Sync: Apply path mappings - - loop For Each Document - Sync->>FS: Check local file exists - Sync->>Git: Get commit timestamp - Git-->>Sync: Timestamp or null - - Sync->>API: Fetch document info - API-->>Sync: Document data + updatedAt - - Sync->>Sync: Compare timestamps & content - - alt Content differs & Remote newer - Sync->>API: Fetch full document - API-->>Sync: Document content - Sync->>FS: Write to local file - FS-->>Sync: Success - else Content differs & Local newer - Sync->>FS: Read local content - FS-->>Sync: File content - Sync->>API: Update remote document - API-->>Sync: Success - else No changes needed - Sync->>Sync: Skip document - end - - Sync->>Sync: Process child documents - end - - Sync->>Config: Update manifest with new IDs - Config->>FS: Persist manifest - Sync-->>CLI: Sync complete -``` - -### Configuration Loading Flow - -```mermaidjs -flowchart TD - A[Start] --> B{Top config exists?} - B -->|No| C[Use defaults] - B -->|Yes| D[Load top config] - D --> E[Parse collections] - E --> F{Collection config exists?} - F -->|No| G[Create default collection config] - F -->|Yes| H[Load collection config] - H --> I[Load pages manifest] - G --> I - C --> I - I --> J[Apply mappings] - J --> K[Ready for sync] -``` - - ---- - -## Sync Algorithm - -### Conflict Resolution Algorithm - -The tool uses a sophisticated conflict resolution strategy: - -```mermaidjs -flowchart TD - A[Compare Documents] --> B[Normalize Content] - B --> C{Content Equal?} - C -->|Yes| D[Skip - No Changes] - C -->|No| E[Get Timestamps] - - E --> F[Get Git Timestamp] - F --> G{Git timestamp available?} - G -->|Yes| H[Use Git commit time] - G -->|No| I[Use filesystem mtime] - - H --> J[Compare with remote updatedAt] - I --> J - - J --> K{Remote newer by >500ms?} - K -->|Yes| L[Pull: Remote → Local] - K -->|No| M{Local newer by >500ms?} - M -->|Yes| N[Push: Local → Remote] - M -->|No| O[Skip - Equal timestamps] - - L --> P[Backup existing file] - N --> Q[Update remote document] - P --> R[Write new content] - Q --> S[Log success] - R --> S - O --> S - D --> S - S --> T[End] -``` - -### Content Normalization - -```typescript -function normalizeContentIgnoreWhitespace(content: string): string { - return content.replace(/\s+/g, ""); -} -``` - -This approach: - -* Removes ALL whitespace characters (spaces, tabs, newlines) -* Enables formatting-agnostic comparison -* Prevents unnecessary syncs due to editor differences - -### Timestamp Comparison Logic - -```mermaidjs -graph TB - A[Local File] --> B{Git tracked?} - B -->|Yes| C[git log -1 --format=%ct] - B -->|No| D["fs.stat().mtimeMs"] - C --> E[Git Timestamp] - D --> F[FS Timestamp] - E --> G[Compare with Remote] - F --> G - G --> H{Difference > 500ms?} - H -->|Yes| I[Sync Required] - H -->|No| J[Skip Sync] -``` - -**Rationale for 500ms threshold:** - -* Accounts for minor timing differences -* Prevents unnecessary syncs for simultaneous changes -* Balances precision with practical usage - - ---- - -## Configuration System - -### Configuration Hierarchy - -```mermaidjs -graph TB - subgraph "Global Level" - A[outline-sync.json] --> B[Collections List] - B --> C[Default Paths] - end - - subgraph "Collection Level" - D[collection-id.config.json] --> E[Mapping Rules] - E --> F[Save Directory] - end - - subgraph "Runtime Level" - G[collection-id.pages.json] --> H[Document Tree] - H --> I[File Paths] - end - - A --> D - D --> G - - style A fill:#0B6C00 - style D fill:#02007F - style G fill:#005F5A -``` - -### Mapping Resolution Algorithm - -```mermaidjs -flowchart TD - A[Process Document] --> B{ID mapping exists?} - B -->|Yes| C[Apply ID mapping] - B -->|No| D{Title mapping exists?} - D -->|Yes| E[Apply title mapping] - D -->|No| F[Use inherited path] - - C --> G{Path is directory?} - E --> G - F --> H[Generate slug-based path] - - G -->|Yes| I[Create README.md in directory] - G -->|No| J[Use exact file path] - H --> K[parent-dir/slug/README.md] - - I --> L[Resolve final path] - J --> L - K --> L - L --> M[Process children with parent context] -``` - -### Path Resolution Examples - -```typescript -// Directory mapping -{ - "match": { "title": "API Guide" }, - "path": "guides/api/" -} -// Result: guides/api/README.md - -// File mapping -{ - "match": { "id": "doc-123" }, - "path": "reference/authentication.md" -} -// Result: reference/authentication.md - -// Inherited path (no mapping) -// Parent: docs/product/README.md -// Child: "Installation" → docs/product/installation/README.md -``` - - ---- - -## File Organization Strategy - -### Folder-Based Architecture - -The tool employs a folder-based file organization strategy: - -```mermaidjs -graph LR - subgraph "Outline Structure" - A[Product Docs] --> B[Getting Started] - A --> C[API Reference] - B --> D[Installation] - B --> E[Configuration] - C --> F[Authentication] - end - - subgraph "File System Result" - G[docs/product-docs/README.md] --> H[docs/product-docs/getting-started/README.md] - G --> I[docs/product-docs/api-reference/README.md] - H --> J[docs/product-docs/getting-started/installation/README.md] - H --> K[docs/product-docs/getting-started/configuration/README.md] - I --> L[docs/product-docs/api-reference/authentication/README.md] - end - - A -.-> G - B -.-> H - C -.-> I - D -.-> J - E -.-> K - F -.-> L -``` - -**Benefits:** - -* Clean URLs when served by static site generators -* Natural hierarchy representation -* SEO-friendly structure -* Easy navigation in file browsers - -### Slug Generation Algorithm - -```typescript -function slugifyTitle(title: string): string { - return title - .toString() - .normalize("NFKD") // Decompose Unicode - .replace(/\p{M}/gu, "") // Remove diacritics - .toLowerCase() // Convert to lowercase - .replace(/[^a-z0-9]+/g, "-") // Replace non-alphanumeric with hyphens - .replace(/(^-|-$)+/g, "") // Remove leading/trailing hyphens - .slice(0, 120); // Limit length -} -``` - - ---- - -## API Integration - -### Outline API Client Architecture - -```mermaidjs -sequenceDiagram - participant App as Application - participant Client as API Client - participant Retry as Retry Logic - participant Outline as Outline API - - App->>Client: Request document list - Client->>Retry: Execute with backoff - - loop "Retry Attempts (max 3)" - Retry->>Outline: POST /api/documents.list - - alt "Success (200)" - Outline-->>Retry: Document data - Retry-->>Client: Success response - else "Rate Limited (429)" - Outline-->>Retry: Rate limit error - Retry->>Retry: Exponential backoff - else Other Error - Outline-->>Retry: Error response - Retry->>Retry: Linear backoff - end - end - - Client-->>App: Final result or error -``` - -### API Request Flow - -```mermaidjs -flowchart TD - A[API Request] --> B[Add Authorization Header] - B --> C[JSON Serialize Body] - C --> D[Send POST Request] - D --> E{Response Status} - - E -->|200-299| F[Parse JSON Response] - E -->|429| G[Rate Limit Backoff] - E -->|4xx/5xx| H[Error Handling] - - G --> I[Wait: attempt * 1000ms] - I --> J{Retry < 3?} - J -->|Yes| D - J -->|No| K[Throw Error] - - H --> L{Retry < 3?} - L -->|Yes| M[Wait: attempt * 500ms] - L -->|No| K - M --> D - - F --> N[Return Data] - K --> O[Propagate Error] -``` - -### Pagination Handling - -```typescript - -async function listDocumentsInCollection(collectionId: string): Promise { - const out: any[] = []; - let offset = 0; - const limit = 100; - - while (true) { - const json = await outlineRequest("documents.list", { - collectionId, - offset, - limit, - }); - - const data = json.data || []; - for (const d of data) out.push(d); - - if (data.length < limit) break; // No more pages - offset += data.length; - } - - return out; -} -``` - - ---- - -## Error Handling & Recovery - -### Error Classification - -```mermaidjs -graph LR - A[Error Occurs] --> B{Error Type} - - B -->|Network| C[Connection Issues] - B -->|API| D[Outline API Errors] - B -->|File System| E[Local File Errors] - B -->|Configuration| F[Config Errors] - - C --> G[Retry with Backoff] - D --> H{Status Code} - E --> I[Permission/Path Checks] - F --> J[Validation & Defaults] - - H -->|401/403| K[Authentication Error] - H -->|429| L[Rate Limit Handling] - H -->|500+| M[Server Error Retry] - H -->|Other| N[Client Error] - - G --> O[Success or Fail] - K --> P[Check API Key] - L --> Q[Exponential Backoff] - M --> G - N --> R[User Action Required] - I --> S[Fix Permissions] - J --> T[Use Safe Defaults] -``` - -### Backup and Recovery - -```mermaidjs -flowchart TD - A[File Write Operation] --> B{File Exists?} - B -->|Yes| C[Create Backup] - B -->|No| D[Ensure Directory] - - C --> E[Copy to .outline-sync.bak.timestamp] - E --> F[Write New Content] - D --> F - - F --> G{Write Successful?} - G -->|Yes| H[Log Success] - G -->|No| I[Restore from Backup] - - I --> J[Copy Backup to Original] - J --> K[Log Recovery] - K --> L[Throw Error] - - H --> M[Continue Processing] -``` - -### Safe File Operations - -```typescript -async function safeWriteFile( - filePath: string, - content: string, - dryRun = false -) { - // 1. Create backup if file exists - if (existsSync(filePath)) { - const backup = `${filePath}.outline-sync.bak.${Date.now()}`; - if (!dryRun) { - await fs.copyFile(filePath, backup); - logger.info(`Backed up existing file to ${backup}`); - } - } - - // 2. Ensure directory structure - if (!dryRun) { - await fs.mkdir(path.dirname(filePath), { recursive: true }); - } - - // 3. Write new content - if (!dryRun) { - await fs.writeFile(filePath, content, "utf8"); - logger.info(`Wrote file ${filePath} (${content.length} bytes)`); - } -} -``` - - ---- - -## Performance Considerations - -### Optimization Strategies - -```mermaidjs -graph LR - subgraph "API Optimization" - direction LR - A[Request Batching] --> B[Pagination Efficiency] - B --> C[Rate Limit Respect] - C --> D[Connection Pooling] - end - - subgraph "File System Optimization" - direction LR - E[Bulk Directory Creation] --> F[Parallel File Operations] - F --> G[Efficient Timestamp Queries] - end - - subgraph "Content Optimization" - direction LR - H[Whitespace Normalization] --> I[Content Hashing] - I --> J[Skip Unchanged Files] - end - - subgraph "Memory Optimization" - direction LR - K[Streaming Large Files] --> L[Incremental Processing] - L --> M[Garbage Collection Hints] - end -``` - -### Concurrency Model - -```typescript -// Sequential processing to respect API rate limits - -for (const collection of collections) { - await processCollection(collection); -} - -// Within collection: sequential to maintain parent-child relationships - -async function processPages(pages: PageEntry[], parentId: string | null) { - for (const page of pages) { - await syncPage(page, parentId); - if (page.children?.length) { - await processPages(page.children, page.id); - } - } -} -``` - -**Rationale:** - -* Sequential processing respects API rate limits -* Maintains document hierarchy integrity -* Predictable resource usage -* Easier error handling and recovery - -### Caching Strategy - -```mermaidjs -graph LR - A[Request] --> B{Cache Hit?} - B -->|Yes| C[Return Cached] - B -->|No| D[Fetch from API] - D --> E[Store in Cache] - E --> F[Return Fresh Data] - - subgraph "Cache Invalidation" - direction LR - G[Time-based TTL] --> H[Content-based Hashing] - H --> I[Manual Cache Clear] - end -``` - -**Current Implementation:** - -* No persistent caching (stateless design) -* In-memory caching for single run -* Git timestamp caching for performance - -**Future Enhancements:** - -* Persistent cache with TTL -* Content-based cache invalidation -* Collection-level cache management - - ---- - -## Security Considerations - -### API Key Management - -```mermaidjs -flowchart TB - A[API Key Sources] --> B{CLI Flag Provided?} - B -->|Yes| C[Use CLI Flag] - B -->|No| D{Environment Variable Set?} - D -->|Yes| E[Use Environment Variable] - D -->|No| F[Error: No API Key] - - C --> G[Set Environment Variable] - G --> H[Continue Execution] - E --> H - F --> I[Exit with Error] - - subgraph "Security Notes" - J[CLI flags visible in process list] - K[Environment variables safer] - L[Consider --api-key-file option] - end -``` - -### File System Security - -```typescript -// Path validation to prevent directory traversal - -function validatePath(filePath: string): boolean { - const resolved = path.resolve(filePath); - const cwd = process.cwd(); - return resolved.startsWith(cwd); -} - -// Safe file operations with permission checks - -async function ensureWritePermissions(dirPath: string): Promise { - try { - await fs.access(dirPath, fs.constants.W_OK); - } catch { - throw new Error(`No write permission for ${dirPath}`); - } -} -``` - -### Content Sanitization - -```typescript -// Sanitize document titles for safe file names - -function sanitizeFileName(title: string): string { - return title - .replace(/[<>:"/\\|?*]/g, '-') // Replace invalid filename chars - .replace(/\.\./g, '-') // Prevent parent directory access - .substring(0, 255); // Limit filename length -} -``` - - ---- - -## Conclusion - -The outline-sync tool provides a robust, extensible foundation for synchronizing Outline documentation with local markdown files. Its modular architecture, comprehensive error handling, and intelligent conflict resolution make it suitable for both individual use and large-scale documentation workflows. - -Key strengths: - -* **Reliability**: Comprehensive error handling and recovery mechanisms -* **Flexibility**: Configurable mapping system and multiple sync modes -* **Safety**: Backup system and dry-run capabilities -* **Performance**: Efficient API usage and content comparison algorithms -* **Maintainability**: Clean separation of concerns and modular design - -The tool's design principles of safety, configurability, and extensibility ensure it can evolve with changing requirements while maintaining backward compatibility and user trust. \ No newline at end of file diff --git a/apps/docs/dockstat/packages/@dockstat-plugin-handler/README.md b/apps/docs/dockstat/packages/@dockstat-plugin-handler/README.md new file mode 100644 index 00000000..b4b11830 --- /dev/null +++ b/apps/docs/dockstat/packages/@dockstat-plugin-handler/README.md @@ -0,0 +1,874 @@ +--- +id: eadaaa93-6c65-4207-a4ac-9b19afc8f2a5 +title: "@dockstat/plugin-handler" +collectionId: b4a5e48f-f103-480b-9f50-8f53f515cab9 +parentDocumentId: bbcefaa2-6bd4-46e8-ae4b-a6b823593e67 +updatedAt: 2025-12-16T19:04:24.453Z +urlId: pXHTTOpluB +--- + +> A dynamic plugin system for DockStat that enables runtime loading, execution, and management of plugins stored in a SQLite database. + +## Overview + +`@dockstat/plugin-handler` provides a complete plugin architecture with: + +* **Database-Backed Storage**: Plugins stored as code in SQLite +* **Dynamic Loading**: Runtime plugin loading from temporary files +* **Route Management**: Plugin-defined API routes with action chaining +* **Event Hooks**: Plugin lifecycle and Docker event integration +* **Table Creation**: Automatic database table creation for plugin data +* **Server-Side Hooks**: Plugin access to database and logger utilities +* **Manifest Support**: Install from JSON/YAML manifests + +## Installation + +```bash +bun add @dockstat/plugin-handler +``` + +## Quick Start + +```typescript +import DB from "@dockstat/sqlite-wrapper" +import PluginHandler from "@dockstat/plugin-handler" + +const db = new DB("./app.db") +const pluginHandler = new PluginHandler(db) + +// Load all plugins from database + +await pluginHandler.loadAllPlugins() + +// Get plugin status + +const status = pluginHandler.getStatus() +console.log(status) +``` + +## Architecture + +### Data Flow + +```mermaidjs +graph LR + A["Plugin Source
(Manifest URL)"] + A --> B["installFrom
ManifestLink()"] + B --> C["(SQLite Database
plugins table)"] + C --> D["loadPlugin()
Write to /tmp"] + D --> E[Dynamic Import
Cache in Map] + E --> F[Plugin Routes &
Event Hooks] +``` + +## Core Concepts + +### Plugin Structure + +A DockStat plugin is a JavaScript module with specific exports: + +```typescript +export default { + id: 1, // Set automatically by handler + name: "example-plugin", + version: "1.0.0", + + config: { + // Database table for plugin data + table: { + name: "plugin_data", + columns: { + id: column.id(), + data: column.json(), + }, + jsonColumns: ["data"] + }, + + // API routes exposed by plugin + apiRoutes: { + "/status": { + method: "GET", + actions: ["getStatus"] + }, + "/save": { + method: "POST", + actions: ["validateData", "saveData"] + } + }, + + // Actions that routes can call + actions: { + getStatus: ({ table, logger }) => { + return table.select(["*"]).all() + }, + validateData: ({ body, logger }) => { + logger.debug("Validating data") + return { valid: true, data: body } + }, + saveData: ({ table, previousAction, logger }) => { + if (previousAction.valid) { + table.insert(previousAction.data) + return { success: true } + } + return { success: false } + } + } + }, + + // Event hooks for Docker events + events: { + onContainerStart: async ({ container, logger }) => { + logger.info(`Container started: ${container.id}`) + }, + onContainerStop: async ({ container, logger }) => { + logger.info(`Container stopped: ${container.id}`) + } + } +} +``` + +### Plugin Database Schema + +Plugins are stored in the `plugins` table: + +```typescript +interface DBPluginSchema { + id: number + repoType: "github" | "gitlab" | "local" | "default" + name: string + description?: string + tags?: string[] + version: string + repository: string + manifest: string + author: object + plugin: string // The actual plugin code +} +``` + +## API Reference + +### Constructor + +```typescript +new PluginHandler(db: DB, loggerParents?: string[]) +``` + +Create a new plugin handler instance. + +**Parameters:** + +* `db` - SQLite database instance from `@dockstat/sqlite-wrapper` +* `loggerParents` - Optional parent logger names for hierarchical logging + +**Example:** + +```typescript +const db = new DB("./dockstat.db") +const handler = new PluginHandler(db, ["API"]) +``` + +### Plugin Management + +#### `savePlugin(plugin: DBPluginSchema, update?: boolean)` + +Save or update a plugin in the database. + +```typescript +const result = handler.savePlugin({ + name: "my-plugin", + version: "1.0.0", + repository: "https://github.com/user/plugin", + manifest: "https://github.com/user/plugin/manifest.json", + author: { name: "Developer", email: "dev@example.com" }, + tags: ["monitoring", "alerts"], + repoType: "github", + plugin: pluginCode +}, false) + +// result: { success: true, id: 5, message: "Plugin saved successfully" } +``` + +**Update Mode:** + +```typescript +handler.savePlugin(updatedPlugin, true) +// Unloads, deletes, and re-saves the plugin +``` + +#### `deletePlugin(id: number)` + +Delete a plugin from the database. + +```typescript +handler.deletePlugin(5) +// Returns: { success: true, message: "Deleted Plugin" } +``` + +#### `getAll()` + +Get all plugins from database (without plugin code). + +```typescript +const plugins = handler.getAll() +// Returns array of plugin metadata +``` + +#### `getStatus()` + +Get comprehensive plugin system status. + +```typescript +const status = handler.getStatus() +``` + +**Returns:** + +```typescript +{ + installed_plugins: { + count: 3, + data: [/* plugin metadata */] + }, + repos: ["https://github.com/user/plugin1", "local"], + loaded_plugins: [/* currently loaded plugin data */] +} +``` + +### Loading & Unloading + +#### `loadAllPlugins()` + +Load all plugins from database into memory. + +```typescript +await handler.loadAllPlugins() +``` + +**Process:** + + +1. Fetch all plugins from database +2. Filter out already-loaded plugins +3. Write each plugin to temporary file +4. Dynamically import the module +5. Create plugin database tables if defined +6. Cache in memory map + +#### `loadPlugins(ids: number[])` + +Load specific plugins by ID. + +```typescript +const result = await handler.loadPlugins([1, 2, 3]) +``` + +**Returns:** + +```typescript +{ + successes: [1, 3], + errors: [ + { pluginId: 2, error: "Could not load 2 - Module not found" } + ] +} +``` + +#### `loadPlugin(id: number)` + +Load a single plugin. + +```typescript +const plugin = await handler.loadPlugin(5) +``` + +#### `unloadPlugin(id: number)` + +Unload a specific plugin from memory. + +```typescript + +handler.unloadPlugin(5) +``` + +#### `unloadAllPlugins()` + +Unload all plugins from memory. + +```typescript +handler.unloadAllPlugins() +``` + +#### `getLoadedPlugins()` + +Get array of loaded plugin IDs. + +```typescript +const loaded = handler.getLoadedPlugins() +// Returns: [1, 3, 5] +``` + +### Route Handling + +#### `handleRoute(id: number, path: string, request: Request)` + +Execute a plugin's API route. + +```typescript +const response = await handler.handleRoute( + 5, + "/status", + request +) +``` + +**Process:** + + +1. Find loaded plugin by ID +2. Lookup route in plugin's `apiRoutes` +3. Execute action chain sequentially +4. Pass `previousAction` result to next action +5. Return final action result + +**Example Plugin Route:** + +```typescript +config: { + apiRoutes: { + "/process": { + method: "POST", + actions: ["validate", "transform", "save"] + } + }, + actions: { + validate: ({ body }) => ({ valid: true, data: body }), + transform: ({ previousAction }) => ({ + ...previousAction, + data: previousAction.data.toUpperCase() + }), + save: ({ table, previousAction }) => { + table.insert(previousAction.data) + return { success: true } + } + } +} +``` + +#### `getAllPluginRoutes()` + +Get all available routes from loaded plugins. + +```typescript +const routes = handler.getAllPluginRoutes() +``` + +**Returns:** + +```typescript +[ + { + plugin: "example-plugin", + routes: ["/status", "/data", "/settings"] + }, + { + plugin: "another-plugin", + routes: ["/info"] + } +] +``` + +### Event System + +#### `getHookHandlers()` + +Get all event hooks from loaded plugins. + +```typescript +const hooks = handler.getHookHandlers() +``` + +**Returns:** `Map>` + +Map of plugin IDs to their event handlers. + +**Event Types:** + +```typescript +interface EVENTS { + onContainerStart?: (context: EventContext) => Promise + onContainerStop?: (context: EventContext) => Promise + onContainerRestart?: (context: EventContext) => Promise + onImagePull?: (context: EventContext) => Promise + onImageRemove?: (context: EventContext) => Promise + // ... more events +} +``` + +### Installation + +#### `installFromManifestLink(url: string)` + +Install a plugin from a manifest URL. + +```typescript +await handler.installFromManifestLink( + "https://example.com/plugin/manifest.json" +) +``` + +**Supported Formats:** + +* JSON (`.json`) +* YAML (`.yml`, `.yaml`) + +**Manifest Example:** + +```json +{ + "name": "notification-plugin", + "version": "1.0.0", + "description": "Send notifications on events", + "repository": "https://github.com/user/notification-plugin", + "manifest": "https://github.com/user/notification-plugin/manifest.json", + "author": { + "name": "Developer", + "email": "dev@example.com" + }, + "tags": ["notifications", "alerts"], + "repoType": "github", + "plugin": "export default { ... }" +} +``` + +### Server Hooks + +#### `getServerHooks(id: number)` + +Get database table and logger for a plugin (used internally). + +```typescript +const hooks = handler.getServerHooks(5) +// Returns: { table: QueryBuilder, logger: Logger } +``` + +### Database Access + +#### `getTable()` + +Get the underlying `plugins` table QueryBuilder. + +```typescript +const table = handler.getTable() +const count = table.count() +``` + +## Usage Patterns + +### Basic Plugin System Setup + +```typescript +import { Elysia } from "elysia" +import DB from "@dockstat/sqlite-wrapper" +import PluginHandler from "@dockstat/plugin-handler" + +const db = new DB("./dockstat.db") +const plugins = new PluginHandler(db) + +await plugins.loadAllPlugins() + +new Elysia() + .get("/plugins", () => plugins.getStatus()) + .get("/plugins/routes", () => plugins.getAllPluginRoutes()) + .all("/plugins/:id/routes/*", async ({ params, request }) => { + const path = new URL(request.url).pathname + .replace(`/plugins/${params.id}/routes`, "") + + return plugins.handleRoute( + Number(params.id), + path, + request + ) + }) + .listen(3000) +``` + +### Installing Plugins from GitHub + +```typescript +// Install from GitHub manifest + +await plugins.installFromManifestLink( + "https://raw.githubusercontent.com/user/plugin/main/manifest.json" +) + +// Load the new plugin + +const status = plugins.getStatus() +const newPlugin = status.installed_plugins.data.at(-1) +await plugins.loadPlugin(newPlugin.id) +``` + +### Plugin Lifecycle Management + +```typescript + +class PluginManager { + private handler: PluginHandler + + async installAndActivate(manifestUrl: string) { + // Install from manifest + const result = await this.handler.installFromManifestLink(manifestUrl) + + if (!result.success) { + throw new Error(result.message) + } + + // Load into memory + await this.handler.loadPlugin(result.id) + + // Verify loaded + const loaded = this.handler.getLoadedPlugins() + return loaded.includes(result.id) + } + + async deactivate(id: number) { + this.handler.unloadPlugin(id) + } + + async uninstall(id: number) { + this.handler.unloadPlugin(id) + this.handler.deletePlugin(id) + } + + async update(id: number, newManifestUrl: string) { + // Fetch new version + const res = await fetch(newManifestUrl) + const manifest = await res.json() + + // Update in database + await this.handler.savePlugin( + { ...manifest, id }, + true // update mode + ) + } +} +``` + +### Docker Event Integration + +```typescript +import { DockerEventEmitter } from "@dockstat/docker-client" + +const eventEmitter = new DockerEventEmitter() +const plugins = new PluginHandler(db) + +await plugins.loadAllPlugins() + +eventEmitter.on("container:start", async (container) => { + const hooks = plugins.getHookHandlers() + + for (const [pluginId, events] of hooks) { + if (events.onContainerStart) { + const serverHooks = plugins.getServerHooks(pluginId) + + await events.onContainerStart({ + container, + logger: serverHooks.logger, + table: serverHooks.table + }) + } + } +}) +``` + +### Multi-Action Routes + +```typescript +// Plugin definition + +export default { + name: "data-processor", + config: { + apiRoutes: { + "/process": { + method: "POST", + actions: [ + "parseInput", + "validateSchema", + "transformData", + "saveToDatabase", + "sendNotification" + ] + } + }, + actions: { + parseInput: ({ body, logger }) => { + logger.debug("Parsing input") + try { + return { success: true, data: JSON.parse(body) } + } catch { + return { success: false, error: "Invalid JSON" } + } + }, + + validateSchema: ({ previousAction, logger }) => { + if (!previousAction.success) return previousAction + + const valid = /* schema validation */ + return { ...previousAction, valid } + }, + + transformData: ({ previousAction }) => { + if (!previousAction.valid) return previousAction + + const transformed = /* transform logic */ + return { ...previousAction, data: transformed } + }, + + saveToDatabase: ({ table, previousAction }) => { + if (!previousAction.valid) return previousAction + + const result = table.insert(previousAction.data) + return { ...previousAction, insertId: result.insertId } + }, + + sendNotification: ({ previousAction, logger }) => { + if (!previousAction.valid) return previousAction + + logger.info(`Data saved with ID: ${previousAction.insertId}`) + return { success: true, id: previousAction.insertId } + } + } + } +} +``` + +## Plugin Development Guide + +### Creating a Plugin + + +1. **Define Plugin Structure** + +```typescript + +import { column } from "@dockstat/sqlite-wrapper" + +export default { + name: "my-plugin", + version: "1.0.0", + + config: { + table: { + name: "my_plugin_data", + columns: { + id: column.id(), + value: column.text(), + metadata: column.json() + }, + jsonColumns: ["metadata"] + }, + + apiRoutes: { + "/data": { + method: "GET", + actions: ["getData"] + } + }, + + actions: { + getData: ({ table, logger }) => { + logger.debug("Fetching data") + return table.select(["*"]).all() + } + } + }, + + events: { + onContainerStart: async ({ container, logger }) => { + logger.info(`Container ${container.id} started`) + } + } +} +``` + + +2. **Create Manifest** + +```json +{ + "name": "my-plugin", + "version": "1.0.0", + "description": "My awesome plugin", + "repository": "https://github.com/user/my-plugin", + "manifest": "https://github.com/user/my-plugin/manifest.json", + "author": { + "name": "Your Name", + "email": "you@example.com" + }, + "tags": ["monitoring"], + "repoType": "github", + "plugin": "export default { /* plugin code */ }" +} +``` + + +3. **Test Locally** + +```typescript +const plugin = { /* your plugin object */ } +const handler = new PluginHandler(db) + +handler.savePlugin({ + ...plugin, + repoType: "local", + manifest: "local", + repository: "local" +}) +``` + +### Action Context + +Actions receive a context object: + +```typescript +interface ActionContext { + logger: Logger // Logger with plugin name + table: QueryBuilder | null // Plugin's database table + body?: unknown // Request body (POST/PUT only) + previousAction?: unknown // Result from previous action +} +``` + +### Event Context + +Event handlers receive: + +```typescript +interface EventContext { + container?: ContainerInfo // Docker container data + image?: ImageInfo // Docker image data + logger: Logger // Plugin logger + table?: QueryBuilder // Plugin table +} +``` + +## Security Considerations + + +1. **Code Execution**: Plugins execute arbitrary code - only install trusted plugins +2. **Database Access**: Plugins have full access to their tables +3. **Temporary Files**: Plugin code is written to `/tmp` - ensure proper permissions +4. **Manifest Sources**: Validate manifest URLs before installation +5. **Action Chain**: Each action has access to previous results + +## Performance + +* **Lazy Loading**: Plugins only loaded when needed +* **Memory Caching**: Loaded plugins cached in Map +* **Temporary Files**: Cleaned up after import +* **Database Tables**: Created once, reused across restarts +* **Action Chaining**: Synchronous by default for performance + +## Troubleshooting + +### Plugin Won't Load + +```typescript +// Check if plugin exists in database + +const plugins = handler.getAll() +console.log(plugins) + +// Check for errors + +try { + await handler.loadPlugin(5) +} catch (err) { + console.error("Load error:", err) +} +``` + +### Route Not Found + +```typescript +// List all available routes + +const routes = handler.getAllPluginRoutes() +console.log(routes) + +// Verify plugin is loaded + +const loaded = handler.getLoadedPlugins() +console.log("Loaded:", loaded) +``` + +### Database Table Issues + +```typescript +// Check if plugin table was created + +const db = handler.getTable().getDb() +const tables = db.query("SELECT name FROM sqlite_master WHERE type='table'").all() +console.log("Tables:", tables) +``` + +## Integration Examples + +### With Elysia API + +```typescript +import { Elysia } from "elysia" + +const app = new Elysia() + +// Plugin management endpoints + +app.group("/api/v2/plugins", (app) => + app + .get("/", () => plugins.getStatus()) + .get("/:id", ({ params }) => { + const all = plugins.getAll() + return all.find(p => p.id === Number(params.id)) + }) + .post("/install", async ({ body }) => { + return plugins.installFromManifestLink(body.url) + }) + .delete("/:id", ({ params }) => { + plugins.unloadPlugin(Number(params.id)) + return plugins.deletePlugin(Number(params.id)) + }) + .post("/:id/activate", async ({ params }) => { + await plugins.loadPlugin(Number(params.id)) + return { success: true } + }) + .post("/:id/deactivate", ({ params }) => { + plugins.unloadPlugin(Number(params.id)) + return { success: true } + }) +) + +// Plugin route proxy + +app.all("/api/v2/plugins/:id/routes/*", async ({ params, request }) => { + const url = new URL(request.url) + const pluginPath = url.pathname.replace(`/api/v2/plugins/${params.id}/routes`, "") + + return plugins.handleRoute(Number(params.id), pluginPath, request) +}) +``` + +## Related Packages + +* `@dockstat/sqlite-wrapper` - Database layer for plugin storage +* `@dockstat/logger` - Logging system provided to plugins +* `@dockstat/typings` - TypeScript types for plugin interfaces +* `@dockstat/docker-client` - Docker events that plugins can hook into + +## License + +Part of the DockStat project. See main repository for license information. + +## Contributing + +Issues and PRs welcome at [github.com/Its4Nik/DockStat](https://github.com/Its4Nik/DockStat) \ No newline at end of file diff --git a/apps/docs/dockstat/packages/@dockstat-sqlite-wrapper/README.md b/apps/docs/dockstat/packages/@dockstat-sqlite-wrapper/README.md index 7ad0b795..c3028290 100644 --- a/apps/docs/dockstat/packages/@dockstat-sqlite-wrapper/README.md +++ b/apps/docs/dockstat/packages/@dockstat-sqlite-wrapper/README.md @@ -1,66 +1,92 @@ --- -id: 56229547-5cee-49ff-be41-1b75e7548809 +id: f543683b-68be-431f-a6d5-7b4012b1345a title: "@dockstat/sqlite-wrapper" collectionId: b4a5e48f-f103-480b-9f50-8f53f515cab9 -parentDocumentId: 75d80211-7262-4064-aaa6-2ead20e17f43 -updatedAt: 2025-08-24T08:13:51.258Z -urlId: Lxt4IphXI5 +parentDocumentId: bbcefaa2-6bd4-46e8-ae4b-a6b823593e67 +updatedAt: 2025-12-17T09:19:53.239Z +urlId: vCSP0qqnqI --- -# @dockstat/sqlite-wrapper +> A fast, type-safe TypeScript wrapper for Bun's `bun:sqlite`. Schema-first table helpers, an expressive chainable QueryBuilder, safe defaults, JSON + generated columns, and production-minded pragmas & transactions. - ![Bun](https://img.shields.io/badge/Bun-%23000000.svg?style=for-the-badge&logo=bun&logoColor=white) ![SQLite](https://img.shields.io/badge/sqlite-%2307405e.svg?style=for-the-badge&logo=sqlite&logoColor=white) ![TypeScript](https://img.shields.io/badge/typescript-%23007ACC.svg?style=for-the-badge&logo=typescript&logoColor=white) +## Overview -**A comprehensive, type-safe TypeScript wrapper for Bun's** `**bun:sqlite**`**.** +`@dockstat/sqlite-wrapper` provides a modern, type-safe interface for SQLite operations in Bun applications. It's designed for production use with features like WAL mode support, prepared statements, and comprehensive type checking. -This library provides a complete SQLite interface with schema-first table helpers, an expressive chainable QueryBuilder, type-safe operations, production-ready defaults, and comprehensive SQLite feature support including JSON columns, generated columns, foreign keys, and advanced query capabilities. +```mermaidjs -## Installation +graph TB + subgraph "Application Layer" + APP["Your Application"] + end + + subgraph "@dockstat/sqlite-wrapper" + DB["DB Class"] + TABLE["Table API"] + QB["QueryBuilder"] + COL["Column Definitions"] + end + + subgraph "Bun Runtime" + SQLITE["bun:sqlite"] + end + + subgraph "Storage" + FILE["SQLite File"] + end -> **Requirements:** Bun runtime v1.0.0 or higher + APP --> DB + DB --> TABLE + TABLE --> QB + DB --> COL + QB --> SQLITE + SQLITE --> FILE +``` + +## Installation ```bash bun add @dockstat/sqlite-wrapper ``` +> **Note**: Requires Bun runtime. This package uses `bun:sqlite` which is not available in Node.js. + ## Quick Start ```typescript import { DB, column } from "@dockstat/sqlite-wrapper"; -// Create database with production-ready defaults -const db = new DB("app.db", { +// Define your data type + +type User = { + id?: number; + name: string; + email: string; + active: boolean; +}; + +// Create database with pragmas + +const db = new DB("app.db", { pragmas: [ - ["journal_mode", "WAL"], - ["foreign_keys", "ON"], - ["synchronous", "NORMAL"], - ["cache_size", -64000] - ] + ["journal_mode", "WAL"], + ["foreign_keys", "ON"] + ] }); -// Define schema with type-safe column helpers -db.createTable("users", { +// Create a typed table + +const users = db.createTable("users", { id: column.id(), name: column.text({ notNull: true }), email: column.text({ unique: true, notNull: true }), active: column.boolean({ default: true }), - metadata: column.json({ validateJson: true }), - created_at: column.createdAt(), - updated_at: column.updatedAt() + created_at: column.createdAt() }); -// Type-safe queries with IntelliSense -interface User { - id?: number; - name: string; - email: string; - active?: boolean; - metadata?: any; - created_at?: number; - updated_at?: number; -} +// Query with full type safety -const users = db.table("users") +const activeUsers = users .select(["id", "name", "email"]) .where({ active: true }) .orderBy("created_at").desc() @@ -68,2483 +94,581 @@ const users = db.table("users") .all(); ``` -## Core Features - -### Type Safety - -* **Compile-time validation** of column names and data shapes -* **IntelliSense support** for all operations -* **Generic interfaces** that adapt to your data models -* **Type-safe column definitions** with comprehensive constraint support - -### Safety-First Design - -* **Mandatory WHERE conditions** for UPDATE and DELETE operations to prevent accidental data loss -* **Parameter binding** for all queries to prevent SQL injection -* **Prepared statements** used internally for optimal performance -* **Transaction support** with automatic rollback on errors - -### Production Ready - -* **WAL mode** support for concurrent read/write operations -* **Comprehensive PRAGMA management** for performance tuning -* **Connection pooling** considerations built-in -* **Bulk operation** support with transaction batching -* **Schema introspection** tools for migrations and debugging - -### Complete SQLite Support +## Core Concepts -* **All SQLite data types** with proper TypeScript mappings -* **Generated columns** (both VIRTUAL and STORED) -* **Foreign key constraints** with cascade options -* **JSON columns** with validation and transformation -* **Full-text search** preparation -* **Custom functions** and extensions support +### Database Initialization -## Database Management +```mermaidjs -### DB Class +sequenceDiagram + participant App as "Application" + participant DB as "DB Instance" + participant SQLite as "bun:sqlite" + participant File as "Database File" -The `DB` class is the main entry point for database operations: + App->>DB: "new DB(path, options)" + DB->>SQLite: "Open connection" + SQLite->>File: "Create/Open file" + DB->>SQLite: "Apply PRAGMAs" + SQLite-->>DB: "Ready" + DB-->>App: "DB instance" +``` ```typescript import { DB } from "@dockstat/sqlite-wrapper"; -// Basic usage -const db = new DB("database.db"); +// Basic initialization +const db = new DB("app.db"); // With configuration const db = new DB("app.db", { pragmas: [ - ["journal_mode", "WAL"], // Enable WAL mode for concurrency - ["foreign_keys", "ON"], // Enable foreign key constraints - ["synchronous", "NORMAL"], // Balance between safety and speed - ["cache_size", -64000], // 64MB cache (negative = KB) - ["temp_store", "MEMORY"], // Store temp tables in memory - ["mmap_size", 268435456] // 256MB memory-mapped I/O - ], - loadExtensions: [ - "/path/to/extension.so" // Load SQLite extensions + ["journal_mode", "WAL"], // Write-Ahead Logging + ["synchronous", "NORMAL"], // Balance safety/speed + ["cache_size", "-64000"], // 64MB cache + ["foreign_keys", "ON"], // Enable foreign keys + ["temp_store", "MEMORY"], // In-memory temp tables + ["busy_timeout", "5000"] // 5 second timeout ] }); - -// In-memory database for testing -const testDb = new DB(":memory:"); ``` -### Database Operations - -```typescript -// Get direct access to underlying SQLite database -const sqliteDb = db.getDb(); - -// Execute raw SQL -db.exec("CREATE INDEX idx_users_email ON users(email)"); - -// Prepare statements for repeated use -const stmt = db.prepare("SELECT * FROM users WHERE active = ?"); -const activeUsers = stmt.all(1); +### Column Definitions -// PRAGMA operations -db.pragma("journal_mode", "WAL"); -const journalMode = db.pragma("journal_mode"); // Get current value +The `column` helper provides type-safe column definitions: -// Database maintenance -db.vacuum(); // Reclaim space -db.analyze(); // Update query planner statistics -db.analyze("users"); // Analyze specific table +```mermaidjs -// Schema introspection -const tableInfo = db.getTableInfo("users"); -const foreignKeys = db.getForeignKeys("orders"); -const indexes = db.getIndexes("users"); -const fullSchema = db.getSchema(); +graph LR + subgraph "Column Types" + ID["column.id()"] + TEXT["column.text()"] + INT["column.integer()"] + REAL["column.real()"] + BOOL["column.boolean()"] + JSON["column.json()"] + BLOB["column.blob()"] + DATE["column.createdAt()"] + end -// Integrity checking -const integrityResults = db.integrityCheck(); + subgraph "Constraints" + NOTNULL["notNull"] + UNIQUE["unique"] + DEFAULT["default"] + FK["foreignKey"] + end -// Connection management -db.close(); + ID --> NOTNULL + TEXT --> UNIQUE + INT --> DEFAULT + REAL --> FK ``` -## Schema Definition - -### Column Types and Helpers - -The library provides comprehensive column helpers that map to SQLite's type system: - ```typescript -import { column, sql } from "@dockstat/sqlite-wrapper"; +import { column } from "@dockstat/sqlite-wrapper"; -db.createTable("comprehensive_example", { - // Primary keys and IDs - id: column.id(), // INTEGER PRIMARY KEY AUTOINCREMENT - uuid: column.uuid({ generateDefault: true }), // TEXT with UUID generation - - // Text columns - name: column.text({ notNull: true }), // TEXT NOT NULL - description: column.text({ length: 500 }), // TEXT with length hint - title: column.varchar(255, { unique: true }), // VARCHAR(255) UNIQUE - code: column.char(10, { notNull: true }), // CHAR(10) NOT NULL - - // Numeric columns - price: column.numeric({ - precision: 10, - scale: 2, - check: "price >= 0" - }), // NUMERIC(10,2) with constraint - weight: column.real({ check: "weight > 0" }), // REAL with constraint - count: column.integer({ default: 0 }), // INTEGER DEFAULT 0 - big_number: column.integer({ size: "BIGINT" }), // BIGINT - - // Boolean (stored as INTEGER with constraint) - active: column.boolean({ default: true }), // INTEGER CHECK (active IN (0,1)) DEFAULT 1 - verified: column.boolean(), // INTEGER CHECK (verified IN (0,1)) +const schema = { + // Auto-incrementing primary key + id: column.id(), - // Date/Time columns - created_date: column.date(), // DATE (stored as TEXT) - event_time: column.time(), // TIME (stored as TEXT) - created_at: column.timestamp(), // INTEGER timestamp - updated_at: column.timestamp({ asText: true }), // TEXT timestamp - expires_at: column.datetime({ - default: sql.raw("datetime('now', '+1 year')") - }), // DATETIME with expression default + // Text with constraints + name: column.text({ notNull: true }), + email: column.text({ unique: true, notNull: true }), + bio: column.text({ default: "" }), - // Special columns - status: column.enum([ - "pending", "active", "inactive", "deleted" - ], { default: "pending" }), // TEXT with CHECK constraint + // Numeric types + age: column.integer(), + score: column.real(), - metadata: column.json({ - validateJson: true, - comment: "Stored as JSON text with validation" - }), // TEXT with JSON validation + // Boolean (stored as INTEGER 0/1) + active: column.boolean({ default: true }), - file_data: column.blob(), // BLOB for binary data + // JSON column (stored as TEXT, parsed on read) + metadata: column.json(), - // Foreign keys - user_id: column.foreignKey("users", "id", { - onDelete: "CASCADE", - onUpdate: "RESTRICT" - }), // INTEGER with FK constraint + // Binary data + avatar: column.blob(), - category_id: column.foreignKey("categories", "uuid", { - type: "TEXT", // Match referenced column type - onDelete: "SET NULL" - }), + // Auto-timestamp + created_at: column.createdAt(), - // Generated columns - full_name: { - type: "TEXT", - generated: { - expression: "first_name || ' ' || last_name", - stored: false // VIRTUAL column + // Foreign key + team_id: column.integer({ + foreignKey: { + table: "teams", + column: "id", + onDelete: "CASCADE", + onUpdate: "CASCADE" } - }, + }), - search_text: { - type: "TEXT", - generated: { - expression: "lower(name || ' ' || coalesce(description, ''))", - stored: true // STORED column (can be indexed) - } - }, + // Generated column (virtual) + full_name: column.generated( + "first_name || ' ' || last_name", + "VIRTUAL" + ), - // Timestamp helpers - created_at: column.createdAt(), // Auto-managed creation time - updated_at: column.updatedAt() // Auto-managed update time -}); + // Generated column (stored) + search_text: column.generated( + "lower(name || ' ' || email)", + "STORED" + ) +}; ``` -### Table Options and Constraints +### Table Creation ```typescript -// Advanced table creation with constraints -db.createTable("orders", { +// Create table with schema + +const users = db.createTable("users", { id: column.id(), - order_number: column.varchar(50), - customer_id: column.integer(), - total: column.numeric({ precision: 10, scale: 2 }), - status: column.enum(["pending", "paid", "shipped", "delivered"]), - created_at: column.createdAt() -}, { - // Table-level constraints - constraints: { - // Composite primary key (alternative to column.id()) - primaryKey: ["customer_id", "order_number"], - - // Unique constraints - unique: [ - ["customer_id", "order_number"], // Single composite unique - [["email"], ["phone"]] // Multiple unique constraints - ], - - // Check constraints - check: [ - "total >= 0", - "status IN ('pending', 'paid', 'shipped', 'delivered')" - ], - - // Foreign key constraints - foreignKeys: [{ - columns: ["customer_id"], - references: { - table: "customers", - columns: ["id"], - onDelete: "CASCADE", - onUpdate: "RESTRICT" - } - }] - }, - - // Table options - ifNotExists: true, // CREATE TABLE IF NOT EXISTS - temporary: false, // CREATE TEMPORARY TABLE - withoutRowId: false, // CREATE TABLE ... WITHOUT ROWID - comment: "Customer order records" // Metadata comment + name: column.text({ notNull: true }), + email: column.text({ unique: true }) }); -``` -### Index Management +// Table is created if it doesn't exist +// Returns a typed QueryBuilder for the table +``` -```typescript -// Create indexes for performance -db.createIndex("idx_users_email", "users", "email", { - unique: true, - ifNotExists: true -}); +## QueryBuilder API -// Composite index -db.createIndex("idx_orders_customer_date", "orders", - ["customer_id", "created_at"], - { ifNotExists: true } -); +### Select Operations -// Partial index with WHERE clause -db.createIndex("idx_active_users", "users", "email", { - where: "active = 1 AND deleted_at IS NULL" -}); +```mermaidjs -// Drop indexes -db.dropIndex("idx_old_index", { ifExists: true }); +graph LR + SELECT["select()"] --> WHERE["where()"] + WHERE --> ORDER["orderBy()"] + ORDER --> LIMIT["limit()"] + LIMIT --> OFFSET["offset()"] + OFFSET --> EXEC["all() / first() / run()"] ``` -## QueryBuilder Operations - -### Basic Querying - -The QueryBuilder provides a fluent interface for constructing and executing queries: - ```typescript -interface User { - id: number; - name: string; - email: string; - active: boolean; - created_at: number; -} - -const users = db.table("users"); - // Select all columns -const allUsers = users.all(); +const all = users.select(["*"]).all(); // Select specific columns -const userNames = users - .select(["id", "name", "email"]) - .all(); - -// Get single record -const user = users - .where({ id: 1 }) - .first(); - -// Check existence -const hasActiveUsers = users - .where({ active: true }) - .exists(); - -// Count records -const userCount = users.count(); -const activeUserCount = users - .where({ active: true }) - .count(); - -// Get single column value -const userName = users - .where({ id: 1 }) - .value("name"); - -// Get array of column values -const allEmails = users - .where({ active: true }) - .pluck("email"); -``` +const names = users.select(["id", "name"]).all(); -### WHERE Conditions - -The library supports comprehensive WHERE condition building: +// With conditions +const filtered = users + .select(["*"]) + .where({ active: true, role: "admin" }) + .all(); -```typescript -// Simple equality conditions -users.where({ - active: true, - name: "John Doe" -}).all(); - -// Null conditions (automatically handled) -users.where({ - deleted_at: null, // Becomes: deleted_at IS NULL - middle_name: undefined // Becomes: middle_name IS NULL -}).all(); - -// Comparison operators -users.whereOp("age", ">", 18).all(); -users.whereOp("created_at", "<=", Date.now()).all(); -users.whereOp("name", "LIKE", "%john%").all(); -users.whereOp("email", "GLOB", "*@gmail.com").all(); - -// IN and NOT IN clauses -users.whereIn("status", ["active", "pending"]).all(); -users.whereNotIn("id", [1, 2, 3]).all(); - -// BETWEEN conditions -users.whereBetween("age", 18, 65).all(); -users.whereNotBetween("score", 0, 50).all(); - -// NULL checks -users.whereNull("deleted_at").all(); -users.whereNotNull("phone").all(); - -// Raw SQL conditions with parameter binding -users.whereRaw("age > ? AND (status = ? OR premium = 1)", [21, "active"]).all(); - -// Complex expressions -users.whereExpr("julianday('now') - julianday(created_at) > 30").all(); - -// Regex conditions (applied client-side after SQL filtering) -users.whereRgx({ - email: /@gmail\.com$/i, - name: /^john/i -}).all(); - -// Chaining conditions (AND logic) -const complexQuery = users +// Complex conditions +const complex = users + .select(["*"]) .where({ active: true }) - .whereOp("age", ">=", 18) - .whereIn("role", ["admin", "moderator"]) - .whereNotNull("email") - .whereBetween("created_at", startDate, endDate) - .whereRaw("last_login > datetime('now', '-30 days')") + .and({ role: "admin" }) + .or({ role: "superuser" }) .all(); -``` -### Ordering and Pagination - -```typescript -// Basic ordering -users.orderBy("name").asc().all(); -users.orderBy("created_at").desc().all(); +// Ordering +const ordered = users + .select(["*"]) + .orderBy("created_at").desc() + .orderBy("name").asc() + .all(); // Pagination -users - .orderBy("id") +const page = users + .select(["*"]) .limit(10) .offset(20) .all(); -// Combined ordering and pagination -const recentUsers = users - .where({ active: true }) - .orderBy("created_at").desc() - .limit(50) - .all(); - -// Complex pagination helper -function paginateUsers(page: number, perPage: number = 20) { - return users - .where({ active: true }) - .orderBy("created_at").desc() - .limit(perPage) - .offset((page - 1) * perPage) - .all(); -} -``` - -### JSON Column Operations - -```typescript -interface UserWithMetadata { - id: number; - name: string; - metadata: { - preferences: Record; - settings: Record; - tags: string[]; - }; -} - -// Configure JSON columns for automatic serialization/deserialization -const usersWithJson = db.table("users", { - jsonColumns: ["metadata"] -}); - -// Insert with JSON data (automatically serialized) -usersWithJson.insert({ - name: "John Doe", - metadata: { - preferences: { theme: "dark", language: "en" }, - settings: { notifications: true }, - tags: ["premium", "early-adopter"] - } -}); - -// Query and get automatically deserialized JSON -const userWithMetadata = usersWithJson - .where({ id: 1 }) - .first(); // metadata is automatically parsed as JavaScript object - -// Use SQL JSON functions in queries -const premiumUsers = usersWithJson - .whereRaw("JSON_EXTRACT(metadata, '$.tags') LIKE '%premium%'") - .all(); - -// Update JSON fields -usersWithJson +// Get single result +const user = users + .select(["*"]) .where({ id: 1 }) - .update({ - metadata: { - preferences: { theme: "light", language: "es" }, - settings: { notifications: false }, - tags: ["premium", "updated"] - } - }); + .first(); ``` -## Data Modification - ### Insert Operations ```typescript -// Single record insert +// Insert single row const result = users.insert({ name: "John Doe", email: "john@example.com", active: true }); -console.log(`Inserted with ID: ${result.insertId}`); +console.log(result.lastInsertRowid); // New row ID -// Multiple record insert -const bulkResult = users.insert([ +// Insert multiple rows +const results = users.insertMany([ { name: "Alice", email: "alice@example.com" }, - { name: "Bob", email: "bob@example.com" }, - { name: "Carol", email: "carol@example.com" } + { name: "Bob", email: "bob@example.com" } ]); -console.log(`Inserted ${bulkResult.changes} records`); - -// Insert and return the created record -const newUser = users.insertAndGet({ - name: "Dave", - email: "dave@example.com" -}); -console.log(`Created user:`, newUser); - -// Conflict resolution -users.insertOrIgnore({ name: "John", email: "john@example.com" }); // Skip if conflict -users.insertOrReplace({ id: 1, name: "John Updated" }); // Replace if conflict -users.insertOrAbort({ name: "Invalid" }); // Abort transaction on conflict -users.insertOrFail({ name: "Invalid" }); // Fail statement on conflict -users.insertOrRollback({ name: "Invalid" }); // Rollback on conflict - -// Batch insert with transaction (high performance) -const batchData = Array.from({ length: 1000 }, (_, i) => ({ - name: `User ${i}`, - email: `user${i}@example.com` -})); - -const batchResult = users.insertBatch(batchData); -console.log(`Batch inserted ${batchResult.changes} records`); - -// Insert with specific conflict resolution -users.insertBatch(batchData, { orIgnore: true }); ``` ### Update Operations -All update operations require WHERE conditions to prevent accidental full-table updates: - ```typescript -// Basic update (throws error without WHERE clause) -users +// Update with WHERE clause (required for safety) +const updated = users + .update({ active: false }) .where({ id: 1 }) - .update({ - name: "John Updated", - active: false - }); + .run(); +console.log(updated.changes); // Number of rows affected -// Update with complex conditions +// Update multiple conditions users - .where({ active: true }) - .whereOp("last_login", "<", Date.now() - 86400000) // 1 day ago - .update({ active: false }); + .update({ role: "inactive" }) + .where({ active: false }) + .and({ last_login: null }) + .run(); +``` -// Increment/decrement operations -users +### Delete Operations + +```typescript +// Delete with WHERE clause (required for safety) +const deleted = users + .delete() .where({ id: 1 }) - .increment("login_count", 1); + .run(); +console.log(deleted.changes); // Number of rows deleted +// Delete with multiple conditions users - .where({ role: "premium" }) - .decrement("credits", 10); - -// Update and return affected records -const updatedUsers = users + .delete() .where({ active: false }) - .updateAndGet({ status: "inactive" }); + .and({ created_at: { lt: "2023-01-01" } }) + .run(); +``` -// Upsert (insert or replace) -users.upsert({ - id: 1, - name: "John Doe", - email: "john@example.com" -}); +## Safety Features -// Batch update with different conditions -const batchUpdates = [ - { - where: { role: "admin" }, - data: { permissions: "full" } - }, - { - where: { role: "user" }, - data: { permissions: "limited" } - } -]; +### Mandatory WHERE Clauses + +To prevent accidental data loss, UPDATE and DELETE operations require WHERE clauses: + +```typescript +// This will throw an error +users.update({ active: false }).run(); // Error! + +// This works +users.update({ active: false }).where({ id: 1 }).run(); -users.updateBatch(batchUpdates); +// To update all rows intentionally, use a truthy condition +users.update({ active: false }).where({ 1: 1 }).run(); ``` -### Delete Operations +### Parameter Binding -Like updates, delete operations require WHERE conditions: +All queries use parameter binding to prevent SQL injection: ```typescript -// Basic delete (throws error without WHERE clause) -users - .where({ active: false }) - .where({ last_login: null }) - .delete(); +// Safe - parameters are bound -// Delete and return deleted records -const deletedUsers = users - .where({ created_at: { $lt: oldTimestamp } }) - .deleteAndGet(); +users.select(["*"]).where({ email: userInput }).all(); -// Soft delete (mark as deleted instead of removing) -users - .where({ id: 1 }) - .softDelete("deleted_at", Date.now()); +// The actual query uses placeholders +// SELECT * FROM users WHERE email = ? +``` -// Restore soft deleted records -users - .where({ id: 1 }) - .restore("deleted_at"); +## Transactions -// Delete records older than timestamp -users.deleteOlderThan("created_at", Date.now() - (30 * 86400000)); // 30 days +```mermaidjs -// Delete duplicates (keep first occurrence) -users.deleteDuplicates(["email"]); -users.deleteDuplicates(["name", "email"]); // Composite duplicate check +sequenceDiagram + participant App as "Application" + participant DB as "Database" + participant SQLite as "SQLite" -// Batch delete with different conditions -const deleteConditions = [ - { status: "temp" }, - { active: false, created_at: { $lt: oldDate } } -]; -users.deleteBatch(deleteConditions); + App->>DB: "db.transaction(() => { ... })" + DB->>SQLite: "BEGIN TRANSACTION" + + loop "Operations" + DB->>SQLite: "INSERT/UPDATE/DELETE" + SQLite-->>DB: "Result" + end -// Truncate table (delete all records - bypasses WHERE requirement) -users.truncate(); // Use with extreme caution! + alt "Success" + DB->>SQLite: "COMMIT" + SQLite-->>DB: "Committed" + DB-->>App: "Return value" + else "Error" + DB->>SQLite: "ROLLBACK" + SQLite-->>DB: "Rolled back" + DB-->>App: "Throw error" + end ``` -## Transaction Management - -### Automatic Transactions - ```typescript -// Simple transaction with automatic commit/rollback +// Transaction with automatic rollback on error + const result = db.transaction(() => { - const user = users.insert({ name: "John", email: "john@example.com" }); + const user = users.insert({ name: "Alice", email: "alice@example.com" }); - profiles.insert({ - user_id: user.insertId, - bio: "Software developer" + teams.insert({ + name: "Alice's Team", + owner_id: user.lastInsertRowid }); - return user; -}); // Automatically commits on success, rolls back on error - -// Transaction with return value -const transferResult = db.transaction(() => { - // Deduct from source account - accounts - .where({ id: sourceAccountId }) - .decrement("balance", amount); - - // Add to destination account - accounts - .where({ id: destAccountId }) - .increment("balance", amount); - - // Log the transfer - const transfer = transfers.insert({ - from_account: sourceAccountId, - to_account: destAccountId, - amount: amount, - timestamp: Date.now() - }); + return user.lastInsertRowid; +}); + +// Nested transactions (savepoints) +db.transaction(() => { + users.insert({ name: "User 1" }); - return transfer; + db.transaction(() => { + users.insert({ name: "User 2" }); + // Inner transaction can rollback independently + }); }); ``` -### Manual Transaction Control +## JSON Columns ```typescript -// Manual transaction management -try { - db.begin("IMMEDIATE"); // Begin with lock mode - - // Perform multiple operations - const user = users.insert({ name: "John" }); - profiles.insert({ user_id: user.insertId }); - - db.commit(); -} catch (error) { - db.rollback(); - throw error; -} +type Config = { + id: number; + settings: { + theme: string; + notifications: boolean; + }; +}; -// Savepoint management for nested transactions -try { - db.begin(); - - // First operation - users.insert({ name: "Alice" }); - - db.savepoint("checkpoint1"); - - try { - // Risky operation - users.insert({ name: "Bob", email: null }); // This might fail - db.releaseSavepoint("checkpoint1"); - } catch (error) { - db.rollbackToSavepoint("checkpoint1"); - console.log("Rolled back risky operation, continuing..."); - } - - // Continue with other operations - profiles.insert({ user_id: 1 }); - - db.commit(); -} catch (error) { - db.rollback(); -} +const configs = db.createTable("configs", { + id: column.id(), + settings: column.json() +}, { + parser: { JSON: ["settings"] } // Specify JSON columns +}); + +// Insert with object + +configs.insert({ + settings: { theme: "dark", notifications: true } +}); + +// Data is automatically serialized/deserialized +const config = configs.select(["*"]).where({ id: 1 }).first(); +console.log(config.settings.theme); // "dark" ``` -### Transaction Modes +## Generated Columns ```typescript -// Different transaction modes for concurrency control -db.begin("DEFERRED"); // Default - lock acquired when first read/write -db.begin("IMMEDIATE"); // Acquire reserved lock immediately -db.begin("EXCLUSIVE"); // Acquire exclusive lock immediately - -// Batch processing with transactions -function processBatchWithTransaction( - items: T[], - processor: (item: T) => void, - batchSize: number = 1000 -) { - for (let i = 0; i < items.length; i += batchSize) { - const batch = items.slice(i, i + batchSize); - - db.transaction(() => { - for (const item of batch) { - processor(item); - } - }); - } -} +const users = db.createTable("users", { + id: column.id(), + first_name: column.text({ notNull: true }), + last_name: column.text({ notNull: true }), + + // Virtual: computed on read + full_name: column.generated( + "first_name || ' ' || last_name", + "VIRTUAL" + ), + + // Stored: computed on write, indexed + search_key: column.generated( + "lower(first_name || last_name)", + "STORED" + ) +}); -// Usage example -const userData = loadLargeDataset(); -processBatchWithTransaction(userData, (user) => { - users.insert(user); -}, 500); +// Query using generated columns +const results = users + .select(["id", "full_name"]) + .where({ search_key: "johndoe" }) + .all(); ``` -## Advanced Features +## Indexes + +```typescript +// Create index after table +db.exec(` + CREATE INDEX IF NOT EXISTS idx_users_email + ON users(email) +`); + +// Composite index +db.exec(` + CREATE INDEX IF NOT EXISTS idx_users_active_created + ON users(active, created_at DESC) +`); + +// Unique index +db.exec(` + CREATE UNIQUE INDEX IF NOT EXISTS idx_users_username + ON users(username) +`); +``` -### Schema Introspection and Migrations +## Schema Introspection ```typescript -// Get detailed table information -const tableInfo = db.getTableInfo("users"); +// Get database schema +const schema = db.getSchema(); +console.log(schema); /* -Returns array of: { - cid: 0, // Column ID - name: "id", // Column name - type: "INTEGER", // Column type - notnull: 1, // NOT NULL constraint - dflt_value: null, // Default value - pk: 1 // Primary key flag + users: { + columns: ["id", "name", "email", "active"], + ... + }, + teams: { ... } } */ -// Get foreign key relationships -const foreignKeys = db.getForeignKeys("orders"); -/* -Returns array of: -{ - id: 0, // FK constraint ID - seq: 0, // Sequence in multi-column FK - table: "users", // Referenced table - from: "user_id", // Local column - to: "id", // Referenced column - on_update: "RESTRICT", - on_delete: "CASCADE", - match: "NONE" -} -*/ +// Check if table exists -// Get index information -const indexes = db.getIndexes("users"); -/* -Returns array of: -{ - name: "idx_users_email", - unique: 1, - origin: "c", // 'c' = CREATE INDEX, 'u' = UNIQUE, 'pk' = PRIMARY KEY - partial: 0 // Partial index flag -} -*/ +const tables = db.exec("SELECT name FROM sqlite_master WHERE type='table'"); +``` -// Get complete schema -const schema = db.getSchema(); -// Returns all CREATE statements for tables, indexes, views, triggers - -// Migration helper function -function migrateTable( - oldTable: string, - newTable: string, - newSchema: any, - columnMapping: Record = {} -) { - return db.transaction(() => { - // Create new table - db.createTable(newTable, newSchema, { ifNotExists: true }); - - // Build column mapping - const oldInfo = db.getTableInfo(oldTable); - const newInfo = db.getTableInfo(newTable); - - const oldColumns = oldInfo.map(col => col.name); - const newColumns = newInfo.map(col => col.name); - - // Map old columns to new columns - const mappedColumns = oldColumns - .map(col => columnMapping[col] || col) - .filter(col => newColumns.includes(col)); - - if (mappedColumns.length > 0) { - const selectCols = oldColumns - .filter(col => mappedColumns.includes(columnMapping[col] || col)) - .map(col => columnMapping[col] ? `${col} AS ${columnMapping[col]}` : col) - .join(", "); - - const insertCols = mappedColumns.join(", "); - - // Copy data - db.exec(`INSERT INTO ${newTable} (${insertCols}) - SELECT ${selectCols} FROM ${oldTable}`); - } - - // Drop old table - db.dropTable(oldTable); - - // Rename new table - db.exec(`ALTER TABLE ${newTable} RENAME TO ${oldTable}`); - }); -} +## Raw SQL -// Usage -migrateTable("users", "users_new", { - id: column.id(), - full_name: column.text({ notNull: true }), // Combined first_name + last_name - email: column.text({ unique: true }), - created_at: column.createdAt() -}, { - "first_name": "full_name", // Map first_name to full_name - // last_name will be dropped +For complex queries not supported by the QueryBuilder: + +```typescript +// Execute raw SQL +const results = db.exec(` + SELECT u.*, COUNT(p.id) as post_count + FROM users u + LEFT JOIN posts p ON p.user_id = u.id + GROUP BY u.id + HAVING post_count > 5 +`); + +// With parameters +const user = db.exec( + "SELECT * FROM users WHERE email = ?", + ["john@example.com"] +); +``` + +## Performance Considerations + +### WAL Mode + +Write-Ahead Logging provides better concurrent read/write performance: + +```typescript +const db = new DB("app.db", { + pragmas: [ + ["journal_mode", "WAL"] + ] }); ``` -### Performance Optimization +### Prepared Statements + +The QueryBuilder uses prepared statements internally for optimal performance: ```typescript -// Database optimization settings -function optimizeDatabase(db: DB) { - // WAL mode for concurrent access - db.pragma("journal_mode", "WAL"); - - // Optimize for speed vs safety balance - db.pragma("synchronous", "NORMAL"); // FULL is safest, OFF is fastest - - // Increase cache size (negative = KB, positive = pages) - db.pragma("cache_size", -64000); // 64MB cache - - // Memory-mapped I/O for large databases - db.pragma("mmap_size", 268435456); // 256MB - - // Store temporary tables/indexes in memory - db.pragma("temp_store", "MEMORY"); - - // Optimize query planner - db.pragma("optimize"); - - // Update table statistics - db.analyze(); +// Queries are prepared and cached + +for (let i = 0; i < 1000; i++) { + users.select(["*"]).where({ id: i }).first(); + // Same prepared statement reused } +``` -// Bulk insert optimization -function bulkInsertOptimized( - table: QueryBuilder, - data: Partial[], - options: { batchSize?: number } = {} -) { - const batchSize = options.batchSize || 1000; - - // Temporarily disable safety features for speed - const originalSync = db.pragma("synchronous"); - db.pragma("synchronous", "OFF"); - - try { - for (let i = 0; i < data.length; i += batchSize) { - const batch = data.slice(i, i + batchSize); - db.transaction(() => { - table.insertBatch(batch); - }); - } - } finally { - // Restore original settings - db.pragma("synchronous", originalSync); - db.pragma("optimize"); // Update statistics - } -} - -// Query optimization examples -function optimizeQueries() { - // Use indexes effectively - db.createIndex("idx_users_active_created", "users", ["active", "created_at"]); - - // Covering index (includes all needed columns) - db.createIndex("idx_users_cover", "users", ["active"], { - // Note: SQLite doesn't support INCLUDE syntax, but you can create composite indexes - }); - - // Partial indexes for common queries - db.createIndex("idx_active_users", "users", "email", { - where: "active = 1" - }); - - // Use LIMIT when you don't need all results - const recentActiveUsers = users - .where({ active: true }) - .orderBy("created_at").desc() - .limit(100) // Don't load everything - .all(); - - // Use EXISTS instead of COUNT when checking existence - const hasActiveUsers = users - .where({ active: true }) - .exists(); // More efficient than .count() > 0 - - // Use appropriate data types - // INTEGER for IDs, REAL for floating point, TEXT for strings - // Avoid storing numbers as TEXT when possible -} - -// Memory management for large datasets -function processLargeDataset(tableName: string) { - const BATCH_SIZE = 1000; - let offset = 0; - let batch: any[]; - - do { - batch = db.table(tableName) - .orderBy("id") - .limit(BATCH_SIZE) - .offset(offset) - .all(); - - // Process batch - for (const row of batch) { - processRow(row); - } - - offset += BATCH_SIZE; - } while (batch.length === BATCH_SIZE); -} -``` - -### Extension and Custom Functions - -```typescript -// Load SQLite extensions -const db = new DB("app.db", { - loadExtensions: [ - "/usr/lib/sqlite3/pcre.so", // PCRE regex support - "/usr/lib/sqlite3/uuid.so", // UUID functions - "/usr/lib/sqlite3/json1.so" // JSON functions (built-in in modern SQLite) - ] -}); - -// Use extension functions -const regexMatches = users - .whereRaw("email REGEXP ?", ["@gmail\\.com$"]) - .all(); - -// Custom application-level functions -class ExtendedDB extends DB { - // Custom method for common query patterns - findUsersByDomain(domain: string) { - return this.table("users") - .whereRaw("email LIKE ?", [`%@${domain}`]) - .all(); - } - - // Pagination helper - paginate( - table: string, - page: number, - perPage: number = 20, - conditions: Partial = {} - ) { - const offset = (page - 1) * perPage; - - const query = this.table(table); - - if (Object.keys(conditions).length > 0) { - query.where(conditions); - } - - const [data, total] = [ - query.limit(perPage).offset(offset).all(), - query.count() - ]; - - return { - data, - total, - page, - perPage, - totalPages: Math.ceil(total / perPage), - hasNext: page * perPage < total, - hasPrev: page > 1 - }; - } - - // Soft delete with automatic timestamp - softDeleteWithTimestamp( - table: string, - conditions: Partial, - deletedColumn: string = "deleted_at" - ) { - return this.table(table) - .where(conditions) - .softDelete(deletedColumn as keyof T, Math.floor(Date.now() / 1000)); - } - - // Bulk upsert operation - bulkUpsert(table: string, records: Partial[]) { - return this.transaction(() => { - return records.map(record => - this.table(table).upsert(record) - ); - }); - } -} - -const extendedDb = new ExtendedDB("app.db"); -const gmailUsers = extendedDb.findUsersByDomain("gmail.com"); -const paginatedUsers = extendedDb.paginate("users", 1, 20, { active: true }); -``` - -## Error Handling and Debugging - -### Safety Features - -```typescript -// All destructive operations require explicit WHERE conditions -try { - users.update({ active: false }); // Throws error - no WHERE clause -} catch (error) { - console.error("Safety check:", error.message); - // Error: UPDATE operation requires at least one WHERE condition -} - -// Correct usage -users - .where({ status: "inactive" }) - .update({ active: false }); - -// To perform full-table updates, use explicit conditions -users - .whereRaw("1 = 1") // Explicit full-table condition - .update({ migrated: true }); - -// Or use specific methods that bypass safety checks -users.truncate(); // Explicitly destructive - -// Parameter validation -try { - users.whereIn("status", []); // Throws error - empty array -} catch (error) { - console.error("Validation error:", error.message); -} - -// Type safety at compile time -const user = users.where({ id: 1 }).first(); -// user.nonExistentField; // TypeScript error - property doesn't exist -``` - -### Database Integrity and Validation - -```typescript -// Run integrity check -const integrityResults = db.integrityCheck(); -if (integrityResults.some(result => result.integrity_check !== "ok")) { - console.error("Database integrity issues found:", integrityResults); -} - -// Foreign key constraint checking -db.pragma("foreign_key_check"); // Check all tables -db.exec("PRAGMA foreign_key_check(users)"); // Check specific table - -// Schema validation helper -function validateSchema() { - const expectedTables = ["users", "profiles", "orders"]; - const schema = db.getSchema(); - const actualTables = schema - .filter(item => item.type === "table") - .map(item => item.name); - - const missing = expectedTables.filter(table => - !actualTables.includes(table) - ); - - if (missing.length > 0) { - throw new Error(`Missing tables: ${missing.join(", ")}`); - } -} - -// JSON validation for JSON columns -db.createTable("documents", { - id: column.id(), - data: column.json({ validateJson: true }) // Validates JSON on insert -}); - -// This will fail if invalid JSON is inserted -try { - documents.insert({ data: "invalid json string" }); -} catch (error) { - console.error("JSON validation failed:", error); -} -``` - -### Debugging and Monitoring - -```typescript -// Enable detailed logging for development -class DebuggingDB extends DB { - private logQueries = true; - - table(tableName: string, jsonConfig?: any) { - const queryBuilder = super.table(tableName, jsonConfig); - - if (this.logQueries) { - // Override query methods to add logging - const originalAll = queryBuilder.all.bind(queryBuilder); - queryBuilder.all = () => { - console.log(`[SQL] SELECT * FROM ${tableName} with conditions`); - const startTime = performance.now(); - const results = originalAll(); - const endTime = performance.now(); - console.log(`[SQL] Query completed in ${endTime - startTime}ms, returned ${results.length} rows`); - return results; - }; - } - - return queryBuilder; - } -} - -// Performance monitoring -function monitorQueryPerformance(queryBuilder: any, operation: string): T { - const startTime = performance.now(); - const result = queryBuilder; - const endTime = performance.now(); - - if (endTime - startTime > 100) { // Log slow queries - console.warn(`[SLOW QUERY] ${operation} took ${endTime - startTime}ms`); - } - - return result; -} - -// Usage -const slowUsers = monitorQueryPerformance( - users.where({ active: true }).all(), - "SELECT active users" -); -``` - -## Real-World Examples - -### E-commerce System - -```typescript -interface Product { - id?: number; - sku: string; - name: string; - description?: string; - price: number; - category_id: number; - inventory: number; - active: boolean; - metadata?: { - tags: string[]; - attributes: Record; - seo?: { - title?: string; - description?: string; - }; - }; - created_at?: number; - updated_at?: number; -} - -interface Order { - id?: number; - order_number: string; - customer_id: number; - status: "pending" | "paid" | "shipped" | "delivered" | "cancelled"; - subtotal: number; - tax: number; - shipping: number; - total: number; - notes?: string; - created_at?: number; - updated_at?: number; -} - -interface OrderItem { - id?: number; - order_id: number; - product_id: number; - quantity: number; - unit_price: number; - total_price: number; -} - -// Schema creation -function setupEcommerceSchema(db: DB) { - // Categories table - db.createTable("categories", { - id: column.id(), - name: column.text({ notNull: true, unique: true }), - slug: column.text({ notNull: true, unique: true }), - parent_id: column.foreignKey("categories", "id", { onDelete: "CASCADE" }), - active: column.boolean({ default: true }), - created_at: column.createdAt() - }); - - // Products table - db.createTable("products", { - id: column.id(), - sku: column.varchar(100, { unique: true, notNull: true }), - name: column.text({ notNull: true }), - description: column.text(), - price: column.numeric({ precision: 10, scale: 2, check: "price >= 0" }), - category_id: column.foreignKey("categories", "id", { onDelete: "RESTRICT" }), - inventory: column.integer({ default: 0, check: "inventory >= 0" }), - active: column.boolean({ default: true }), - metadata: column.json({ validateJson: true }), - - // Generated columns for search - search_text: { - type: "TEXT", - generated: { - expression: "lower(name || ' ' || coalesce(description, '') || ' ' || sku)", - stored: true - } - }, - - created_at: column.createdAt(), - updated_at: column.updatedAt() - }, { - constraints: { - check: ["price > 0 OR active = 0"] // Inactive products can have 0 price - } - }); - - // Orders table - db.createTable("orders", { - id: column.id(), - order_number: column.varchar(50, { unique: true, notNull: true }), - customer_id: column.integer({ notNull: true }), - status: column.enum(["pending", "paid", "shipped", "delivered", "cancelled"], { - default: "pending" - }), - subtotal: column.numeric({ precision: 10, scale: 2, notNull: true }), - tax: column.numeric({ precision: 10, scale: 2, default: 0 }), - shipping: column.numeric({ precision: 10, scale: 2, default: 0 }), - total: column.numeric({ precision: 10, scale: 2, notNull: true }), - notes: column.text(), - created_at: column.createdAt(), - updated_at: column.updatedAt() - }, { - constraints: { - check: [ - "subtotal >= 0", - "tax >= 0", - "shipping >= 0", - "total = subtotal + tax + shipping" - ] - } - }); - - // Order items table - db.createTable("order_items", { - id: column.id(), - order_id: column.foreignKey("orders", "id", { onDelete: "CASCADE" }), - product_id: column.foreignKey("products", "id", { onDelete: "RESTRICT" }), - quantity: column.integer({ notNull: true, check: "quantity > 0" }), - unit_price: column.numeric({ precision: 10, scale: 2, notNull: true }), - total_price: column.numeric({ precision: 10, scale: 2, notNull: true }) - }, { - constraints: { - check: ["total_price = quantity * unit_price"], - unique: [["order_id", "product_id"]] // Prevent duplicate items in same order - } - }); - - // Indexes for performance - db.createIndex("idx_products_category", "products", "category_id"); - db.createIndex("idx_products_active_price", "products", ["active", "price"]); - db.createIndex("idx_products_search", "products", "search_text"); - db.createIndex("idx_orders_customer", "orders", "customer_id"); - db.createIndex("idx_orders_status_created", "orders", ["status", "created_at"]); -} - -// Business logic implementation -class EcommerceService { - constructor(private db: DB) {} - - // Product management - searchProducts(query: string, options: { - category?: number; - minPrice?: number; - maxPrice?: number; - limit?: number; - offset?: number; - } = {}) { - const products = this.db.table("products", { - jsonColumns: ["metadata"] - }); - - let queryBuilder = products - .where({ active: true }) - .whereRaw("search_text LIKE ?", [`%${query.toLowerCase()}%`]); - - if (options.category) { - queryBuilder = queryBuilder.where({ category_id: options.category }); - } - - if (options.minPrice !== undefined) { - queryBuilder = queryBuilder.whereOp("price", ">=", options.minPrice); - } - - if (options.maxPrice !== undefined) { - queryBuilder = queryBuilder.whereOp("price", "<=", options.maxPrice); - } - - return queryBuilder - .orderBy("name") - .limit(options.limit || 20) - .offset(options.offset || 0) - .all(); - } - - // Order processing - createOrder(orderData: { - customer_id: number; - items: Array<{ product_id: number; quantity: number }>; - shipping_cost?: number; - tax_rate?: number; - }) { - return this.db.transaction(() => { - // Get products and validate inventory - const productIds = orderData.items.map(item => item.product_id); - const products = this.db.table("products") - .whereIn("id", productIds) - .all(); - - const productMap = new Map(products.map(p => [p.id!, p])); - - let subtotal = 0; - const orderItems: Omit[] = []; - - // Calculate totals and validate inventory - for (const item of orderData.items) { - const product = productMap.get(item.product_id); - if (!product) { - throw new Error(`Product ${item.product_id} not found`); - } - - if (product.inventory < item.quantity) { - throw new Error(`Insufficient inventory for ${product.name}`); - } - - const itemTotal = product.price * item.quantity; - subtotal += itemTotal; - - orderItems.push({ - product_id: item.product_id, - quantity: item.quantity, - unit_price: product.price, - total_price: itemTotal - }); - } - - const tax = subtotal * (orderData.tax_rate || 0); - const shipping = orderData.shipping_cost || 0; - const total = subtotal + tax + shipping; - - // Generate order number - const orderNumber = `ORD-${Date.now()}-${Math.random().toString(36).substr(2, 5).toUpperCase()}`; - - // Create order - const order = this.db.table("orders").insertAndGet({ - order_number: orderNumber, - customer_id: orderData.customer_id, - subtotal, - tax, - shipping, - total, - status: "pending" - }); - - if (!order) { - throw new Error("Failed to create order"); - } - - // Create order items - const itemsWithOrderId = orderItems.map(item => ({ - ...item, - order_id: order.id! - })); - - this.db.table("order_items").insertBatch(itemsWithOrderId); - - // Update product inventory - for (const item of orderData.items) { - this.db.table("products") - .where({ id: item.product_id }) - .decrement("inventory", item.quantity); - } - - return order; - }); - } - - // Order status updates - updateOrderStatus(orderId: number, status: Order["status"]) { - return this.db.transaction(() => { - const order = this.db.table("orders") - .where({ id: orderId }) - .first(); - - if (!order) { - throw new Error("Order not found"); - } - - // Business logic for status transitions - const validTransitions: Record = { - pending: ["paid", "cancelled"], - paid: ["shipped", "cancelled"], - shipped: ["delivered"], - delivered: [], - cancelled: [] - }; - - if (!validTransitions[order.status]?.includes(status)) { - throw new Error(`Cannot transition from ${order.status} to ${status}`); - } - - // If cancelling, restore inventory - if (status === "cancelled" && order.status !== "cancelled") { - const orderItems = this.db.table("order_items") - .where({ order_id: orderId }) - .all(); - - for (const item of orderItems) { - this.db.table("products") - .where({ id: item.product_id }) - .increment("inventory", item.quantity); - } - } - - return this.db.table("orders") - .where({ id: orderId }) - .updateAndGet({ status, updated_at: Math.floor(Date.now() / 1000) }); - }); - } - - // Analytics and reporting - getOrderStatistics(dateRange?: { start: number; end: number }) { - let query = this.db.table("orders"); - - if (dateRange) { - query = query.whereBetween("created_at", dateRange.start, dateRange.end); - } - - const orders = query.all(); - - const stats = { - total_orders: orders.length, - total_revenue: orders.reduce((sum, order) => sum + order.total, 0), - avg_order_value: 0, - status_breakdown: {} as Record - }; - - stats.avg_order_value = stats.total_revenue / stats.total_orders || 0; - - for (const order of orders) { - stats.status_breakdown[order.status] = - (stats.status_breakdown[order.status] || 0) + 1; - } - - return stats; - } - - // Low inventory alerts - getLowInventoryProducts(threshold: number = 10) { - return this.db.table("products", { - jsonColumns: ["metadata"] - }) - .where({ active: true }) - .whereOp("inventory", "<=", threshold) - .orderBy("inventory") - .all(); - } -} - -// Usage example -const ecommerceDb = new DB("ecommerce.db", { - pragmas: [ - ["journal_mode", "WAL"], - ["foreign_keys", "ON"], - ["synchronous", "NORMAL"] - ] -}); - -setupEcommerceSchema(ecommerceDb); - -const ecommerce = new EcommerceService(ecommerceDb); - -// Create an order -const newOrder = ecommerce.createOrder({ - customer_id: 123, - items: [ - { product_id: 1, quantity: 2 }, - { product_id: 2, quantity: 1 } - ], - shipping_cost: 9.99, - tax_rate: 0.08 -}); - -// Search products -const laptops = ecommerce.searchProducts("laptop", { - category: 1, - minPrice: 500, - maxPrice: 2000, - limit: 10 -}); - -// Update order status -ecommerce.updateOrderStatus(newOrder.id!, "paid"); - -// Get analytics -const monthlyStats = ecommerce.getOrderStatistics({ - start: Date.now() - (30 * 24 * 60 * 60 * 1000), - end: Date.now() -}); -``` - -### User Management System - -```typescript -interface User { - id?: number; - username: string; - email: string; - password_hash: string; - first_name: string; - last_name: string; - role: "admin" | "moderator" | "user"; - active: boolean; - email_verified: boolean; - last_login?: number; - login_count: number; - preferences?: { - theme: "light" | "dark"; - language: string; - notifications: { - email: boolean; - push: boolean; - sms: boolean; - }; - }; - created_at?: number; - updated_at?: number; - deleted_at?: number; -} - -interface Session { - id?: string; - user_id: number; - token_hash: string; - expires_at: number; - ip_address?: string; - user_agent?: string; - created_at?: number; -} - -class UserManagementSystem { - constructor(private db: DB) { - this.setupSchema(); - } - - private setupSchema() { - // Users table - this.db.createTable("users", { - id: column.id(), - username: column.varchar(50, { unique: true, notNull: true }), - email: column.varchar(255, { unique: true, notNull: true }), - password_hash: column.varchar(255, { notNull: true }), - first_name: column.varchar(100, { notNull: true }), - last_name: column.varchar(100, { notNull: true }), - role: column.enum(["admin", "moderator", "user"], { default: "user" }), - active: column.boolean({ default: true }), - email_verified: column.boolean({ default: false }), - last_login: column.timestamp(), - login_count: column.integer({ default: 0 }), - preferences: column.json({ validateJson: true }), - - // Full name generated column - full_name: { - type: "TEXT", - generated: { - expression: "first_name || ' ' || last_name", - stored: false - } - }, - - created_at: column.createdAt(), - updated_at: column.updatedAt(), - deleted_at: column.timestamp() - }); - - // Sessions table - this.db.createTable("sessions", { - id: column.uuid({ generateDefault: true }), - user_id: column.foreignKey("users", "id", { onDelete: "CASCADE" }), - token_hash: column.varchar(255, { notNull: true, unique: true }), - expires_at: column.timestamp({ notNull: true }), - ip_address: column.varchar(45), // IPv6 compatible - user_agent: column.text(), - created_at: column.createdAt() - }); - - // Audit log table - this.db.createTable("user_audit_log", { - id: column.id(), - user_id: column.foreignKey("users", "id", { onDelete: "CASCADE" }), - action: column.varchar(100, { notNull: true }), - details: column.json(), - ip_address: column.varchar(45), - created_at: column.createdAt() - }); - - // Indexes - this.db.createIndex("idx_users_email", "users", "email", { unique: true }); - this.db.createIndex("idx_users_username", "users", "username", { unique: true }); - this.db.createIndex("idx_users_active", "users", "active"); - this.db.createIndex("idx_sessions_user", "sessions", "user_id"); - this.db.createIndex("idx_sessions_expires", "sessions", "expires_at"); - this.db.createIndex("idx_audit_user_action", "user_audit_log", ["user_id", "action"]); - } - - // User CRUD operations - createUser(userData: Omit) { - return this.db.transaction(() => { - const user = this.db.table("users", { - jsonColumns: ["preferences"] - }).insertAndGet(userData); - - if (user) { - this.logAction(user.id!, "user_created", { - username: userData.username, - email: userData.email - }); - } - - return user; - }); - } - - getUserById(id: number): User | null { - return this.db.table("users", { - jsonColumns: ["preferences"] - }) - .where({ id }) - .whereNull("deleted_at") - .first(); - } - - getUserByEmail(email: string): User | null { - return this.db.table("users", { - jsonColumns: ["preferences"] - }) - .where({ email }) - .whereNull("deleted_at") - .first(); - } - - getUserByUsername(username: string): User | null { - return this.db.table("users", { - jsonColumns: ["preferences"] - }) - .where({ username }) - .whereNull("deleted_at") - .first(); - } - - updateUser(id: number, updates: Partial) { - return this.db.transaction(() => { - const result = this.db.table("users", { - jsonColumns: ["preferences"] - }) - .where({ id }) - .whereNull("deleted_at") - .update({ - ...updates, - updated_at: Math.floor(Date.now() / 1000) - }); - - if (result.changes > 0) { - this.logAction(id, "user_updated", updates); - } - - return result; - }); - } - - // Soft delete - deleteUser(id: number, deletedBy?: number) { - return this.db.transaction(() => { - const now = Math.floor(Date.now() / 1000); - - const result = this.db.table("users") - .where({ id }) - .whereNull("deleted_at") - .update({ - deleted_at: now, - active: false, - updated_at: now - }); - - if (result.changes > 0) { - // Invalidate all sessions - this.db.table("sessions") - .where({ user_id: id }) - .delete(); - - this.logAction(id, "user_deleted", { deleted_by: deletedBy }); - } - - return result; - }); - } - - // Authentication - login(identifier: string, passwordHash: string, sessionData: { - ip_address?: string; - user_agent?: string; - expires_in?: number; // seconds - }) { - return this.db.transaction(() => { - // Find user by email or username - const user = this.db.table("users", { - jsonColumns: ["preferences"] - }) - .whereNull("deleted_at") - .where({ active: true }) - .whereRaw("(email = ? OR username = ?)", [identifier, identifier]) - .first(); - - if (!user || user.password_hash !== passwordHash) { - this.logAction(user?.id, "login_failed", { - identifier, - ip_address: sessionData.ip_address - }); - return null; - } - - // Update login info - this.db.table("users") - .where({ id: user.id! }) - .update({ - last_login: Math.floor(Date.now() / 1000), - login_count: user.login_count + 1, - updated_at: Math.floor(Date.now() / 1000) - }); - - // Create session - const expiresIn = sessionData.expires_in || (24 * 60 * 60); // 24 hours default - const sessionToken = crypto.randomUUID(); - const tokenHash = await this.hashToken(sessionToken); - - const session = this.db.table("sessions").insertAndGet({ - user_id: user.id!, - token_hash: tokenHash, - expires_at: Math.floor(Date.now() / 1000) + expiresIn, - ip_address: sessionData.ip_address, - user_agent: sessionData.user_agent - }); - - this.logAction(user.id!, "login_success", { - ip_address: sessionData.ip_address - }); - - return { - user: { ...user, password_hash: undefined }, // Don't return password hash - session, - token: sessionToken - }; - }); - } - - logout(sessionId: string, userId?: number) { - return this.db.transaction(() => { - const result = this.db.table("sessions") - .where({ id: sessionId }) - .delete(); - - if (result.changes > 0 && userId) { - this.logAction(userId, "logout", { session_id: sessionId }); - } - - return result; - }); - } - - validateSession(token: string): { user: User; session: Session } | null { - const tokenHash = this.hashToken(token); - - const session = this.db.table("sessions") - .where({ token_hash: tokenHash }) - .whereOp("expires_at", ">", Math.floor(Date.now() / 1000)) - .first(); - - if (!session) { - return null; - } - - const user = this.getUserById(session.user_id); - if (!user || !user.active) { - // Clean up invalid session - this.db.table("sessions") - .where({ id: session.id! }) - .delete(); - return null; - } - - return { user, session }; - } - - // Session management - getUserSessions(userId: number) { - return this.db.table("sessions") - .where({ user_id: userId }) - .whereOp("expires_at", ">", Math.floor(Date.now() / 1000)) - .orderBy("created_at").desc() - .all(); - } - - revokeAllSessions(userId: number) { - return this.db.transaction(() => { - const result = this.db.table("sessions") - .where({ user_id: userId }) - .delete(); - - if (result.changes > 0) { - this.logAction(userId, "sessions_revoked", { count: result.changes }); - } - - return result; - }); - } - - // Cleanup expired sessions - cleanupExpiredSessions() { - return this.db.table("sessions") - .whereOp("expires_at", "<=", Math.floor(Date.now() / 1000)) - .delete(); - } +### Bulk Operations - // User queries - searchUsers(query: string, filters: { - role?: User["role"]; - active?: boolean; - email_verified?: boolean; - limit?: number; - offset?: number; - } = {}) { - let queryBuilder = this.db.table("users", { - jsonColumns: ["preferences"] - }) - .whereNull("deleted_at") - .whereRaw("(username LIKE ? OR email LIKE ? OR first_name LIKE ? OR last_name LIKE ?)", [ - `%${query}%`, `%${query}%`, `%${query}%`, `%${query}%` - ]); - - if (filters.role) { - queryBuilder = queryBuilder.where({ role: filters.role }); - } - - if (filters.active !== undefined) { - queryBuilder = queryBuilder.where({ active: filters.active }); - } - - if (filters.email_verified !== undefined) { - queryBuilder = queryBuilder.where({ email_verified: filters.email_verified }); - } - - return queryBuilder - .select(["id", "username", "email", "first_name", "last_name", "role", "active", "email_verified", "last_login", "created_at"]) - .orderBy("created_at").desc() - .limit(filters.limit || 50) - .offset(filters.offset || 0) - .all(); - } - - getUserStatistics() { - const users = this.db.table("users") - .whereNull("deleted_at") - .all(); - - const now = Math.floor(Date.now() / 1000); - const dayAgo = now - (24 * 60 * 60); - const weekAgo = now - (7 * 24 * 60 * 60); - const monthAgo = now - (30 * 24 * 60 * 60); - - return { - total: users.length, - active: users.filter(u => u.active).length, - verified: users.filter(u => u.email_verified).length, - by_role: users.reduce((acc, user) => { - acc[user.role] = (acc[user.role] || 0) + 1; - return acc; - }, {} as Record), - recent_logins: { - last_24h: users.filter(u => u.last_login && u.last_login >= dayAgo).length, - last_week: users.filter(u => u.last_login && u.last_login >= weekAgo).length, - last_month: users.filter(u => u.last_login && u.last_login >= monthAgo).length - }, - registrations: { - last_24h: users.filter(u => u.created_at && u.created_at >= dayAgo).length, - last_week: users.filter(u => u.created_at && u.created_at >= weekAgo).length, - last_month: users.filter(u => u.created_at && u.created_at >= monthAgo).length - } - }; - } - - // Audit logging - private logAction(userId: number | undefined, action: string, details: any = {}, ipAddress?: string) { - if (!userId) return; - - this.db.table("user_audit_log").insert({ - user_id: userId, - action, - details, - ip_address: ipAddress - }); - } - - private async hashToken(token: string): Promise { - // In a real implementation, use a proper hashing function like bcrypt - return token; // Simplified for example - } - - getUserAuditLog(userId: number, limit: number = 50) { - return this.db.table("user_audit_log") - .where({ user_id: userId }) - .orderBy("created_at").desc() - .limit(limit) - .all(); - } -} - -// Usage example -const userSystem = new UserManagementSystem(new DB("users.db")); - -const newUser = userSystem.createUser({ - username: "johndoe", - email: "john@example.com", - password_hash: "hashed_password", - first_name: "John", - last_name: "Doe", - role: "user", - active: true, - email_verified: false -}); -``` - -## Migration Patterns - -### Schema Evolution +Use transactions for bulk operations: ```typescript -// Migration system -class MigrationRunner { - private db: DB; - - constructor(db: DB) { - this.db = db; - this.setupMigrationsTable(); - } - - private setupMigrationsTable() { - this.db.createTable("migrations", { - id: column.id(), - name: column.text({ unique: true, notNull: true }), - executed_at: column.createdAt() - }, { ifNotExists: true }); - } - - async runMigration(name: string, migration: () => void | Promise) { - const existing = this.db.table("migrations") - .where({ name }) - .first(); - - if (existing) { - console.log(`Migration ${name} already executed`); - return; - } - - console.log(`Running migration: ${name}`); - - try { - await this.db.transaction(async () => { - await migration(); - - this.db.table("migrations").insert({ - name, - executed_at: Math.floor(Date.now() / 1000) - }); - }); - - console.log(`Migration ${name} completed successfully`); - } catch (error) { - console.error(`Migration ${name} failed:`, error); - throw error; - } - } - - getExecutedMigrations() { - return this.db.table("migrations") - .orderBy("executed_at") - .all(); - } -} - -// Example migrations -const migrations = new MigrationRunner(db); - -// Add new column -migrations.runMigration("add_user_phone", () => { - db.exec("ALTER TABLE users ADD COLUMN phone TEXT"); - db.createIndex("idx_users_phone", "users", "phone"); -}); - -// Create new table -migrations.runMigration("create_notifications", () => { - db.createTable("notifications", { - id: column.id(), - user_id: column.foreignKey("users", "id", { onDelete: "CASCADE" }), - title: column.text({ notNull: true }), - message: column.text({ notNull: true }), - type: column.enum(["info", "warning", "error", "success"]), - read: column.boolean({ default: false }), - created_at: column.createdAt() - }); -}); - -// Data migration -migrations.runMigration("migrate_user_names", () => { - const users = db.table("users").all(); - - for (const user of users) { - if (user.name && !user.first_name && !user.last_name) { - const [first, ...lastParts] = user.name.split(" "); - - db.table("users") - .where({ id: user.id }) - .update({ - first_name: first || "", - last_name: lastParts.join(" ") || "" - }); - } - } -}); -``` - -## Performance Best Practices - -### Query Optimization - -```typescript -// Good practices for high-performance queries -class PerformanceOptimizer { - private db: DB; - - constructor(db: DB) { - this.db = db; - this.setupOptimalSettings(); - } - - private setupOptimalSettings() { - // WAL mode for concurrent access - this.db.pragma("journal_mode", "WAL"); - - // Optimize for mixed read/write workloads - this.db.pragma("synchronous", "NORMAL"); - - // Increase cache size (64MB) - this.db.pragma("cache_size", -64000); - - // Use memory for temp storage - this.db.pragma("temp_store", "MEMORY"); - - // Enable memory-mapped I/O - this.db.pragma("mmap_size", 268435456); // 256MB - - // Optimize query planner - this.db.pragma("optimize"); - } - - // Create optimal indexes - createOptimalIndexes() { - // Covering indexes include all columns needed for the query - this.db.createIndex("idx_users_active_cover", "users", - ["active", "created_at", "id", "email"], - { where: "deleted_at IS NULL" } - ); - - // Partial indexes for common filters - this.db.createIndex("idx_orders_pending", "orders", - ["created_at"], - { where: "status = 'pending'" } - ); - - // Composite indexes for multi-column sorts - this.db.createIndex("idx_products_category_price", "products", - ["category_id", "price", "active"] - ); - } - - // Efficient pagination - paginateEfficiently( - table: string, - page: number, - pageSize: number, - orderBy: string = "id" - ) { - // Use cursor-based pagination for better performance - const offset = (page - 1) * pageSize; - - return this.db.table(table) - .orderBy(orderBy as keyof T) - .limit(pageSize + 1) // Get one extra to check if there's a next page - .offset(offset) - .all(); - } - - // Batch operations for bulk data - bulkInsertOptimized( - table: string, - records: Partial[], - batchSize: number = 1000 - ) { - const originalSync = this.db.pragma("synchronous"); - - try { - // Temporarily disable sync for bulk operations - this.db.pragma("synchronous", "OFF"); - - for (let i = 0; i < records.length; i += batchSize) { - const batch = records.slice(i, i + batchSize); - - this.db.transaction(() => { - this.db.table(table).insertBatch(batch); - }); - } - } finally { - // Restore original sync setting - this.db.pragma("synchronous", originalSync); - - // Update query planner statistics - this.db.analyze(); - } - } - - // Connection pooling simulation for high-concurrency scenarios - createConnectionPool(dbPath: string, poolSize: number = 5) { - const connections: DB[] = []; - - for (let i = 0; i < poolSize; i++) { - const conn = new DB(dbPath, { - pragmas: [ - ["journal_mode", "WAL"], - ["synchronous", "NORMAL"], - ["cache_size", -32000], // Smaller cache per connection - ["busy_timeout", 5000] // Wait up to 5 seconds for locks - ] - }); - connections.push(conn); - } - - return { - getConnection(): DB { - // Simple round-robin (in production, use proper pooling) - return connections[Math.floor(Math.random() * connections.length)]; - }, - - closeAll() { - connections.forEach(conn => conn.close()); - } - }; - } +// Slow: each insert is a separate transaction +for (const user of userList) { + users.insert(user); } -``` - -## Testing and Debugging - -### Test Utilities - -```typescript -// Testing helper utilities -class TestDatabase { - private db: DB; - private originalTables: string[] = []; - - constructor() { - this.db = new DB(":memory:"); // In-memory for fast tests - } - - async setup(schema: () => void) { - // Record original state - this.originalTables = this.getTableNames(); - - // Apply schema - schema(); - - return this.db; - } - async cleanup() { - // Drop all tables created during test - const currentTables = this.getTableNames(); - const newTables = currentTables.filter(t => - !this.originalTables.includes(t) && t !== "migrations" - ); - - for (const table of newTables) { - this.db.dropTable(table, { ifExists: true }); - } +// Fast: all inserts in one transaction +db.transaction(() => { + for (const user of userList) { + users.insert(user); } - - async reset() { - // Clear all data but keep schema - const tables = this.getTableNames(); - - for (const table of tables) { - if (table !== "sqlite_master" && table !== "migrations") { - this.db.table(table).truncate(); - } - } - } - - private getTableNames(): string[] { - return this.db.getSchema() - .filter(item => item.type === "table") - .map(item => item.name); - } - - // Create test data factories - createUserFactory() { - let counter = 0; - - return (overrides: Partial = {}) => { - counter++; - return { - username: `user${counter}`, - email: `user${counter}@test.com`, - first_name: `First${counter}`, - last_name: `Last${counter}`, - role: "user" as const, - active: true, - email_verified: true, - login_count: 0, - ...overrides - }; - }; - } - - // Seed test data - async seedTestData() { - const userFactory = this.createUserFactory(); - - const users = this.db.table("users"); - - // Create test users - const testUsers = [ - userFactory({ role: "admin", username: "admin" }), - userFactory({ role: "moderator", username: "mod" }), - userFactory({ active: false, username: "inactive" }), - ...Array.from({ length: 10 }, () => userFactory()) - ]; - - users.insertBatch(testUsers); - - return { - adminUser: users.where({ username: "admin" }).first(), - moderatorUser: users.where({ username: "mod" }).first(), - inactiveUser: users.where({ username: "inactive" }).first(), - regularUsers: users.where({ role: "user", active: true }).all() - }; - } - - getDb() { - return this.db; - } -} - -// Example test suite -describe("User Management", () => { - let testDb: TestDatabase; - let userSystem: UserManagementSystem; - - beforeEach(async () => { - testDb = new TestDatabase(); - const db = await testDb.setup(() => { - // Schema setup would go here - }); - - userSystem = new UserManagementSystem(db); - await testDb.seedTestData(); - }); - - afterEach(async () => { - await testDb.cleanup(); - }); - - test("should create user successfully", async () => { - const userData = { - username: "newuser", - email: "new@test.com", - password_hash: "hashed", - first_name: "New", - last_name: "User", - role: "user" as const, - active: true, - email_verified: false - }; - - const user = userSystem.createUser(userData); - expect(user).toBeTruthy(); - expect(user?.username).toBe("newuser"); - expect(user?.email).toBe("new@test.com"); - }); - - test("should handle duplicate email", async () => { - const userData = { - username: "duplicate", - email: "user1@test.com", // Existing email - password_hash: "hashed", - first_name: "Dupe", - last_name: "User", - role: "user" as const, - active: true, - email_verified: false - }; - - expect(() => userSystem.createUser(userData)).toThrow(); - }); }); -``` - -## FAQ and Troubleshooting - -### Common Issues - -**Q: Why am I getting "UPDATE operation requires at least one WHERE condition"?** -A: This is a safety feature to prevent accidental full-table updates. Always add a WHERE clause: - -```typescript -// ❌ This will throw an error -users.update({ active: false }); - -// ✅ Correct usage -users.where({ role: "temp" }).update({ active: false }); - -// ✅ For intentional full-table updates -users.whereRaw("1 = 1").update({ migrated: true }); +// Or use insertMany +users.insertMany(userList); ``` -**Q: How do I handle large datasets without running out of memory?** - -A: Use pagination and process data in chunks: - -```typescript -// Process large tables in batches -function processLargeTable(tableName: string, processor: (row: any) => void) { - const BATCH_SIZE = 1000; - let offset = 0; - let batch: any[]; - - do { - batch = db.table(tableName) - .limit(BATCH_SIZE) - .offset(offset) - .all(); - - batch.forEach(processor); - offset += BATCH_SIZE; - } while (batch.length === BATCH_SIZE); -} -``` - -**Q: Why are my regex conditions slow?** - -A: Regex conditions are applied client-side after SQL filtering. Always use SQL WHERE conditions first to reduce the dataset: - -```typescript -// ❌ Slow - regex on entire table -users.whereRgx({ email: /@gmail\.com$/ }).all(); - -// ✅ Fast - SQL filter first, then regex -users - .where({ active: true }) - .whereRaw("email LIKE '%@gmail.com'") - .whereRgx({ email: /@gmail\.com$/ }) - .all(); -``` +## API Reference -**Q: How do I handle database migrations safely?** +### DB Class -A: Always use transactions and backup before migrations: +| Method | Description | +|----|----| +| `new DB(path, options?)` | Create database connection | +| `createTable(name, schema, options?)` | Create table and return QueryBuilder | +| `transaction(fn: () => T)` | Execute function in transaction | +| `exec(sql, params?)` | Execute raw SQL | +| `getSchema()` | Get database schema | +| `close()` | Close database connection | + +### QueryBuilder Methods + +| Method | Description | +|----|----| +| `select(columns)` | Start select query | +| `insert(data)` | Insert single row | +| `insertMany(data[])` | Insert multiple rows | +| `update(data)` | Start update query | +| `delete()` | Start delete query | +| `where(conditions)` | Add WHERE clause | +| `and(conditions)` | Add AND condition | +| `or(conditions)` | Add OR condition | +| `orderBy(column)` | Add ORDER BY | +| `.asc()` / `.desc()` | Set order direction | +| `limit(n)` | Limit results | +| `offset(n)` | Offset results | +| `all()` | Execute and return all rows | +| `first()` | Execute and return first row | +| `run()` | Execute and return result info | + +### Column Helpers + +| Method | SQLite Type | Description | +|----|----|----| +| `column.id()` | INTEGER PRIMARY KEY | Auto-increment ID | +| `column.text(opts?)` | TEXT | String column | +| `column.integer(opts?)` | INTEGER | Integer column | +| `column.real(opts?)` | REAL | Float column | +| `column.boolean(opts?)` | INTEGER | Boolean (0/1) | +| `column.json(opts?)` | TEXT | JSON serialized | +| `column.blob(opts?)` | BLOB | Binary data | +| `column.createdAt()` | TEXT | Auto timestamp | +| `column.generated(expr, type)` | varies | Generated column | + +## Integration with DockStat + +This package is used throughout DockStat for data persistence: ```typescript -// Safe migration pattern -function safeMigration() { - // Backup first (in production) - db.exec("VACUUM INTO 'backup.db'"); - - try { - db.transaction(() => { - // Migration steps - db.createTable("new_table", schema); - db.exec("INSERT INTO new_table SELECT * FROM old_table"); - db.dropTable("old_table"); - db.exec("ALTER TABLE new_table RENAME TO old_table"); - }); - } catch (error) { - console.error("Migration failed:", error); - // Restore from backup if needed - throw error; - } -} -``` - -**Q: Can I use this library with Node.js?** - -A: No, this library is specifically designed for Bun's `bun:sqlite`. For Node.js, consider using `better-sqlite3` or similar libraries. - -**Q: How do I optimize for high concurrency?** +import { DB, column } from "@dockstat/sqlite-wrapper"; +import DockStatDB from "@dockstat/db"; +import DockerClient from "@dockstat/docker-client"; -A: Use WAL mode and optimize your pragmas: +// DockStatDB uses sqlite-wrapper internally +const dockstatDb = new DockStatDB(); -```typescript -const db = new DB("app.db", { - pragmas: [ - ["journal_mode", "WAL"], // Enable concurrent reads - ["synchronous", "NORMAL"], // Balance safety and speed - ["busy_timeout", 5000], // Wait for locks - ["cache_size", -64000], // 64MB cache - ["wal_autocheckpoint", 1000] // Checkpoint every 1000 pages - ] +// Share the underlying DB with DockerClient +const dockerClient = new DockerClient(dockstatDb.getDB(), { + enableMonitoring: true }); ``` -## License and Contributing - -This library is released under the Mozilla Public License 2.0 (MPL-2.0). - -### Contributing - -We welcome contributions! Please: - - -1. Fork the repository -2. Create a feature branch -3. Add tests for new functionality -4. Ensure all tests pass -5. Submit a pull request - -### Performance Contributions - -For performance-related PRs, please include: +## Related Packages -* Benchmark scripts showing before/after performance -* Clear description of the optimization -* Test coverage for the changes +* `@dockstat/db` - Database layer built on sqlite-wrapper +* `@dockstat/docker-client` - Uses sqlite-wrapper for persistence +* `@dockstat/plugin-handler` - Plugin database tables +* `@dockstat/typings` - Type definitions -### Reporting Issues +## License -When reporting issues, please include: +MPL-2.0 — Part of the DockStat project. -* Bun version -* Library version -* Minimal reproduction code -* Expected vs actual behavior -* Database schema (if relevant) +## Contributing -For more information and updates, visit the [project homepage](https://outline.itsnik.de/s/9d88c471-373e-4ef2-a955-b1058eb7dc99/doc/dockstatsqlite-wrapper-Lxt4IphXI5). \ No newline at end of file +Issues and PRs welcome at [github.com/Its4Nik/DockStat](https://github.com/Its4Nik/DockStat) \ No newline at end of file diff --git a/apps/docs/dockstat/packages/@dockstat-theme-handler/README.md b/apps/docs/dockstat/packages/@dockstat-theme-handler/README.md deleted file mode 100644 index d22a4e3a..00000000 --- a/apps/docs/dockstat/packages/@dockstat-theme-handler/README.md +++ /dev/null @@ -1,882 +0,0 @@ ---- -id: b4148ac3-5b60-4223-aa0d-48111649b91f -title: "@dockstat/theme-handler" -collectionId: b4a5e48f-f103-480b-9f50-8f53f515cab9 -parentDocumentId: 75d80211-7262-4064-aaa6-2ead20e17f43 -updatedAt: 2025-08-24T18:23:52.182Z -urlId: zqe3ITb7jc ---- - -## Overview - -The theme-handler is a comprehensive theme management system for React applications that automatically converts theme configurations into CSS custom properties, enabling seamless theme switching with TypeScript support. - -## Architecture - -### Core Components - -``` -@dockstat/theme-handler/ -├── index.ts # ThemeHandler database class (used by DockStat) -├── src/ -│ ├── index.ts # Main exports -│ ├── context.tsx # React Context & base hook -│ ├── ThemeProvider.tsx # Main provider component -│ ├── hooks.ts # Utility hooks -│ ├── cssVariableParser.ts # CSS variable generation -│ └── ThemeLoadingOverlay.tsx # Loading UI -``` - -## API Reference - -### ThemeProvider - -The main component that provides theme context to your application. - -```tsx -interface ThemeProviderProps { - children: React.ReactNode; - - // Theme Configuration - initialThemeName?: string; // Default: "default" - initialTheme?: THEME.THEME_config; // Preloaded theme object - fallbackThemeName?: string; // Default: "default" - - // Data Sources - themeHandler?: ThemeHandler; // Database handler instance - apiEndpoint?: string; // API base URL (e.g., "/api") - apiHeaders?: Record; // Additional headers - - // CSS Variable Configuration - cssParserConfig?: Partial; - - // UI Configuration - showLoadingOverlay?: boolean; // Default: true - customLoadingContent?: React.ReactNode; - customErrorContent?: React.ReactNode; - - // Event Handlers - onThemeChange?: (name: string, theme: THEME.THEME_config | null) => void; - onThemeLoadError?: (error: Error, themeName: string) => void; - onThemeLoaded?: (themeName: string, theme: THEME.THEME_config) => void; - - // Retry Configuration - retryAttempts?: number; // Default: 3 - retryDelay?: number; // Default: 1000ms -} -``` - -#### Usage Examples - -**API-based themes:** - -```tsx - console.log('Theme changed:', name)} -> - - -``` - -**Database-based themes:** - -```tsx -import DB from "@dockstat/sqlite-wrapper" -const db = new DB('themes.sqlite'); -const themeHandler = new ThemeHandler(db); - - - - -``` - -**Preloaded theme:** - -```tsx - - - -``` - -### Core Hooks - -#### useTheme() - -Returns the complete theme context. - -```tsx -interface ThemeContextType { - // Current State - theme: THEME.THEME_config | null; // Current theme object - themeName: string; // Current theme name - themeVars: Record; // Generated CSS variables - - // Loading States - isLoading: boolean; // Loading indicator - isThemeLoaded: boolean; // Theme ready state - error: string | null; // Error message - - // Available Data - availableThemes: string[]; // List of theme names - - // Actions - setThemeName: (name: string) => void; - refreshTheme: () => Promise; -} - -// Usage - -const { theme, themeName, themeVars, isThemeLoaded, error } = useTheme(); -``` - -#### useThemeSwitch() - -Provides theme switching functionality with loading states. - -```tsx -interface ThemeSwitchHook { - switchTheme: (themeName: string) => Promise; - isSwitching: boolean; // Currently switching - switchingTo: string | null; // Target theme name - error: string | null; // Switch error -} - -// Usage - -const { switchTheme, isSwitching, switchingTo } = useThemeSwitch(); - -await switchTheme('dark'); -``` - -### Utility Hooks - -#### useThemeVariable() - -Get a specific CSS variable value with fallback. - -```tsx -function useThemeVariable( - variableName: string, - fallback?: string -): string | undefined - -// Usage - -const accentColor = useThemeVariable('theme-components-card-accent', '#007bff'); -const bgColor = useThemeVariable('--theme-background-effect-solid-color'); -``` - -#### useThemeVariables() - -Get multiple CSS variables at once. - -```tsx -function useThemeVariables(variableNames: string[]): Record - -// Usage - -const vars = useThemeVariables([ - 'theme-components-card-accent', - 'theme-components-card-border-color' -]); -// Returns: { 'theme-components-card-accent': '#007bff', ... } -``` - -#### useComponentTheme() - -Get theme configuration for a specific component. - -```tsx -function useComponentTheme( - componentName: K -): THEME.THEME_components[K] | null - -// Usage - -const cardStyles = useComponentTheme('Card'); -// Returns: { accent: '#007bff', border: true, title: { color: '#333', ... }, ... } -``` - -#### useThemePersistence() - -Automatically save/restore theme selection from localStorage. - -```tsx -function useThemePersistence(storageKey?: string): { - clearPersistedTheme: () => void; -} - -// Usage - -useThemePersistence('my-app-theme'); // Auto-saves theme changes -``` - -#### useSystemTheme() - -Detect system dark/light mode preference. - -```tsx -function useSystemTheme(): 'light' | 'dark' - -// Usage - -const systemPreference = useSystemTheme(); -``` - -#### useThemeHealthCheck() - -Monitor theme validity and detect issues. - -```tsx -interface ThemeHealthStatus { - isHealthy: boolean; - issues: string[]; // Critical problems - warnings: string[]; // Non-critical issues -} - -function useThemeHealthCheck(): ThemeHealthStatus - -// Usage - -const { isHealthy, issues, warnings } = useThemeHealthCheck(); -``` - -#### useCustomCSSProperties() - -Map theme variables to custom CSS property names. - -```tsx -function useCustomCSSProperties( - mapping: Record, - prefix?: string -): void - -// Usage - -useCustomCSSProperties({ - 'primary': 'theme-components-card-accent', - 'background': 'theme-background-effect-solid-color' -}, '--my-component'); - -// Creates: --my-component-primary, --my-component-background -``` - -## CSS Variable Parser - -### Configuration Interface - -```tsx -interface CSSVariableParserConfig { - prefix: string; // CSS variable prefix - separator: string; // Path separator - transformKey?: (key: string, path: string[]) => string; - transformValue?: (value: unknown, key: string, path: string[]) => string; - shouldInclude?: (key: string, value: unknown, path: string[]) => boolean; -} -``` - -### Default Configuration - -```tsx -const defaultParserConfig: CSSVariableParserConfig = { - prefix: "--theme", - separator: "-", - transformKey: (key: string) => - key.toLowerCase().replace(/[A-Z_]/g, (match) => - match === "_" ? "-" : `-${match.toLowerCase()}` - ), - transformValue: (value: unknown) => { - if (typeof value === "string") return value; - if (typeof value === "number") return value.toString(); - if (typeof value === "boolean") return value ? "1" : "0"; - return String(value); - }, - shouldInclude: (_key: string, value: unknown) => { - return ( - typeof value === "string" || - typeof value === "number" || - typeof value === "boolean" - ); - }, -}; -``` - -### Predefined Configurations - -```tsx -import { parserConfigs } from '@dockstat/theme-handler'; - -// Standard (default) -parserConfigs.standard -// Output: --theme-components-card-accent - -// Compact - -parserConfigs.compact -// Output: --t-components-card-accent - -// Verbose - -parserConfigs.verbose -// Output: --dockstat-theme__components__card__accent - -// Components Only - -parserConfigs.componentsOnly -// Only includes theme.vars.components.* -``` - -### Custom Configuration Example - -```tsx - key.toUpperCase(), - shouldInclude: (key, value) => key !== 'internal_prop' - }} -> - - -``` - -## ThemeHandler (Database Integration) - -### Class Interface - -```tsx -class ThemeHandler { - constructor(DB: DB); - - // Theme Management - addTheme(theme: THEME.THEME_config): QueryResult; - getTheme(name: string): THEME.THEME_config | null; - getAllThemes(): THEME.THEME_config[]; - getThemeNames(): string[]; - deleteTheme(name: string): QueryResult; - updateTheme(name: string, updates: Partial): QueryResult; - - // Active Theme Management - getActiveTheme(): THEME.THEME_config | null; - setActiveTheme(name: string): QueryResult; - - // Utilities - themeExists(name: string): boolean; -} -``` - -### Database Schema - -```sql -CREATE TABLE themes ( - name TEXT PRIMARY KEY NOT NULL UNIQUE, - version TEXT NOT NULL, - creator TEXT NOT NULL, - license TEXT NOT NULL, - description TEXT NOT NULL, - active BOOLEAN NOT NULL DEFAULT 0, - vars TEXT NOT NULL -); -``` - -### Usage Example - -```tsx -import { DB } from '@dockstat/sqlite-wrapper'; -import { ThemeHandler } from '@dockstat/theme-handler/core'; - -const db = new DB('themes.sqlite'); -const themeHandler = new ThemeHandler(db); - -// Add themes - -themeHandler.addTheme({ - name: 'corporate', - version: '1.0.0', - creator: 'Design Team', - license: 'MIT', - description: 'Corporate theme', - active: false, - vars: { /* theme configuration */ } -}); - -// Get themes - -const theme = themeHandler.getTheme('corporate'); -const allThemes = themeHandler.getAllThemes(); -const themeNames = themeHandler.getThemeNames(); - -// Set active theme - -themeHandler.setActiveTheme('corporate'); -const activeTheme = themeHandler.getActiveTheme(); -``` - -## Theme Structure - -### Type Definitions - -```tsx -interface THEME_config { - name: string; - version: string; - creator: string; - license: string; - description: string; - active: boolean | 0 | 1; - vars: THEME_vars; -} - -interface THEME_vars { - background_effect: THEME_background_effects; - components: THEME_components; -} - -// Background Effects - -type THEME_background_effects = - | { Solid: { color: string } } - | { Gradient: { from: string; to: string; direction: string } }; - -// Component Themes - -interface THEME_components { - Card: { - accent: string; - border: boolean; - border_color: string; - border_size: number; - title: THEME_font_config; - sub_title: THEME_font_config; - content: THEME_font_config; - }; - // ... other components -} - -interface THEME_font_config { - font: string; - color: string; - font_size: number; - font_weight: number; -} -``` - -### Example Theme - -```tsx -const exampleTheme: THEME.THEME_config = { - name: "professional", - version: "1.2.0", - creator: "Design System Team", - license: "MIT", - description: "Professional dark theme with blue accents", - active: false, - vars: { - background_effect: { - Gradient: { - from: "#1a1a2e", - to: "#16213e", - direction: "tl-br" - } - }, - components: { - Card: { - accent: "#0066cc", - border: true, - border_color: "rgba(255, 255, 255, 0.1)", - border_size: 1, - title: { - font: "Inter, -apple-system, sans-serif", - color: "#ffffff", - font_size: 18, - font_weight: 600 - }, - sub_title: { - font: "Inter, -apple-system, sans-serif", - color: "rgba(255, 255, 255, 0.8)", - font_size: 14, - font_weight: 400 - }, - content: { - font: "Inter, -apple-system, sans-serif", - color: "rgba(255, 255, 255, 0.9)", - font_size: 13, - font_weight: 300 - } - } - } - } -}; -``` - -### Generated CSS Variables - -The above theme generates these CSS variables: - -```css -:root { - --theme-background-effect-gradient-from: #1a1a2e; - --theme-background-effect-gradient-to: #16213e; - --theme-background-effect-gradient-direction: tl-br; - --theme-components-card-accent: #0066cc; - --theme-components-card-border: 1; - --theme-components-card-border-color: rgba(255, 255, 255, 0.1); - --theme-components-card-border-size: 1; - --theme-components-card-title-font: Inter, -apple-system, sans-serif; - --theme-components-card-title-color: #ffffff; - --theme-components-card-title-font-size: 18; - --theme-components-card-title-font-weight: 600; - --theme-components-card-sub-title-font: Inter, -apple-system, sans-serif; - --theme-components-card-sub-title-color: rgba(255, 255, 255, 0.8); - --theme-components-card-sub-title-font-size: 14; - --theme-components-card-sub-title-font-weight: 400; - --theme-components-card-content-font: Inter, -apple-system, sans-serif; - --theme-components-card-content-color: rgba(255, 255, 255, 0.9); - --theme-components-card-content-font-size: 13; - --theme-components-card-content-font-weight: 300; -} -``` - -## API Integration - -### Required Endpoints - -Your API must provide these endpoints: - - -:::info -`{PREFIX}` is adjustable - -::: - -#### GET `/{PREFIX}/themes` - -Returns available theme names or theme objects. - -**Response Format:** - -```tsx -// Option 1: Array of names - -string[] - -// Option 2: Array of objects with name property -Array<{ name: string; [key: string]: any }> -``` - -**Example:** - -```json -["light", "dark", "high-contrast"] -``` - -#### GET `/{PREFIX}/themes/{name}` - -Returns a complete theme object. - -**Response Format:** - -```tsx -THEME.THEME_config -``` - -**Example:** - -```json -{ - "name": "dark", - "version": "1.0.0", - "creator": "Design Team", - "license": "MIT", - "description": "Dark theme", - "active": false, - "vars": { - "background_effect": { - "Solid": { "color": "#1a1a1a" } - }, - "components": { - "Card": { - "accent": "#007bff", - "border": true, - "border_color": "#333", - "border_size": 1, - "title": { - "font": "Arial, sans-serif", - "color": "#ffffff", - "font_size": 16, - "font_weight": 600 - }, - "sub_title": { - "font": "Arial, sans-serif", - "color": "#cccccc", - "font_size": 14, - "font_weight": 400 - }, - "content": { - "font": "Arial, sans-serif", - "color": "#eeeeee", - "font_size": 12, - "font_weight": 300 - } - } - } - } -} -``` - -### Error Handling - -The theme provider includes automatic error handling: - -* **Network errors**: Automatic retry with exponential back off -* **404 errors**: Falls back to `fallbackThemeName` -* **Parse errors**: Calls `onThemeLoadError` callback -* **Validation errors**: Logged to console, triggers health check warnings - -### Custom Headers - -```tsx - - - -``` - -## Performance Considerations - -### CSS Variable Management - -* CSS variables are applied/removed efficiently using `document.documentElement.style` -* Previous variables are cleaned up before applying new ones -* Variables persist across component unmounts to prevent flickering - -### Theme Loading - -* Themes are loaded asynchronously with loading states -* Failed requests trigger automatic retry with exponential backoff -* AbortController cancels previous requests when new ones start -* Available themes are cached after first load - -### Memory Management - -* Theme objects are stored in React context (single instance) -* CSS variable objects are recreated only when themes change -* Event listeners are properly cleaned up on unmount - -## Error Handling & Debugging - -### Error Types - -```tsx -// Loading errors - -onThemeLoadError={(error: Error, themeName: string) => { - console.error(`Failed to load theme "${themeName}":`, error); - // Handle error (show toast, fallback, etc.) -}} - -// Network errors -// Parse errors -// Validation errors -``` - -### Debug Mode - -Enable debugging by checking the browser console for theme-related logs: - -```tsx -// Theme changes - -console.log('Theme changed to: themeName', themeObject); - -// CSS variable generation - -console.log('Generated CSS variables:', variables); - -// Loading states - -console.log('Theme loading...', themeName); -console.log('Theme loaded successfully:', themeName); -``` - -### Health Monitoring - -```tsx -function ThemeMonitor() { - const { isHealthy, issues, warnings } = useThemeHealthCheck(); - - React.useEffect(() => { - if (!isHealthy) { - console.warn('Theme issues detected:', { issues, warnings }); - } - }, [isHealthy, issues, warnings]); - - return null; -} -``` - -## Migration Guide - -### From CSS-in-JS - -```tsx -// Before: styled-components/emotion - -const Card = styled.div` - background: ${props => props.theme.cardBg}; - color: ${props => props.theme.cardText}; -`; - -// After: CSS variables - -const Card = styled.div` - background: var(--theme-components-card-background); - color: var(--theme-components-card-title-color); -`; -``` - -### From Manual Theme Switching - -```tsx -// Before: Manual state management - -const [theme, setTheme] = useState('light'); -const handleThemeChange = (newTheme) => { - setTheme(newTheme); - document.body.className = `theme-${newTheme}`; -}; - -// After: Theme handler - -const { switchTheme } = useThemeSwitch(); -const handleThemeChange = (newTheme) => { - switchTheme(newTheme); // Automatic CSS variable updates -}; -``` - -## Testing - -### Unit Testing - -```tsx -import { renderHook } from '@testing-library/react-hooks'; -import { ThemeProvider, useTheme } from '@dockstat/theme-handler'; - -const wrapper = ({ children }) => ( - - {children} - -); - -test('useTheme returns theme data', () => { - const { result } = renderHook(() => useTheme(), { wrapper }); - - expect(result.current.theme).toEqual(mockTheme); - expect(result.current.isThemeLoaded).toBe(true); - expect(Object.keys(result.current.themeVars)).toHaveLength(17); -}); -``` - -### Integration Testing - -```tsx -import { render, screen, fireEvent, waitFor } from '@testing-library/react'; - -test('theme switching works', async () => { - render( - - - - ); - - fireEvent.click(screen.getByText('Switch to Dark')); - - await waitFor(() => { - expect(screen.getByText('Current: dark')).toBeInTheDocument(); - }); -}); -``` - -## Troubleshooting - -### Common Issues - -**Theme stuck on "Loading..."** - -* Check API endpoints are responding correctly -* Verify theme object structure matches `THEME.THEME_config` -* Check browser console for network/parsing errors - -**CSS variables not applying** - -* Ensure theme contains valid primitive values (string/number/boolean) -* Check `cssParserConfig.shouldInclude` isn't filtering out needed values -* Verify CSS variable names in your stylesheets match generated ones - -**Theme switching not working** - -* Make sure `availableThemes` contains the target theme name -* Check `onThemeLoadError` for loading failures -* Verify API endpoints return correct theme objects - -**Memory leaks** - -* ThemeProvider cleans up automatically on unmount -* Custom event listeners should use cleanup functions -* Avoid storing theme objects in component state - -### Performance Issues - -**Slow theme switching** - -* Enable loading overlays to improve perceived performance -* Preload frequently used themes -* Optimize theme object size by removing unused properties - -**CSS variable conflicts** - -* Use custom `prefix` in parser config to avoid naming collisions -* Consider `parserConfigs.verbose` for unique variable names -* Check for existing CSS variables in your codebase - -## Best Practices - -### Theme Design - - -1. **Keep themes consistent**: Use the same structure across all themes -2. **Use semantic naming**: Choose descriptive property names -3. **Limit nesting depth**: Avoid deeply nested theme objects -4. **Include fallbacks**: Provide default values for optional properties - -### Component Design - - -1. **Use CSS variables in stylesheets**: Avoid JavaScript-based styling when possible -2. **Implement loading states**: Handle theme loading gracefully -3. **Cache theme data**: Use React.memo for theme-dependent components -4. **Test with multiple themes**: Ensure components work across all themes - -### Performance - - -1. **Lazy load themes**: Load themes on demand rather than all at once -2. **Use theme persistence**: Save user preferences to avoid repeated loads -3. **Minimize theme objects**: Remove unused properties and nested objects -4. **Batch theme operations**: Group related theme changes together \ No newline at end of file diff --git a/apps/docs/dockstat/packages/@dockstat-theme-handler/flow/README.md b/apps/docs/dockstat/packages/@dockstat-theme-handler/flow/README.md deleted file mode 100644 index 5aaa6d63..00000000 --- a/apps/docs/dockstat/packages/@dockstat-theme-handler/flow/README.md +++ /dev/null @@ -1,135 +0,0 @@ ---- -id: c4955c6c-2184-493e-bb07-12757a457289 -title: Flow -collectionId: b4a5e48f-f103-480b-9f50-8f53f515cab9 -parentDocumentId: b4148ac3-5b60-4223-aa0d-48111649b91f -updatedAt: 2025-08-28T12:36:27.384Z -urlId: 9ndQsUblrd ---- - -# CSSVariableParser - -```mermaidjs -flowchart TB - - %% Entry Points - A["parseThemeVars(themeVars, config?)"] --> B["flattenThemeVars(obj, config, path)"] - A -->|Merges| AC[defaultParserConfig] - - %% flattenThemeVars process - B -->|Checks nested objects| B1{"value is object?"} - B1 -->|Yes| B2["Recurse flattenThemeVars"] - B1 -->|No| B3{"shouldInclude?"} - - B3 -->|No| Bskip[Skip value] - B3 -->|Yes| B4["transformKey + transformValue"] - - B4 --> B5[Build CSS Var Name] - B5 --> B6["Assign to result{}"] - - B2 --> B6 - B6 --> Bout["Return Record"] - - %% Usage - AC --> A - Bout --> C["applyCSSVariables(variables)"] - Bout --> D["removeCSSVariables(variables)"] - - %% Apply CSS Vars Flow - C --> C1[Iterate variables] - C1 --> C2["document.documentElement.style.setProperty"] - C2 --> C3["Verify via getComputedStyle"] - - %% Remove CSS Vars Flow - D --> D1[Iterate keys] - D1 --> D2["document.documentElement.style.removeProperty"] - - %% Parser Configs - subgraph Configurations - AC - E1[parserConfigs.standard] - E2[parserConfigs.compact] - E3[parserConfigs.verbose] - E4[parserConfigs.componentsOnly] - end - - AC -.base config.-> E1 - AC -.base config.-> E2 - AC -.base config.-> E3 - AC -.base config.-> E4 -``` - -# ThemeProvider.tsx - -```mermaidjs -flowchart TD - - %% Entry - A[ThemeProvider] --> S1["Initialize State + Refs"] - A --> M1["Merge CSS Parser Config (useMemo)"] - A --> L1["useLayoutEffect - Apply Initial Theme"] - A --> E1["useEffect - Initialize Theme + Load Available Themes"] - - %% Config - M1 --> M2{"themeNamespace?"} - M2 -->|"components"| C1[parserConfigs.componentsOnly] - M2 -->|"background"| C2[parserConfigs.standard] - M2 -->|"else"| C3[parserConfigs.standard] - M2 --> M3["Enable Tailwind Variables?"] - - %% Theme Loading - E1 --> LA[loadAvailableThemes] - E1 --> ST["setThemeName(initialThemeName)"] - - ST --> LT["loadTheme(name, attempt?)"] - LT -->|"ThemeHandler"| H1["themeHandler.getTheme(name)"] - LT -->|"API"| H2["fetch /themes/:name"] - LT -->|"Retry"| LT - LT -->|"Success"| AT["applyThemeVariables(themeConfig)"] - LT -->|"Fail"| Fallback["Try fallbackThemeName"] - - %% Apply Variables - AT --> RV["removeCSSVariables(currentCSSVars)"] - AT --> PV["parseThemeVars(themeConfig.vars, finalConfig)"] - PV --> AV["applyCSSVariables(cssVars)"] - AV --> UV["Update currentCSSVars + setThemeVars"] - - %% State Updates + Events - AT --> SU["Update state: theme, themeName, isThemeLoaded"] - SU --> EH["Call onThemeChange / onThemeLoaded"] - Fallback --> SU - LT -->|"Error"| EH2["onThemeLoadError"] - - %% Refresh - A --> R1[refreshTheme] --> ST - - %% Context - A --> P1[ThemeContext.Provider] - P1 --> O1[ThemeLoadingOverlay] - P1 --> Children[children] - - %% Subgraphs - subgraph "Hooks & Lifecycle" - L1 - E1 - R1 - end - - subgraph "Theme Ops" - LA - LT - AT - RV - PV - AV - end - - subgraph "State Mgmt" - SU - EH - EH2 - end -``` - - -\ \ No newline at end of file diff --git a/apps/docs/dockstat/packages/@dockstat-typings/README.md b/apps/docs/dockstat/packages/@dockstat-typings/README.md new file mode 100644 index 00000000..84975fc2 --- /dev/null +++ b/apps/docs/dockstat/packages/@dockstat-typings/README.md @@ -0,0 +1,518 @@ +--- +id: ecb9e07b-37e7-430a-b3fc-eb51515ab9ac +title: "@dockstat/typings" +collectionId: b4a5e48f-f103-480b-9f50-8f53f515cab9 +parentDocumentId: bbcefaa2-6bd4-46e8-ae4b-a6b823593e67 +updatedAt: 2025-12-16T19:05:51.049Z +urlId: 7Ae5a9YIKb +--- + +> Centralized TypeScript type definitions and Typebox schemas for the DockStat monorepo. + +## Overview + +`@dockstat/typings` provides shared type definitions, interfaces, and runtime validation schemas used across all DockStat packages. It ensures type consistency between frontend, backend, and packages. + +## Installation + +```bash +bun add @dockstat/typings +``` + +## Exports + +The package provides multiple export paths: + +```typescript +// Main exports (all types) +import { THEME, DATABASE, DOCKER, ADAPTER, HOTKEY, PLUGIN, EVENTS } from "@dockstat/typings" + +// Typebox schemas (runtime validation) +import { schemas } from "@dockstat/typings/schemas" + +// Typebox-derived types + +import { types } from "@dockstat/typings/types" +``` + +## Core Type Namespaces + +### THEME Types + +Theme system types for UI customization: + +```typescript +import type { THEME } from "@dockstat/typings" + +// Theme configuration + +type Config = THEME.THEME_config + +type Vars = THEME.THEME_vars + +type BackgroundEffect = THEME.THEME_background_effects + +type Components = THEME.THEME_components + +type FontConfig = THEME.THEME_font_config + +// Example usage + +const theme: THEME.THEME_config = { + name: "dark-blue", + version: "1.0.0", + creator: "DockStat", + license: "MIT", + description: "Dark blue theme", + active: true, + vars: { + background_effect: { + Gradient: { + from: "#1a1a2e", + to: "#16213e", + direction: "to bottom right" + } + }, + components: { + Card: { + accent: "#0f3460", + border: "1px solid #e94560", + // ... + } + } + } +} +``` + +### DATABASE Types + +Database schema types for SQLite tables: + +```typescript +import type { DATABASE } from "@dockstat/typings" + +// Plugin schema + +type PluginSchema = DATABASE.DBPluginShemaT + +type PluginInsert = DATABASE.DBPluginInsertT + +type PluginUpdate = DATABASE.DBPluginUpdateT + +// Theme schema + +type ThemeSchema = DATABASE.DBThemeSchemaT + +// Config schema + +type ConfigSchema = DATABASE.DBConfigSchemaT + +// Example + +const plugin: DATABASE.DBPluginShemaT = { + id: 1, + name: "my-plugin", + version: "1.0.0", + repository: "https://github.com/user/plugin", + manifest: "https://github.com/user/plugin/manifest.json", + author: { name: "Developer" }, + tags: ["monitoring"], + repoType: "github", + plugin: "export default { ... }" +} +``` + +### DOCKER Types + +Docker client and container types: + +```typescript +import type { DOCKER } from "@dockstat/typings" + +// Host configuration + +type HostConfig = DOCKER.HostConfig + +type Host = DOCKER.Host + +type HostWithHealth = DOCKER.HostWithHealth + +// Container types + +type Container = DOCKER.Container + +type ContainerStats = DOCKER.ContainerStats + +type ContainerInspect = DOCKER.ContainerInspect + +// Client options + +type DockerClientOptions = DOCKER.DockerClientOptions + +type MonitoringOptions = DOCKER.MonitoringOptions + +// Example + +const host: DOCKER.HostConfig = { + id: 1, + host: "192.168.1.100", + port: 2375, + secure: false, + name: "Docker Host 1" +} +``` + +### PLUGIN Types + +Plugin system types: + +```typescript +import type { PLUGIN } from "@dockstat/typings" + +// Plugin structure + +type Plugin = PLUGIN.Plugin + +type PluginConfig = PLUGIN.PluginConfig + +type PluginRoute = PLUGIN.PluginRoute + +type PluginAction = PLUGIN.PluginAction + +type PluginActionContext = PLUGIN.PluginActionContext + +// Example + +const plugin: PLUGIN.Plugin = { + id: 1, + name: "example-plugin", + version: "1.0.0", + config: { + table: { + name: "plugin_data", + columns: { /* column definitions */ }, + jsonColumns: ["data"] + }, + apiRoutes: { + "/status": { + method: "GET", + actions: ["getStatus"] + } + }, + actions: { + getStatus: ({ table, logger }) => { + return table.select(["*"]).all() + } + } + } +} +``` + +### EVENTS Types + +Docker event hook types for plugins: + +```typescript +import type { EVENTS } from "@dockstat/typings" + +// Event context + +type EventContext = { + container?: any + image?: any + logger: Logger + table?: QueryBuilder +} + +// Event handlers + +const events: EVENTS = { + onContainerStart: async (ctx) => { + ctx.logger.info(`Container ${ctx.container.id} started`) + }, + onContainerStop: async (ctx) => { + ctx.logger.info(`Container ${ctx.container.id} stopped`) + }, + onContainerRestart: async (ctx) => { /* ... */ }, + onImagePull: async (ctx) => { /* ... */ }, + onImageRemove: async (ctx) => { /* ... */ } +} +``` + +### ADAPTER Types + +React Router and framework adapter types: + +```typescript +import type { ADAPTER } from "@dockstat/typings" + +// React Router types + +type LoaderData = ADAPTER.LoaderData + +type ActionData = ADAPTER.ActionData +``` + +### HOTKEY Types + +Keyboard shortcut configuration: + +```typescript +import type { HOTKEY } from "@dockstat/typings" + +type HotkeyConfig = HOTKEY.HotkeyConfig + +type HotkeyAction = HOTKEY.HotkeyAction + +const hotkeys: HOTKEY.HotkeyConfig = { + search: { + key: "k", + ctrl: true, + action: "openSearch" + } +} +``` + +## Typebox Schemas + +Runtime validation schemas built with Typebox: + +```typescript +import { schemas } from "@dockstat/typings/schemas" +import { types } from "@dockstat/typings/types" + +// Docker host schema + +const hostSchema = schemas.HostConfigSchema + +type HostConfig = types.HostConfigType + +// Validate at runtime + +import { Value } from "@sinclair/typebox/value" + +const data = { /* host data */ } +if (Value.Check(schemas.HostConfigSchema, data)) { + // data is valid HostConfig +} + +// Use in Elysia routes + +import { Elysia } from "elysia" + +app.post("/hosts", ({ body }) => { + // body is automatically validated against schema + return createHost(body) +}, { + body: schemas.HostConfigSchema +}) +``` + +## Common Use Cases + +### API Route Validation + +```typescript +import { Elysia } from "elysia" +import { schemas } from "@dockstat/typings/schemas" + +new Elysia() + .post("/api/v2/docker/hosts", ({ body }) => { + // body is validated as HostConfig + return addDockerHost(body) + }, { + body: schemas.HostConfigSchema, + response: schemas.HostSchema + }) +``` + +### Plugin Development + +```typescript +import type { PLUGIN, EVENTS } from "@dockstat/typings" +import { column } from "@dockstat/sqlite-wrapper" + +const plugin: PLUGIN.Plugin = { + name: "my-plugin", + version: "1.0.0", + config: { + table: { + name: "my_data", + columns: { + id: column.id(), + value: column.text() + } + }, + apiRoutes: { + "/data": { + method: "GET", + actions: ["getData"] + } + }, + actions: { + getData: (ctx: PLUGIN.PluginActionContext) => { + return ctx.table?.select(["*"]).all() + } + } + }, + events: { + onContainerStart: async (ctx) => { + ctx.logger.info("Container started") + } + } satisfies EVENTS +} +``` + +### Theme Development + +```typescript +import type { THEME } from "@dockstat/typings" + +const customTheme: THEME.THEME_config = { + name: "custom-dark", + version: "1.0.0", + creator: "Your Name", + license: "MIT", + description: "Custom dark theme", + active: true, + vars: { + background_effect: { + Solid: { color: "#1a1a1a" } + }, + components: { + Card: { + accent: "#007acc", + border: "1px solid #333", + border_color: "#333", + border_size: "1px", + title: { + font: "Inter", + color: "#ffffff", + font_size: "18px", + font_weight: "600" + }, + sub_title: { + font: "Inter", + color: "#cccccc", + font_size: "14px", + font_weight: "400" + }, + content: { + font: "Inter", + color: "#e0e0e0", + font_size: "14px", + font_weight: "400" + } + } + } + } +} +``` + +### Database Operations + +```typescript +import type { DATABASE } from "@dockstat/typings" +import DB from "@dockstat/sqlite-wrapper" + +const db = new DB("./dockstat.db") + +// Type-safe database operations + +const plugins = db.table("plugins") + .select(["id", "name", "version"]) + .where({ repoType: "github" }) + .all() + +// Insert with type checking + +const newPlugin: DATABASE.DBPluginInsertT = { + name: "new-plugin", + version: "1.0.0", + repository: "https://github.com/user/plugin", + manifest: "https://github.com/user/plugin/manifest.json", + author: { name: "Developer" }, + repoType: "github", + plugin: "export default { ... }" +} +``` + +## Type Safety Benefits + + +1. **Compile-Time Checking**: Catch type errors during development +2. **IDE Autocomplete**: Full IntelliSense support across all packages +3. **Runtime Validation**: Typebox schemas validate data at runtime +4. **API Consistency**: Shared types ensure API contracts are maintained +5. **Refactoring Safety**: Changes to types propagate across the monorepo + +## Architecture + +``` +@dockstat/typings +├── src/ +│ ├── adapter.ts # React Router types +│ ├── database.ts # Database schema types +│ ├── docker-client.ts # Docker client types +│ ├── events.ts # Plugin event types +│ ├── hotkeys.ts # Keyboard shortcut types +│ ├── plugins.ts # Plugin system types +│ ├── themes.ts # Theme system types +│ ├── index.ts # Main export +│ └── typebox/ +│ ├── _schemas.ts # Typebox schemas +│ └── _types.ts # Derived types +``` + +## Package Dependencies + +Used by: + +* `@dockstat/sqlite-wrapper` - Database type definitions +* `@dockstat/docker-client` - Docker types and schemas +* `@dockstat/plugin-handler` - Plugin types +* `@dockstat/db` - Database operations +* `apps/api` - API route validation +* `apps/dockstat` - Frontend type safety + +## Best Practices + + +1. **Import from Namespaces**: Use namespaced imports for clarity + + ```typescript + import type { DOCKER } from "@dockstat/typings" + const host: DOCKER.HostConfig = { /* ... */ } + ``` +2. **Use Typebox Schemas**: Validate runtime data with schemas + + ```typescript + import { schemas } from "@dockstat/typings/schemas" + ``` +3. **Type Assertions**: Use `satisfies` for better inference + + ```typescript + const plugin = { /* ... */ } satisfies PLUGIN.Plugin + ``` +4. **Extend Types**: Create custom types by extending base types + + ```typescript + interface CustomHost extends DOCKER.Host { + customField: string + } + ``` + +## Related Packages + +* `@sinclair/typebox` - Runtime type validation +* `@dockstat/sqlite-wrapper` - Type-safe database queries +* `@dockstat/docker-client` - Docker operations with type safety +* `@dockstat/plugin-handler` - Plugin system using these types + +## License + +Part of the DockStat project. See main repository for license information. + +## Contributing + +Issues and PRs welcome at [github.com/Its4Nik/DockStat](https://github.com/Its4Nik/DockStat) \ No newline at end of file diff --git a/apps/docs/dockstat/packages/@dockstat-ui/README.md b/apps/docs/dockstat/packages/@dockstat-ui/README.md new file mode 100644 index 00000000..620f3cdb --- /dev/null +++ b/apps/docs/dockstat/packages/@dockstat-ui/README.md @@ -0,0 +1,787 @@ +--- +id: a04555b5-b827-4441-ae20-9cad1a2be714 +title: "@dockstat/ui" +collectionId: b4a5e48f-f103-480b-9f50-8f53f515cab9 +parentDocumentId: bbcefaa2-6bd4-46e8-ae4b-a6b823593e67 +updatedAt: 2025-12-17T09:23:29.497Z +urlId: NXWduCaEB3 +--- + +> A React component library for DockStat applications. Built with TypeScript, TailwindCSS, and designed for theme integration. Includes Storybook for component development and documentation. + +## Overview + +`@dockstat/ui` provides a comprehensive set of React components used across DockStat applications. The library is designed to work seamlessly with the DockStat theming system and follows consistent design patterns. + +```mermaidjs + +graph TB + subgraph "Applications" + DS["dockstat (Frontend)"] + OTHER["Other Apps"] + end + + subgraph "@dockstat/ui" + COMPONENTS["Component Library"] + STORIES["Storybook"] + THEMES["Theme Integration"] + UTILS["UI Utilities"] + end + + subgraph "Component Categories" + LAYOUT["Layout Components"] + FORMS["Form Components"] + DISPLAY["Display Components"] + FEEDBACK["Feedback Components"] + NAV["Navigation Components"] + end + + subgraph "Dependencies" + REACT["React"] + TW["TailwindCSS"] + TYP["@dockstat/typings"] + UTIL["@dockstat/utils"] + end + + DS --> COMPONENTS + OTHER --> COMPONENTS + COMPONENTS --> LAYOUT + COMPONENTS --> FORMS + COMPONENTS --> DISPLAY + COMPONENTS --> FEEDBACK + COMPONENTS --> NAV + COMPONENTS --> REACT + COMPONENTS --> TW + THEMES --> TYP + UTILS --> UTIL +``` + +## Installation + +```bash +bun add @dockstat/ui +``` + +> **Note**: This is an internal package. Peer dependencies include React, TailwindCSS, and other DockStat packages. + +## Quick Start + +```tsx +import { Card, Button, Badge, Table } from "@dockstat/ui"; + +function Dashboard() { + return ( + +
Running }, + { name: "redis", status: Paused } + ]} + /> + + + ); +} +``` + +## Component Architecture + +```mermaidjs + +graph LR + subgraph "Component Structure" + COMP["Component"] + PROPS["Props Interface"] + STYLES["Styles"] + LOGIC["Logic/Hooks"] + end + + subgraph "Styling Approach" + TW["TailwindCSS Classes"] + CSS_VARS["CSS Variables"] + THEME["Theme Integration"] + end + + subgraph "Output" + JSX["JSX Element"] + TYPES["TypeScript Types"] + end + + COMP --> PROPS + COMP --> STYLES + COMP --> LOGIC + STYLES --> TW + STYLES --> CSS_VARS + CSS_VARS --> THEME + COMP --> JSX + PROPS --> TYPES +``` + +## Components + +### Layout Components + +#### Card + +A versatile container component for grouping related content. + +```tsx +import { Card } from "@dockstat/ui"; + +// Basic card + +

CPU: 45%

+

Memory: 256MB

+
+ +// Card with subtitle + +

Status: Running

+
+ +// Card with custom styling + +

High CPU usage detected!

+
+``` + +**Props:** + +| Prop | Type | Default | Description | +|----|----|----|----| +| `title` | `string` | — | Card title | +| `subtitle` | `string` | — | Card subtitle | +| `children` | `ReactNode` | — | Card content | +| `className` | `string` | — | Additional CSS classes | +| `accent` | `string` | — | Accent color override | + +#### Divider + +A horizontal separator for content sections. + +```tsx +import { Divider } from "@dockstat/ui"; + +
+

Section 1

+ +

Section 2

+
+ +// With label + + +// Custom styling + +``` + +#### Modal + +A dialog component for overlays and popups. + +```tsx +import { Modal } from "@dockstat/ui"; +import { useState } from "react"; + +function Example() { + const [isOpen, setIsOpen] = useState(false); + + return ( + <> + + + setIsOpen(false)} + title="Confirm Action" + > +

Are you sure you want to proceed?

+
+ + +
+
+ + ); +} +``` + +**Props:** + +| Prop | Type | Default | Description | +|----|----|----|----| +| `isOpen` | `boolean` | `false` | Controls modal visibility | +| `onClose` | `() => void` | — | Close handler | +| `title` | `string` | — | Modal title | +| `children` | `ReactNode` | — | Modal content | +| `size` | `"sm" \| "md" \| "lg"` | `"md"` | Modal size | + +### Form Components + +#### Button + +A customizable button component with multiple variants. + +```tsx +import { Button } from "@dockstat/ui"; + +// Variants + + + + + +// Sizes + + + + +// States + + + +// With icon + +``` + +**Props:** + +| Prop | Type | Default | Description | +|----|----|----|----| +| `variant` | `"primary" \| "secondary" \| "danger" \| "ghost"` | `"primary"` | Button style variant | +| `size` | `"sm" \| "md" \| "lg"` | `"md"` | Button size | +| `disabled` | `boolean` | `false` | Disable button | +| `loading` | `boolean` | `false` | Show loading state | +| `icon` | `ReactNode` | — | Icon element | +| `onClick` | `() => void` | — | Click handler | +| `children` | `ReactNode` | — | Button text | + +#### Form Inputs + +```tsx +import { Input, Select, Checkbox, Switch } from "@dockstat/ui"; + +// Text input + setName(e.target.value)} +/> + +// Select dropdown +
console.log("Clicked:", row)} +/> +``` + +**Props:** + +| Prop | Type | Description | +|----|----|----| +| `columns` | `Column[]` | Column definitions | +| `data` | `T[]` | Table data array | +| `onRowClick` | `(row: T) => void` | Row click handler | +| `loading` | `boolean` | Show loading state | +| `emptyMessage` | `string` | Message when no data | + +#### Link + +A styled link component with router integration. + +```tsx +import { Link } from "@dockstat/ui"; + +// Internal link (uses React Router) +View Containers + +// External link + + Docker Docs + + +// Styled variants +Settings +``` + +### Navigation Components + +#### Navbar + +A top navigation bar component. + +```tsx +import { Navbar } from "@dockstat/ui"; + +} + items={[ + { label: "Dashboard", to: "/" }, + { label: "Containers", to: "/containers" }, + { label: "Images", to: "/images" }, + { label: "Settings", to: "/settings" } + ]} + actions={ + + } +/> +``` + +### Feedback Components + +#### HoverBubble + +A tooltip-like component that appears on hover. + +```tsx +import { HoverBubble } from "@dockstat/ui"; + + + Running + + +// With custom positioning + + Hover me + +``` + +### Plugin Components + +#### Extensions + +Components for rendering plugin-provided UI elements. + +```tsx +import { PluginSlot, PluginWidget } from "@dockstat/ui/Plugins"; + +// Render plugin content in a slot + + {(plugins) => plugins.map(plugin => ( + + ))} + +``` + +## Theme Integration + +Components automatically integrate with the DockStat theming system: + +```mermaidjs + +graph TB + subgraph "Theme System" + DB["@dockstat/db"] + THEME["Theme Config"] + end + + subgraph "CSS Variables" + BG["--bg-*"] + CARD["--card-*"] + BTN["--btn-*"] + TEXT["--text-*"] + end + + subgraph "Components" + UI["@dockstat/ui Components"] + end + + DB --> THEME + THEME --> BG + THEME --> CARD + THEME --> BTN + THEME --> TEXT + BG --> UI + CARD --> UI + BTN --> UI + TEXT --> UI +``` + +### Using Theme Variables + +```typescript +// Components use CSS variables that map to theme settings + +const Card = ({ children, ...props }) => ( +
+ {children} +
+); +``` + +### Applying Themes + +```typescript +import DockStatDB from "@dockstat/db"; +import type { THEME } from "@dockstat/typings"; + +function applyTheme(theme: THEME.THEME_config) { + const root = document.documentElement; + + // Background + const bg = theme.vars.background_effect; + if ("Solid" in bg) { + root.style.setProperty("--bg-color", bg.Solid.color); + } else if ("Gradient" in bg) { + root.style.setProperty("--bg-from", bg.Gradient.from); + root.style.setProperty("--bg-to", bg.Gradient.to); + root.style.setProperty("--bg-direction", bg.Gradient.direction); + } + + // Card component + const card = theme.vars.components.Card; + root.style.setProperty("--card-accent", card.accent); + root.style.setProperty("--card-border", card.border); + root.style.setProperty("--card-title-color", card.title.color); + root.style.setProperty("--card-title-font", card.title.font); + root.style.setProperty("--card-content-color", card.content.color); +} +``` + +## Storybook + +The package includes Storybook for component development and documentation. + +### Running Storybook + +```bash +cd packages/ui + +bun run storybook +# or +bun run dev +# Available at http://localhost:6006 +``` + +### Writing Stories + +```tsx +// src/stories/Button.stories.tsx + +import type { Meta, StoryObj } from "@storybook/react"; +import { Button } from "../components/Button"; + +const meta: Meta = { + title: "Components/Button", + component: Button, + parameters: { + layout: "centered" + }, + tags: ["autodocs"], + argTypes: { + variant: { + control: "select", + options: ["primary", "secondary", "danger", "ghost"] + }, + size: { + control: "select", + options: ["sm", "md", "lg"] + } + } +}; + +export default meta; +type Story = StoryObj; + +export const Primary: Story = { + args: { + variant: "primary", + children: "Primary Button" + } +}; + +export const Secondary: Story = { + args: { + variant: "secondary", + children: "Secondary Button" + } +}; + +export const AllVariants: Story = { + render: () => ( +
+ + + + +
+ ) +}; +``` + +## Directory Structure + +``` +packages/ui/ +├── src/ +│ ├── components/ +│ │ ├── Badge/ +│ │ │ ├── Badge.tsx +│ │ │ └── index.ts +│ │ ├── Button/ +│ │ │ ├── Button.tsx +│ │ │ └── index.ts +│ │ ├── Card/ +│ │ │ ├── Card.tsx +│ │ │ └── index.ts +│ │ ├── Divider/ +│ │ ├── Extensions/ +│ │ ├── Forms/ +│ │ ├── HoverBubble/ +│ │ ├── Link/ +│ │ ├── Modal/ +│ │ ├── Navbar/ +│ │ ├── Plugins/ +│ │ ├── Slider/ +│ │ ├── Table/ +│ │ └── index.ts +│ ├── stories/ +│ │ ├── Badge.stories.tsx +│ │ ├── Button.stories.tsx +│ │ └── ... +│ ├── themes/ +│ │ └── default.css +│ ├── utils/ +│ │ └── cn.ts +│ ├── welcome/ +│ └── App.tsx +├── .storybook/ +│ ├── main.ts +│ └── preview.ts +├── public/ +├── index.html +├── vite.config.ts +├── tsconfig.json +└── package.json +``` + +## Utility Functions + +### Class Name Utility + +```typescript + +import { cn } from "@dockstat/ui/utils"; + +// Merge class names conditionally +const buttonClass = cn( + "px-4 py-2 rounded", + variant === "primary" && "bg-blue-500 text-white", + variant === "secondary" && "bg-gray-200 text-gray-800", + disabled && "opacity-50 cursor-not-allowed", + className +); +``` + +## Development + +### Setup + +```bash +cd packages/ui + +bun install +``` + +### Development Mode + +```bash +# Run Storybook dev server + +bun run dev + +# Run Storybook + +bun run storybook +``` + +### Building + +```bash +bun run build +``` + +### Type Checking + +```bash +bun run check-types +``` + +## Best Practices + +### Component Design + + +1. **Props Interface**: Always define TypeScript interfaces for props +2. **Default Values**: Provide sensible defaults for optional props +3. **Accessibility**: Include ARIA attributes and keyboard support +4. **Composition**: Design components for composition over configuration + +### Styling + + +1. **TailwindCSS**: Use Tailwind classes for styling +2. **CSS Variables**: Use theme variables for customizable properties +3. **Responsive**: Design mobile-first with responsive utilities +4. **Dark Mode**: Support both light and dark themes + +### Performance + + +1. **Memoization**: Use `React.memo` for expensive components +2. **Lazy Loading**: Code-split large components +3. **Event Handlers**: Memoize callbacks with `useCallback` + +## Related Packages + +* `@dockstat/typings` - Type definitions including theme types +* `@dockstat/utils` - Shared utility functions +* `@dockstat/db` - Theme management and persistence + +## License + +Part of the DockStat project - See main repository for license information. + +## Contributing + +Issues and PRs welcome at [github.com/Its4Nik/DockStat](https://github.com/Its4Nik/DockStat) \ No newline at end of file diff --git a/apps/docs/dockstat/packages/@dockstat-utils/README.md b/apps/docs/dockstat/packages/@dockstat-utils/README.md new file mode 100644 index 00000000..7dbfc868 --- /dev/null +++ b/apps/docs/dockstat/packages/@dockstat-utils/README.md @@ -0,0 +1,712 @@ +--- +id: d3039895-f53c-46ab-89ce-de0ad22ce03c +title: "@dockstat/utils" +collectionId: b4a5e48f-f103-480b-9f50-8f53f515cab9 +parentDocumentId: bbcefaa2-6bd4-46e8-ae4b-a6b823593e67 +updatedAt: 2025-12-17T09:38:34.988Z +urlId: dHg9YfruFJ +--- + +> A collection of shared utility functions used across DockStat packages and applications. Provides common helpers for string manipulation, data formatting, type checking, and other frequently needed operations. + +## Overview + +`@dockstat/utils` centralizes common utility functions to ensure consistency and reduce code duplication across the DockStat monorepo. These utilities are designed to be lightweight, well-tested, and tree-shakeable. + +```mermaidjs + +graph TB + subgraph "Consumers" + API["apps/api"] + DS["apps/dockstat"] + DC["@dockstat/docker-client"] + UI["@dockstat/ui"] + PH["@dockstat/plugin-handler"] + end + + subgraph "@dockstat/utils" + STRING["String Utilities"] + FORMAT["Formatting Utilities"] + TYPE["Type Utilities"] + DATA["Data Utilities"] + ASYNC["Async Utilities"] + end + + API --> STRING + API --> FORMAT + DS --> FORMAT + DS --> TYPE + DC --> ASYNC + DC --> DATA + UI --> STRING + UI --> FORMAT + PH --> TYPE + PH --> DATA +``` + +## Installation + +```bash +bun add @dockstat/utils # currently only available in the Monorepo! +``` + +## Quick Start + +```typescript +import { + formatBytes, + formatDuration, + truncate, + debounce, + isNotNullish +} from "@dockstat/utils"; + +// Format container memory usage + +const memoryStr = formatBytes(1073741824); // "1.00 GB" + +// Format container uptime + +const uptimeStr = formatDuration(86400000); // "1d 0h 0m" + +// Truncate long container names + +const shortName = truncate("very-long-container-name-here", 20); // "very-long-contain..." + +// Debounce search input + +const debouncedSearch = debounce((query) => { + console.log("Searching:", query); +}, 300); + +// Filter out null/undefined values + +const validItems = items.filter(isNotNullish); +``` + +## Utility Categories + +### String Utilities + +#### truncate + +Truncates a string to a specified length with an ellipsis. + +```typescript +import { truncate } from "@dockstat/utils"; + +truncate("Hello World", 5); // "Hello..." +truncate("Hi", 10); // "Hi" +truncate("Hello World", 8, "…"); // "Hello Wo…" +``` + +**Signature:** + +```typescript +function truncate( + str: string, + maxLength: number, + suffix?: string +): string +``` + +#### capitalize + +Capitalizes the first letter of a string. + +```typescript +import { capitalize } from "@dockstat/utils"; + +capitalize("hello"); // "Hello" +capitalize("WORLD"); // "WORLD" +capitalize("hello world"); // "Hello world" +``` + +#### camelToKebab + +Converts camelCase to kebab-case. + +```typescript +import { camelToKebab } from "@dockstat/utils"; + +camelToKebab("containerName"); // "container-name" +camelToKebab("myDockerHost"); // "my-docker-host" +camelToKebab("APIResponse"); // "api-response" +``` + +#### kebabToCamel + +Converts kebab-case to camelCase. + +```typescript +import { kebabToCamel } from "@dockstat/utils"; + +kebabToCamel("container-name"); // "containerName" +kebabToCamel("my-docker-host"); // "myDockerHost" +``` + +#### slugify + +Creates a URL-friendly slug from a string. + +```typescript +import { slugify } from "@dockstat/utils"; + +slugify("Hello World!"); // "hello-world" +slugify("My Container Name"); // "my-container-name" +slugify("nginx/proxy:latest"); // "nginx-proxy-latest" +``` + +#### escapeHtml + +Escapes HTML special characters. + +```typescript +import { escapeHtml } from "@dockstat/utils"; + +escapeHtml(""); +// "<script>alert('xss')</script>" +``` + +### Formatting Utilities + +#### formatBytes + +Formats bytes into human-readable strings. + +```typescript +import { formatBytes } from "@dockstat/utils"; + +formatBytes(0); // "0 Bytes" +formatBytes(1024); // "1.00 KB" +formatBytes(1048576); // "1.00 MB" +formatBytes(1073741824); // "1.00 GB" +formatBytes(1099511627776); // "1.00 TB" + +// Custom decimal places + +formatBytes(1536, 0); // "2 KB" +formatBytes(1536, 3); // "1.500 KB" +``` + +**Signature:** + +```typescript +function formatBytes( + bytes: number, + decimals?: number +): string +``` + +#### formatDuration + +Formats milliseconds into a human-readable duration. + +```typescript +import { formatDuration } from "@dockstat/utils"; + +formatDuration(1000); // "1s" +formatDuration(60000); // "1m 0s" +formatDuration(3600000); // "1h 0m" +formatDuration(86400000); // "1d 0h 0m" +formatDuration(90061000); // "1d 1h 1m" + +// Compact format + +formatDuration(90061000, { compact: true }); // "1d 1h" +``` + +**Signature:** + +```typescript +function formatDuration( + ms: number, + options?: { compact?: boolean } +): string +``` + +#### formatNumber + +Formats numbers with thousands separators. + +```typescript +import { formatNumber } from "@dockstat/utils"; + +formatNumber(1234); // "1,234" +formatNumber(1234567.89); // "1,234,567.89" +formatNumber(1234, "de-DE"); // "1.234" +``` + +#### formatPercent + +Formats a number as a percentage. + +```typescript +import { formatPercent } from "@dockstat/utils"; + +formatPercent(0.5); // "50%" +formatPercent(0.1234, 2); // "12.34%" +formatPercent(1.5); // "150%" +``` + +#### formatDate + +Formats dates into readable strings. + +```typescript +import { formatDate } from "@dockstat/utils"; + +const date = new Date("2024-01-15T10:30:00Z"); + +formatDate(date); // "Jan 15, 2024" +formatDate(date, "short"); // "1/15/24" +formatDate(date, "long"); // "January 15, 2024" +formatDate(date, "time"); // "10:30 AM" +formatDate(date, "datetime"); // "Jan 15, 2024, 10:30 AM" +formatDate(date, "iso"); // "2024-01-15T10:30:00.000Z" +``` + +#### relativeTime + +Formats a date as relative time (e.g., "2 hours ago"). + +```typescript +import { relativeTime } from "@dockstat/utils"; + +const past = new Date(Date.now() - 3600000); +relativeTime(past); // "1 hour ago" + +const future = new Date(Date.now() + 86400000); +relativeTime(future); // "in 1 day" +``` + +### Type Utilities + +#### isNotNullish + +Type guard that checks if a value is not null or undefined. + +```typescript +import { isNotNullish } from "@dockstat/utils"; + +const items = [1, null, 2, undefined, 3]; +const valid = items.filter(isNotNullish); // [1, 2, 3] + +// Type narrowing works correctly + +if (isNotNullish(value)) { + // value is guaranteed to be non-null here +} +``` + +#### isString + +Type guard for strings. + +```typescript +import { isString } from "@dockstat/utils"; + +isString("hello"); // true +isString(123); // false +isString(null); // false +``` + +#### isNumber + +Type guard for numbers. + +```typescript +import { isNumber } from "@dockstat/utils"; + +isNumber(123); // true +isNumber("123"); // false +isNumber(NaN); // false +isNumber(Infinity); // true +``` + +#### isObject + +Type guard for plain objects. + +```typescript +import { isObject } from "@dockstat/utils"; + +isObject({}); // true +isObject({ a: 1 }); // true +isObject([]); // false +isObject(null); // false +isObject(new Date()); // false +``` + +#### isArray + +Type guard for arrays. + +```typescript +import { isArray } from "@dockstat/utils"; + +isArray([]); // true +isArray([1, 2, 3]); // true +isArray("array"); // false +``` + +#### isFunction + +Type guard for functions. + +```typescript +import { isFunction } from "@dockstat/utils"; + +isFunction(() => {}); // true +isFunction(function() {}); // true +isFunction(class {}); // true +isFunction({}); // false +``` + +### Data Utilities + +#### pick + +Creates an object with only the specified keys. + +```typescript + +import { pick } from "@dockstat/utils"; + +const container = { + id: "abc123", + name: "nginx", + image: "nginx:latest", + status: "running", + created: 1704067200 +}; + +const summary = pick(container, ["id", "name", "status"]); +// { id: "abc123", name: "nginx", status: "running" } +``` + +#### omit + +Creates an object without the specified keys. + +```typescript +import { omit } from "@dockstat/utils"; + +const user = { + id: 1, + name: "John", + password: "secret", + email: "john@example.com" +}; + +const safe = omit(user, ["password"]); +// { id: 1, name: "John", email: "john@example.com" } +``` + +#### groupBy + +Groups array items by a key or function. + +```typescript +import { groupBy } from "@dockstat/utils"; + +const containers = [ + { name: "nginx", status: "running" }, + { name: "redis", status: "running" }, + { name: "postgres", status: "stopped" } +]; + +const byStatus = groupBy(containers, "status"); +// { +// running: [{ name: "nginx", ... }, { name: "redis", ... }], +// stopped: [{ name: "postgres", ... }] +// } + +// With function + +const byFirstLetter = groupBy(containers, (c) => c.name[0]); +``` + +#### uniqueBy + +Returns unique items from an array based on a key. + +```typescript +import { uniqueBy } from "@dockstat/utils"; + +const items = [ + { id: 1, name: "A" }, + { id: 2, name: "B" }, + { id: 1, name: "A duplicate" } +]; + +const unique = uniqueBy(items, "id"); +// [{ id: 1, name: "A" }, { id: 2, name: "B" }] +``` + +#### sortBy + +Sorts an array by a key or function. + +```typescript +import { sortBy } from "@dockstat/utils"; + +const containers = [ + { name: "nginx", cpu: 45 }, + { name: "redis", cpu: 12 }, + { name: "postgres", cpu: 30 } +]; + +// Sort by key +const byCpu = sortBy(containers, "cpu"); + +// Sort descending +const byCpuDesc = sortBy(containers, "cpu", "desc"); + +// Sort by function +const byNameLength = sortBy(containers, (c) => c.name.length); +``` + +### Async Utilities + +#### debounce + +Creates a debounced function that delays invocation. + +```typescript +import { debounce } from "@dockstat/utils"; + +const search = debounce((query: string) => { + console.log("Searching:", query); +}, 300); + +// Rapid calls + +search("h"); +search("he"); +search("hel"); +search("hell"); +search("hello"); +// Only "hello" is logged after 300ms +``` + +**Signature:** + +```typescript +function debounce any>( + fn: T, + wait: number +): (...args: Parameters) => void +``` + +#### retry + +Retries a function with exponential backoff. + +```typescript +import { retry } from "@dockstat/utils"; + +const result = await retry( + async () => { + const response = await fetch("https://api.example.com/data"); + if (!response.ok) throw new Error("Request failed"); + return response.json(); + }, + { + attempts: 3, + delay: 1000, + backoff: 2 // Exponential backoff multiplier + } +); +``` + +**Signature:** + +```typescript +function retry( + fn: () => Promise, + options?: { + attempts?: number; + delay?: number; + backoff?: number; + onRetry?: (error: Error, attempt: number) => void; + } +): Promise +``` + +### Container Utilities + +Specialized utilities for Docker container operations. + +#### parseContainerName + +Parses a Docker container name into its components. + +```typescript +import { parseContainerName } from "@dockstat/utils"; + +parseContainerName("/nginx-proxy"); +// { name: "nginx-proxy", prefix: null } + +parseContainerName("/project_nginx_1"); +// { name: "nginx_1", prefix: "project" } +``` + +#### parseImageName + +Parses a Docker image reference. + +```typescript +import { parseImageName } from "@dockstat/utils"; + +parseImageName("nginx"); +// { registry: null, repository: "nginx", tag: "latest" } + +parseImageName("nginx:1.19"); +// { registry: null, repository: "nginx", tag: "1.19" } + +parseImageName("docker.io/library/nginx:alpine"); +// { registry: "docker.io", repository: "library/nginx", tag: "alpine" } + +parseImageName("ghcr.io/user/app:v1.0.0"); +// { registry: "ghcr.io", repository: "user/app", tag: "v1.0.0" } +``` + +#### calculateCpuPercent + +Calculates CPU percentage from Docker stats. + +```typescript +import { calculateCpuPercent } from "@dockstat/utils"; + +const cpuPercent = calculateCpuPercent( + previousCpuUsage, + currentCpuUsage, + previousSystemCpu, + currentSystemCpu, + numCpus +); +``` + +#### calculateMemoryPercent + +Calculates memory percentage from Docker stats. + +```typescript +import { calculateMemoryPercent } from "@dockstat/utils"; + +const memPercent = calculateMemoryPercent( + memoryUsage, + memoryLimit +); +``` + +## API Reference + +### String Utilities + +| Function | Description | +|----|----| +| `truncate(str, maxLength, suffix?)` | Truncate string with suffix | +| `capitalize(str)` | Capitalize first letter | +| `camelToKebab(str)` | Convert camelCase to kebab-case | +| `kebabToCamel(str)` | Convert kebab-case to camelCase | +| `slugify(str)` | Create URL-friendly slug | +| `escapeHtml(str)` | Escape HTML special characters | + +### Formatting Utilities + +| Function | Description | +|----|----| +| `formatBytes(bytes, decimals?)` | Format bytes to human-readable | +| `formatDuration(ms, options?)` | Format milliseconds to duration | +| `formatNumber(num, locale?)` | Format number with separators | +| `formatPercent(num, decimals?)` | Format as percentage | +| `formatDate(date, format?)` | Format date | +| `relativeTime(date)` | Format as relative time | + +### Type Utilities + +| Function | Description | +|----|----| +| `isNotNullish(value)` | Check not null/undefined | +| `isString(value)` | Type guard for string | +| `isNumber(value)` | Type guard for number | +| `isObject(value)` | Type guard for plain object | +| `isArray(value)` | Type guard for array | +| `isFunction(value)` | Type guard for function | + +### Data Utilities + +| Function | Description | +|----|----| +| `deepClone(obj)` | Deep clone object | +| `deepMerge(...objs)` | Deep merge objects | +| `pick(obj, keys)` | Pick specific keys | +| `omit(obj, keys)` | Omit specific keys | +| `groupBy(arr, key)` | Group array by key | +| `uniqueBy(arr, key)` | Get unique by key | +| `sortBy(arr, key, dir?)` | Sort array by key | + +### Async Utilities + +| Function | Description | +|----|----| +| `debounce(fn, wait)` | Debounce function calls | +| `throttle(fn, wait)` | Throttle function calls | +| `sleep(ms)` | Delay execution | +| `retry(fn, options?)` | Retry with backoff | +| `timeout(promise, ms)` | Add timeout to promise | + +## Development + +### Directory Structure + +``` +packages/utils/ +├── src/ +│ ├── string.ts # String utilities +│ ├── format.ts # Formatting utilities +│ ├── type.ts # Type utilities +│ ├── data.ts # Data utilities +│ ├── async.ts # Async utilities +│ ├── container.ts # Container utilities +│ └── index.ts # Main export +├── package.json +└── tsconfig.json +``` + +### Building + +```bash +cd packages/utils + +bun run build +``` + +### Testing + +```bash +bun run test +``` + +### Type Checking + +```bash +bun run check-types +``` + +## Related Packages + +* `@dockstat/typings` - Type definitions +* `@dockstat/logger` - Logging utilities +* `@dockstat/ui` - UI components using these utilities +* `@dockstat/docker-client` - Docker client using async utilities + +## License + +Part of the DockStat project - See main repository for license information. + +## Contributing + +Issues and PRs welcome at [github.com/Its4Nik/DockStat](https://github.com/Its4Nik/DockStat) \ No newline at end of file diff --git a/apps/docs/dockstat/packages/README.md b/apps/docs/dockstat/packages/README.md index 687d8d80..26bade00 100644 --- a/apps/docs/dockstat/packages/README.md +++ b/apps/docs/dockstat/packages/README.md @@ -1,35 +1,220 @@ --- -id: 75d80211-7262-4064-aaa6-2ead20e17f43 +id: bbcefaa2-6bd4-46e8-ae4b-a6b823593e67 title: Packages collectionId: b4a5e48f-f103-480b-9f50-8f53f515cab9 parentDocumentId: 7dddd764-6483-4f84-96a3-988304e772d3 -updatedAt: 2025-08-24T17:32:02.644Z -urlId: kRAif1izY8 +updatedAt: 2025-12-16T19:07:33.253Z +urlId: waARCz3AZ0 --- -# Public Packages +# Packages +The DockStat monorepo contains shared packages under `packages/`. These provide reusable functionality across applications. -:::info -These Packages are publically available on NPM for download. +## Package Overview -::: +```mermaidjs -## [@dockstat/theme-handler](/doc/b4148ac3-5b60-4223-aa0d-48111649b91f) +graph TD + subgraph Public Packages + SW["@dockstat/sqlite-wrapper"] + LOG["@dockstat/logger"] + TYP["@dockstat/typings"] + OS["@dockstat/outline-sync"] + RRE["@dockstat/create-rr-elysia"] + end -## [@dockstat/outline-sync](/doc/b931ba3f-2f39-4414-9c80-fb1ebbe92771) + subgraph Internal Packages + DB["@dockstat/db"] + DC["@dockstat/docker-client"] + PH["@dockstat/plugin-handler"] + UI["@dockstat/ui"] + UT["@dockstat/utils"] + end -## [@dockstat/sqlite-wrapper](/doc/56229547-5cee-49ff-be41-1b75e7548809) + DB --> SW + DB --> TYP + DC --> SW + DC --> PH + DC --> LOG + DC --> TYP + DC --> UT + PH --> SW + PH --> LOG + PH --> TYP + UI --> TYP + UI --> UT +``` +## Package List -___ +| Package | Version | Description | Public | +|----|----|----|----| +| `@dockstat/sqlite-wrapper` | 1.2.8 | Type-safe SQLite query builder for Bun | Yes | +| `@dockstat/logger` | 1.0.1 | Colorized logging utility with source maps | Yes | +| `@dockstat/typings` | 1.1.0 | Shared TypeScript types and Typebox schemas | Yes | +| `@dockstat/outline-sync` | 1.2.4 | Markdown sync tool for Outline Wiki | Yes | +| `@dockstat/create-rr-elysia` | 1.0.2 | React Router + Elysia project template | Yes | +| `@dockstat/db` | 1.0.0 | Database layer with theme management | No | +| `@dockstat/docker-client` | 1.0.2 | Docker operations via Dockerode | No | +| `@dockstat/plugin-handler` | — | Plugin lifecycle management | No | +| `@dockstat/ui` | 1.0.0 | Shared React UI components | No | +| `@dockstat/utils` | — | Common utilities | No | -# Private Packages +## Core Packages +### @dockstat/sqlite-wrapper -:::info -These packages are only internally used by DockStat, you can still use them by forking their code. Private Packages are under the MIT license, since they use the public packages under the hood and are just wrappers in the end. +Type-safe SQLite wrapper for Bun's `bun:sqlite`. Provides schema-first table definitions and a chainable query builder. -::: +Features: -## [[PRIVATE] @dockstat/db Features](/doc/c87977e4-e0f6-49ae-9cf5-e979c86605b1) \ No newline at end of file +* Compile-time type validation +* JSON column support +* Generated columns (virtual/stored) +* Foreign key constraints +* WAL mode and PRAGMA management + +```typescript +import { DB, column } from "@dockstat/sqlite-wrapper"; + +const db = new DB("app.db"); +const users = db.createTable("users", { + id: column.id(), + name: column.text({ notNull: true }), + email: column.text({ unique: true }), +}); + +const result = users.select(["id", "name"]).where({ email: "a@b.com" }).first(); +``` + +### @dockstat/docker-client + +Docker client library built on Dockerode with real-time monitoring, streaming, and multi-host support. + +Features: + +* Host and container management +* Real-time statistics streaming +* Event-driven monitoring +* Worker pool architecture +* WebSocket-compatible streaming + +```typescript +import DockerClient from "@dockstat/docker-client"; + +const client = new DockerClient({ enableMonitoring: true }); +client.addHost({ id: 1, host: "localhost", name: "local", secure: false }); +const containers = await client.getAllContainers(); +``` + +### @dockstat/db + +Database abstraction layer for DockStat. Manages themes and provides database access for integration with docker-client. + +Features: + +* Theme CRUD operations +* Predefined table schemas +* Integration with sqlite-wrapper + +```typescript +import DockStatDB from "@dockstat/db"; + +const db = new DockStatDB(); +const theme = db.getCurrentTheme(); +db.setTheme("dark-theme"); +``` + +### @dockstat/plugin-handler + +Plugin system for DockStat. Manages plugin installation, activation, and execution. + +Features: + +* Dynamic plugin loading +* Custom database tables per plugin +* API route proxying +* Event hooks + +```typescript +import PluginHandler from "@dockstat/plugin-handler"; + +const handler = new PluginHandler(db); +await handler.loadPlugins([1, 2, 3]); +const result = await handler.handleRoute(1, "/custom", request); +``` + +### @dockstat/logger + +Colorized logging utility with source map support and hierarchical logger naming. + +Environment variables: + +* `DOCKSTAT_LOGGER_FULL_FILE_PATH` — Show full file paths +* `DOCKSTAT_LOGGER_DISABLED_LOGGERS` — Disable specific loggers +* `DOCKSTAT_LOGGER_ONLY_SHOW` — Show only specific loggers +* `DOCKSTAT_LOGGER_SEPERATOR` — Name separator (default `:`) + +```typescript +import Logger from "@dockstat/logger"; + +const log = new Logger("MyService"); +log.info("Starting service"); +const child = log.spawn("SubModule"); +child.debug("Processing"); +``` + +### @dockstat/typings + +Shared TypeScript types and Typebox schemas used across all packages. + +Exports: + +* `@dockstat/typings` — Main types +* `@dockstat/typings/schemas` — Typebox schemas +* `@dockstat/typings/types` — Type definitions + +### @dockstat/ui + +React UI component library with TailwindCSS styling. Includes Storybook for development. + +```typescript +import { Button, Card } from "@dockstat/ui"; +``` + +## Public Packages (NPM) + +The following packages are published to NPM: + +* `@dockstat/sqlite-wrapper` — [npm](https://www.npmjs.com/package/@dockstat/sqlite-wrapper) +* `@dockstat/logger` — [npm](https://www.npmjs.com/package/@dockstat/logger) +* `@dockstat/typings` — [npm](https://www.npmjs.com/package/@dockstat/typings) +* `@dockstat/outline-sync` — [npm](https://www.npmjs.com/package/@dockstat/outline-sync) +* `@dockstat/create-rr-elysia` — [npm](https://www.npmjs.com/package/@dockstat/create-rr-elysia) + +## Development + +Run package-specific commands from the package directory: + +```bash +cd packages/sqlite-wrapper + +bun run test + +bun run lint +``` + +Build a package: + +```bash +cd packages/db + +bun run build +``` + +Check types across all packages: + +```bash +bun run check-types +``` \ No newline at end of file diff --git a/apps/docs/dockstat/packages/[private]-@dockstat-db-features/README.md b/apps/docs/dockstat/packages/[private]-@dockstat-db-features/README.md deleted file mode 100644 index 32e4797f..00000000 --- a/apps/docs/dockstat/packages/[private]-@dockstat-db-features/README.md +++ /dev/null @@ -1,375 +0,0 @@ ---- -id: c87977e4-e0f6-49ae-9cf5-e979c86605b1 -title: "[PRIVATE] @dockstat/db Features" -collectionId: b4a5e48f-f103-480b-9f50-8f53f515cab9 -parentDocumentId: 75d80211-7262-4064-aaa6-2ead20e17f43 -updatedAt: 2025-08-23T21:21:44.483Z -urlId: NWsT8KT5bX ---- - -## 🏗️ Core Architecture - -### Type-Safe Foundation - -* **Full TypeScript Support**: Complete type safety with @dockstat/typings integration -* **Dockerode Integration**: Built on the robust Dockerode library with enhanced abstractions -* **Modular Design**: Extensible architecture with separate concerns for monitoring, streaming, and client operations -* **Error Handling**: Comprehensive error handling with retry mechanisms and detailed error reporting - -### Configuration & Options - -* **Flexible Configuration**: Extensive configuration options for timeouts, retries, and monitoring -* **Environment Adaptability**: Support for both secure (HTTPS/TLS) and insecure HTTP connections -* **Resource Management**: Automatic cleanup and resource management with graceful shutdown - -## 🐳 Docker Host Management - -### Host Operations - -* **Multi-Host Support**: Manage multiple Docker hosts simultaneously -* **Dynamic Host Management**: Add, remove, and update hosts at runtime -* **Connection Pooling**: Efficient connection management with automatic retry mechanisms -* **Health Monitoring**: Continuous health checks for all registered hosts - -### Host Configuration - -```typescript - -interface HostConfig { - id: number; // Unique identifier - host: string; // IP address or hostname - port: number; // Specify the port of the socket proxy - secure: boolean; // SSL/TLS support - name: string; // Human-readable name -} -``` - -## 📦 Container Operations - -### Container Lifecycle Management - -* **Start/Stop/Restart**: Full container lifecycle control -* **Pause/Unpause**: Container execution control -* **Create/Remove**: Container creation and deletion with force options -* **Kill with Signals**: Send specific signals to containers (SIGTERM, SIGKILL, etc.) -* **Rename**: Dynamic container renaming - -### Container Information - -* **List Containers**: Get all containers across hosts with filtering options -* **Detailed Info**: Comprehensive container inspection data -* **Port Mapping**: Automatic port mapping discovery and formatting -* **Label Management**: Container label inspection and management -* **Network Information**: Container network settings and connectivity - -### Container Execution - -* **Execute Commands**: Run commands inside containers with full stdio control -* **Working Directory**: Set custom working directories for command execution -* **Environment Variables**: Pass environment variables to executed commands -* **Exit Code Handling**: Capture and handle command exit codes - -### Container Logs - -* **Stream Logs**: Real-time log streaming from containers -* **Historical Logs**: Retrieve historical log data with filtering -* **Timestamp Support**: Include timestamps in log output -* **Line Limiting**: Control log output volume with tail options -* **Time Range Filtering**: Filter logs by time ranges (since/until) - -## 📊 Container Statistics & Monitoring - -### Real-Time Statistics - -* **CPU Usage**: Accurate CPU usage percentage calculation -* **Memory Usage**: Memory consumption and limits tracking -* **Network I/O**: Receive/transmit bytes tracking across all networks -* **Block I/O**: Disk read/write operations monitoring -* **Live Updates**: Real-time statistics with configurable intervals - -### Calculated Metrics - -* **Usage Percentages**: Automatic percentage calculations for resources -* **Resource Utilization**: Comprehensive resource utilization analysis -* **Performance Trends**: Historical performance data collection -* **Threshold Monitoring**: Configurable threshold-based alerts - -### Statistics Aggregation - -* **Host-Level Stats**: Aggregate statistics across all containers on a host -* **Cross-Host Stats**: Global statistics across all monitored hosts -* **Filtering**: Filter statistics by container state, image, or labels - -## 🖥️ Host Metrics & System Information - -### System Information - -* **Docker Version**: Docker daemon version and API version -* **Operating System**: Host OS information and architecture -* **Hardware Info**: CPU count, memory, and storage information -* **Kernel Version**: Host kernel version information -* **Runtime Info**: Docker runtime and driver information - -### Resource Metrics - -* **Total Resources**: Host-level CPU, memory, and storage totals -* **Usage Statistics**: Current resource utilization across the host -* **Container Counts**: Running, stopped, and paused container counts -* **Image Statistics**: Total images and storage usage - -### System Health - -* **Daemon Status**: Docker daemon health and connectivity -* **Resource Availability**: Available system resources -* **Storage Usage**: Docker storage driver and usage information -* **Network Status**: Docker network configuration and status - -## 📡 Real-Time Monitoring & Events - -### Automated Monitoring - -* **Health Checks**: Continuous host health monitoring with configurable intervals -* **Container Events**: Real-time container lifecycle event detection -* **Resource Monitoring**: Automatic resource usage monitoring -* **State Changes**: Detection of container and host state changes - -### Event System - -* **Typed Events**: Comprehensive event system with full TypeScript support -* **Event Categories**: Host events, container events, stream events, and error events -* **Event Context**: Rich context information for all events -* **Event Filtering**: Configurable event filtering and routing - -### Monitoring Configuration - -```typescript - -interface MonitoringOptions { - healthCheckInterval?: number; // Default: 30000ms - containerEventPollingInterval?: number; // Default: 5000ms - hostMetricsInterval?: number; // Default: 10000ms - enableContainerEvents?: boolean; // Default: true - enableHostMetrics?: boolean; // Default: true - enableHealthChecks?: boolean; // Default: true -} -``` - -## 🌊 Streaming & Real-Time Updates - -### WebSocket-Compatible Streaming - -* **Bidirectional Communication**: Full duplex communication for web UIs -* **Channel-Based Subscriptions**: Subscribe to specific data channels -* **Connection Management**: Automatic connection lifecycle management -* **Message Queuing**: Efficient message queuing and delivery - -### Stream Channels - -* **Container Stats**: Real-time container statistics streaming -* **Host Metrics**: Host-level metrics streaming -* **Container Lists**: Live container list updates -* **All Stats**: Combined container statistics and host metrics streaming -* **Docker Events**: Real-time Docker daemon events -* **Container Logs**: Live log streaming (planned) - -### Stream Features - -* **Configurable Intervals**: Customize update frequencies per channel -* **Filtering**: Filter streamed data by criteria (state, image, labels) -* **Multiplexing**: Multiple concurrent streams per connection -* **Backpressure Handling**: Intelligent backpressure management -* **Atomic Updates**: Combined stats ensure consistent timestamp across all data - -### Bun WebSocket Integration - -* **Native Bun Support**: Built-in Bun WebSocket server integration -* **High Performance**: Optimized for high-throughput scenarios -* **Test Interface**: Built-in HTML test client for development -* **Auto-Reconnection**: Client-side reconnection logic - -## 🖼️ Image Management - -### Image Operations - -* **List Images**: Retrieve all images with size and tag information -* **Pull Images**: Download images from registries with progress tracking -* **Image Inspection**: Detailed image metadata and layer information -* **Image Cleanup**: Planned: Image pruning and cleanup operations - -### Registry Support - -* **Multi-Registry**: Support for multiple Docker registries -* **Authentication**: Registry authentication support (planned) -* **Tag Management**: Image tag parsing and validation - -## 🌐 Network Management - -### Network Operations - -* **List Networks**: Retrieve all Docker networks with driver information -* **Network Inspection**: Detailed network configuration and connected containers -* **Network Filtering**: Filter networks by driver type or custom criteria - -### Network Information - -* **Driver Support**: Support for all Docker network drivers -* **Container Connectivity**: Network connectivity mapping between containers -* **IP Address Management**: Container IP address tracking within networks - -## 💾 Volume Management - -### Volume Operations - -* **List Volumes**: Retrieve all Docker volumes with mount information -* **Volume Inspection**: Detailed volume metadata and usage information -* **Mount Point Tracking**: Track volume mount points and usage - -### Storage Management - -* **Storage Drivers**: Support for all Docker storage drivers -* **Usage Analytics**: Volume usage tracking and analytics -* **Cleanup Operations**: Volume pruning and cleanup (planned) - -## 🔧 System Operations - -### System Information - -* **Disk Usage**: Docker system disk usage analysis with df command -* **Version Information**: Complete Docker version and component information -* **System Events**: System-level Docker events monitoring - -### Maintenance Operations - -* **System Pruning**: Clean up unused containers, networks, and images -* **Resource Cleanup**: Comprehensive system resource cleanup -* **Health Diagnostics**: System health diagnostics and reporting - -## 🛡️ Error Handling & Resilience - -### Retry Mechanisms - -* **Configurable Retries**: Customizable retry attempts and delays -* **Exponential Backoff**: Intelligent retry spacing -* **Circuit Breaker**: Prevent cascading failures -* **Graceful Degradation**: Continue operation when some hosts are unavailable - -### Error Classification - -* **Network Errors**: Connection and timeout error handling -* **API Errors**: Docker API error classification and handling -* **Validation Errors**: Input validation and sanitization -* **Resource Errors**: Resource exhaustion and limit handling - -### Logging & Debugging - -* **Structured Logging**: Comprehensive logging with context -* **Debug Information**: Detailed debug information for troubleshooting -* **Error Context**: Rich error context for better diagnostics -* **Performance Metrics**: Operation timing and performance tracking - -## 🔌 Utility Functions - -### Data Formatting - -* **Byte Formatting**: Human-readable byte size formatting (1.5 GB, 256 MB) -* **Time Formatting**: Uptime and duration formatting (1h 30m 45s) -* **Percentage Formatting**: CPU and memory percentage formatting -* **Rate Formatting**: Network and disk I/O rate formatting - -### Validation & Sanitization - -* **Container Names**: Docker container name validation and sanitization -* **Image Names**: Docker image name parsing and validation -* **Port Validation**: Port number and mapping validation -* **Label Validation**: Docker label key/value validation - -### Health Checks - -* **Container Health**: Container health assessment based on metrics -* **Host Health**: Host health evaluation with configurable thresholds -* **Resource Monitoring**: Resource usage threshold monitoring -* **Performance Analysis**: Performance bottleneck detection - -### Data Analysis - -* **Summary Generation**: Generate summaries for containers and hosts -* **Trend Analysis**: Basic trend analysis for metrics -* **Aggregation**: Data aggregation across hosts and containers -* **Filtering**: Advanced filtering capabilities - -## 🚀 Performance Optimizations - -### Efficient Operations - -* **Connection Reuse**: HTTP connection pooling and reuse -* **Parallel Processing**: Concurrent operations across multiple hosts -* **Caching**: Intelligent caching of frequently accessed data -* **Batch Operations**: Batch multiple operations for efficiency - -### Memory Management - -* **Stream Processing**: Memory-efficient stream processing -* **Garbage Collection**: Proper cleanup and garbage collection -* **Resource Pooling**: Efficient resource pooling and management -* **Memory Limits**: Configurable memory usage limits - -### Network Optimization - -* **Request Compression**: HTTP request/response compression -* **Keep-Alive**: HTTP keep-alive for persistent connections -* **Timeouts**: Intelligent timeout management -* **Rate Limiting**: Prevent API rate limit exhaustion - -## 🧪 Testing & Development - -### Test Suite - -* **Unit Tests**: Comprehensive unit test coverage -* **Integration Tests**: Full integration testing with Docker daemon -* **Mock Support**: Docker daemon mocking for testing -* **Error Scenarios**: Error condition testing and validation - -### Development Tools - -* **Example Applications**: Complete example applications and use cases -* **Documentation**: Comprehensive API documentation and guides -* **TypeScript Support**: Full TypeScript definitions and IntelliSense -* **Debug Utilities**: Development and debugging utilities - -### Example Applications - -* **Basic Usage**: Simple Docker client usage examples -* **WebSocket Server**: Complete WebSocket server implementation -* **Monitoring Dashboard**: Real-time monitoring dashboard example -* **CLI Tools**: Command-line interface examples - -## 📋 API Surface - -### Main Classes - -* **DockerClient**: Primary client class with all operations -* **HostHandler**: Docker host management -* **MonitoringManager**: Automated monitoring and events -* **StreamManager**: Real-time streaming and WebSocket support -* **DockerEventEmitter**: Type-safe event system - -### Key Functions - -* **getAllStats()**: Combined container statistics and host metrics in a single call -* **startAllStatsStream()**: Stream combined stats with configurable intervals -* **WebSocket all_stats channel**: Real-time combined stats for web applications - -### Utility Modules - -* **docker-helpers**: Utility functions for formatting and validation -* **WebSocket Integration**: Bun WebSocket server integration -* **Type Definitions**: Complete TypeScript type definitions - -### Configuration Interfaces - -* **DockerClientOptions**: Main client configuration -* **MonitoringOptions**: Monitoring system configuration -* **StreamOptions**: Streaming system configuration -* **WebSocket Options**: WebSocket server configuration - - \ \ No newline at end of file diff --git a/apps/docs/dockstat/troubleshooting/README.md b/apps/docs/dockstat/troubleshooting/README.md new file mode 100644 index 00000000..28b967e9 --- /dev/null +++ b/apps/docs/dockstat/troubleshooting/README.md @@ -0,0 +1,882 @@ +--- +id: 88a5f959-3f89-4266-9d8e-eb50193425b0 +title: Troubleshooting +collectionId: b4a5e48f-f103-480b-9f50-8f53f515cab9 +parentDocumentId: 7dddd764-6483-4f84-96a3-988304e772d3 +updatedAt: 2025-12-17T09:44:51.395Z +urlId: lgGFKJhiDO +--- + +> Comprehensive troubleshooting guide for DockStat applications and packages. This document covers common issues, diagnostic procedures, and solutions. + +## Diagnostic Overview + +```mermaidjs + +graph TB + subgraph "Issue Categories" + CONN["Connection Issues"] + PERF["Performance Issues"] + PLUGIN["Plugin Issues"] + DB["Database Issues"] + AUTH["Authentication Issues"] + BUILD["Build Issues"] + end + + subgraph "Diagnostic Tools" + LOGS["Logger Output"] + METRICS["Prometheus Metrics"] + API["API Health Checks"] + DOCKER["Docker Diagnostics"] + end + + CONN --> LOGS + CONN --> API + PERF --> METRICS + PERF --> LOGS + PLUGIN --> LOGS + PLUGIN --> DB + DB --> LOGS + AUTH --> LOGS + BUILD --> LOGS +``` + +## Quick Diagnostics + +### Health Check Endpoints + +```bash +# Check API status +curl http://localhost:9876/api/v2/docker/status + +# Check DockNode status +curl http://localhost:4000/api/status + +# Get Prometheus metrics +curl http://localhost:9876/api/v2/metrics +``` + +### Log Analysis + +```bash +# View API logs with filtering +DOCKSTAT_LOGGER_ONLY_SHOW="API,Docker" bun run dev + +# Show full file paths for debugging +DOCKSTAT_LOGGER_FULL_FILE_PATH=true bun run dev + +# Filter out noise +DOCKSTAT_LOGGER_IGNORE_MESSAGES="health check,heartbeat" bun run dev +``` + +## Connection Issues + +### Docker Daemon Connection Failed + +```mermaidjs + +flowchart TD + START["Docker Connection Failed"] --> CHECK_SOCKET{"Check Docker Socket"} + CHECK_SOCKET -->|"Socket exists"| CHECK_PERMS{"Check Permissions"} + CHECK_SOCKET -->|"Socket missing"| START_DOCKER["Start Docker daemon"] + CHECK_PERMS -->|"Permission denied"| ADD_GROUP["Add user to docker group"] + CHECK_PERMS -->|"Permissions OK"| CHECK_CONFIG{"Check host config"} + CHECK_CONFIG -->|"Config invalid"| FIX_CONFIG["Update host configuration"] + CHECK_CONFIG -->|"Config valid"| CHECK_NETWORK{"Check network"} + CHECK_NETWORK -->|"Network issue"| FIX_NETWORK["Fix network/firewall"] + CHECK_NETWORK -->|"Network OK"| CONTACT_SUPPORT["Check Docker logs"] + START_DOCKER --> SUCCESS["Connection established"] + ADD_GROUP --> SUCCESS + FIX_CONFIG --> SUCCESS + FIX_NETWORK --> SUCCESS +``` + +**Symptoms:** + +* "ENOENT" error when connecting to Docker +* "Permission denied" errors +* Connection timeout + +**Solutions:** + + +1. **Check Docker daemon is running:** + +```bash +# Linux/macOS + +sudo systemctl status docker +# or + +docker info +``` + + +2. **Check socket permissions:** + +```bash +ls -la /var/run/docker.sock +# Should show: srw-rw---- 1 root docker +``` + + +3. **Add user to docker group:** + +```bash +sudo usermod -aG docker $USER +# Log out and back in for changes to take effect +``` + + +4. **Verify host configuration:** + +```typescript +// Correct local socket configuration + +const hostConfig = { + id: 1, + host: "/var/run/docker.sock", + name: "Local Docker", + secure: false, + port: 0 // Port not used for socket connections +}; +``` + +### Remote Docker Host Connection Failed + +**Symptoms:** + +* "ECONNREFUSED" error +* "ETIMEDOUT" error +* TLS handshake failures + +**Solutions:** + + +1. **Check Docker is listening on TCP:** + +```bash +# On remote host + +sudo netstat -tlnp | grep 2375 +# or + +ss -tlnp | grep docker +``` + + +2. **Enable Docker TCP (without TLS - development only):** + +```bash +# Edit /etc/docker/daemon.json +{ + "hosts": ["unix:///var/run/docker.sock", "tcp://0.0.0.0:2375"] +} +``` + + +3. **Enable Docker TCP with TLS:** + +```bash +# Edit /etc/docker/daemon.json +{ + "hosts": ["unix:///var/run/docker.sock", "tcp://0.0.0.0:2376"], + "tls": true, + "tlscacert": "/etc/docker/ca.pem", + "tlscert": "/etc/docker/server-cert.pem", + "tlskey": "/etc/docker/server-key.pem", + "tlsverify": true +} +``` + + +4. **Check firewall rules:** + +```bash +# Allow Docker ports + +sudo ufw allow 2375/tcp # Unsecured +sudo ufw allow 2376/tcp # TLS +``` + +### API Connection Failed + +**Symptoms:** + +* Frontend shows "Failed to fetch" +* Eden client returns network errors +* CORS errors in browser console + +**Solutions:** + + +1. **Check API is running:** + +```bash +curl http://localhost:9876/api/v2/docker/status +``` + + +2. **Check CORS configuration:** + +```typescript +// apps/api/src/index.ts + +import { cors } from "@elysiajs/cors"; + +new Elysia() + .use(cors({ + origin: ["http://localhost:5173", "http://localhost:3000"], + methods: ["GET", "POST", "PUT", "DELETE"], + credentials: true + })) +``` + + +3. **Verify frontend API URL:** + +```typescript +// apps/dockstat/app/api.ts + +export const api = treaty("http://localhost:9876"); +``` + +## Performance Issues + +### Slow API Responses + +```mermaidjs + +flowchart TD + START["Slow API Response"] --> CHECK_WORKERS{"Check worker count"} + CHECK_WORKERS -->|"Too few workers"| INCREASE_WORKERS["Increase DOCKSTAT_MAX_WORKERS"] + CHECK_WORKERS -->|"Workers OK"| CHECK_DOCKER{"Check Docker response time"} + CHECK_DOCKER -->|"Docker slow"| OPTIMIZE_DOCKER["Optimize Docker queries"] + CHECK_DOCKER -->|"Docker fast"| CHECK_DB{"Check database"} + CHECK_DB -->|"DB slow"| OPTIMIZE_DB["Optimize database"] + CHECK_DB -->|"DB fast"| CHECK_PLUGINS{"Check plugin overhead"} + CHECK_PLUGINS -->|"Plugin issue"| DISABLE_PLUGINS["Disable problematic plugins"] + CHECK_PLUGINS -->|"Plugins OK"| PROFILE["Profile application"] + INCREASE_WORKERS --> SUCCESS["Performance improved"] + OPTIMIZE_DOCKER --> SUCCESS + OPTIMIZE_DB --> SUCCESS + DISABLE_PLUGINS --> SUCCESS +``` + +**Diagnostic Steps:** + + +1. **Check worker pool status:** + +```bash +curl http://localhost:9876/api/v2/docker/manager/pool-stats +``` + + +2. **Increase worker threads:** + +```bash +DOCKSTAT_MAX_WORKERS=100 bun run dev +``` + + +3. **Enable server timing traces:** + +```bash +DOCKSTATAPI_SHOW_TRACES=true bun run dev +# Check response headers for timing information +``` + +### High Memory Usage + +**Symptoms:** + +* Process memory grows continuously +* Out of memory errors +* System slowdown + +**Solutions:** + + +1. **Monitor memory usage:** + +```typescript +import Logger from "@dockstat/logger"; + +const log = new Logger("Memory"); + +setInterval(() => { + const usage = process.memoryUsage(); + log.info(`RSS: ${Math.round(usage.rss / 1024 / 1024)}MB, Heap: ${Math.round(usage.heapUsed / 1024 / 1024)}MB`); +}, 30000); +``` + + +2. **Limit monitoring frequency:** + +```typescript +const client = new DockerClient(db.getDB(), { + enableMonitoring: true, + monitoringInterval: 10000 // 10 seconds instead of default +}); +``` + + +3. **Clean up event listeners:** + +```typescript +// Properly remove listeners when done + +streamManager.unsubscribe(STREAM_CHANNELS.CONTAINER_STATS, handler); +``` + +### Database Performance + +**Symptoms:** + +* Slow queries +* Database file growing large +* Lock contention + +**Solutions:** + + +1. **Enable WAL mode:** + +```typescript +import { DB } from "@dockstat/sqlite-wrapper"; + +const db = new DB("app.db", { + pragmas: [ + ["journal_mode", "WAL"], + ["synchronous", "NORMAL"], + ["cache_size", "-64000"], // 64MB cache + ["temp_store", "MEMORY"] + ] +}); +``` + + +2. **Add indexes for frequent queries:** + +```typescript +// After table creation + +db.exec(`CREATE INDEX IF NOT EXISTS idx_containers_host ON containers(host_id)`); +db.exec(`CREATE INDEX IF NOT EXISTS idx_events_timestamp ON events(timestamp)`); +``` + + +3. **Vacuum database periodically:** + +```typescript +// Run during maintenance window + +db.exec("VACUUM"); +db.exec("ANALYZE"); +``` + +## Plugin Issues + +### Plugin Won't Load + +```mermaidjs + +flowchart TD + START["Plugin Load Failed"] --> CHECK_INSTALLED{"Is plugin installed?"} + CHECK_INSTALLED -->|"Not installed"| INSTALL["Install plugin"] + CHECK_INSTALLED -->|"Installed"| CHECK_CODE{"Check plugin code"} + CHECK_CODE -->|"Syntax error"| FIX_CODE["Fix plugin syntax"] + CHECK_CODE -->|"Code OK"| CHECK_DEPS{"Check dependencies"} + CHECK_DEPS -->|"Missing deps"| INSTALL_DEPS["Install dependencies"] + CHECK_DEPS -->|"Deps OK"| CHECK_CONFIG{"Check plugin config"} + CHECK_CONFIG -->|"Invalid config"| FIX_CONFIG["Fix configuration"] + CHECK_CONFIG -->|"Config OK"| CHECK_CONFLICTS{"Check conflicts"} + CHECK_CONFLICTS -->|"Conflict found"| RESOLVE_CONFLICT["Resolve plugin conflict"] + CHECK_CONFLICTS -->|"No conflicts"| DEBUG["Enable debug logging"] + INSTALL --> SUCCESS["Plugin loaded"] + FIX_CODE --> SUCCESS + INSTALL_DEPS --> SUCCESS + FIX_CONFIG --> SUCCESS + RESOLVE_CONFLICT --> SUCCESS +``` + +**Diagnostic Steps:** + + +1. **Check installed plugins:** + +```typescript +const plugins = await pluginHandler.getAll(); +console.log("Installed plugins:", plugins); +``` + + +2. **Check plugin status:** + +```typescript +const status = await pluginHandler.getStatus(); +console.log("Plugin status:", status); +``` + + +3. **Attempt to load with error capture:** + +```typescript +const result = await pluginHandler.loadPlugins([pluginId]); +if (result.errors.length > 0) { + for (const err of result.errors) { + console.error(`Plugin ${err.pluginId} failed:`, err.error); + } +} +``` + +### Plugin Routes Not Working + +**Symptoms:** + +* 404 errors on plugin routes +* Routes not listed in plugin routes endpoint +* Incorrect responses + +**Solutions:** + + +1. **Verify plugin routes are registered:** + +```typescript +const routes = await pluginHandler.getAllPluginRoutes(); +console.log("Available routes:", routes); +``` + + +2. **Check route configuration:** + +```typescript +// Correct route definition + +const config = { + apiRoutes: { + "/status": { // Must start with / + method: "GET", // Must be uppercase + actions: ["getStatus"] // Actions must exist + } + } +}; +``` + + +3. **Test route directly:** + +```bash +curl http://localhost:9876/api/v2/plugins/1/routes/status +``` + +### Plugin Database Table Issues + +**Symptoms:** + +* "Table not found" errors +* Data not persisting +* Column type mismatches + +**Solutions:** + + +1. **Check if table was created:** + +```typescript +const schema = db.getSchema(); +console.log("Tables:", schema); +``` + + +2. **Verify column definitions:** + +```typescript +import { column } from "@dockstat/sqlite-wrapper"; + +const config = { + table: { + name: "plugin_data", + columns: { + id: column.id(), // Primary key + data: column.json(), // JSON column + created_at: column.createdAt() // Timestamp + }, + jsonColumns: ["data"] // Must list JSON columns + } +}; +``` + + +3. **Drop and recreate table (development only):** + +```typescript +db.exec(`DROP TABLE IF EXISTS plugin_data`); +// Reload plugin to recreate table + +await pluginHandler.loadPlugins([pluginId]); +``` + +## Database Issues + +### Database Locked + +```mermaidjs + +flowchart TD + START["Database Locked Error"] --> CHECK_CONNECTIONS{"Check open connections"} + CHECK_CONNECTIONS -->|"Multiple processes"| SINGLE_PROCESS["Use single process or WAL"] + CHECK_CONNECTIONS -->|"Single process"| CHECK_TRANSACTIONS{"Check open transactions"} + CHECK_TRANSACTIONS -->|"Uncommitted txn"| COMMIT_TXN["Commit or rollback transaction"] + CHECK_TRANSACTIONS -->|"No open txn"| CHECK_TIMEOUT{"Check busy timeout"} + CHECK_TIMEOUT -->|"Timeout too low"| INCREASE_TIMEOUT["Increase busy_timeout PRAGMA"] + CHECK_TIMEOUT -->|"Timeout OK"| CHECK_WAL{"WAL mode enabled?"} + CHECK_WAL -->|"Not enabled"| ENABLE_WAL["Enable WAL mode"] + CHECK_WAL -->|"Enabled"| INVESTIGATE["Investigate deadlock"] + SINGLE_PROCESS --> SUCCESS["Database unlocked"] + COMMIT_TXN --> SUCCESS + INCREASE_TIMEOUT --> SUCCESS + ENABLE_WAL --> SUCCESS +``` + +**Solutions:** + + +1. **Enable WAL mode:** + +```typescript +const db = new DB("app.db", { + pragmas: [ + ["journal_mode", "WAL"] + ] +}); +``` + + +2. **Set busy timeout:** + +```typescript +const db = new DB("app.db", { + pragmas: [ + ["busy_timeout", "5000"] // 5 seconds + ] +}); +``` + + +3. **Use transactions properly:** + +```typescript +// Wrap related operations in a transaction + +db.transaction(() => { + table.insert({ ... }); + table.update({ ... }).where({ ... }).run(); +}); +``` + +### Database Corruption + +**Symptoms:** + +* "Database disk image is malformed" +* Unexpected query results +* Crashes on database operations + +**Solutions:** + + +1. **Check database integrity:** + +```bash +sqlite3 dockstat.db "PRAGMA integrity_check;" +``` + + +2. **Attempt recovery:** + +```bash +# Backup corrupted database +cp dockstat.db dockstat.db.corrupt + +# Export and reimport +sqlite3 dockstat.db.corrupt ".dump" | sqlite3 dockstat.db.new + +# Replace if successful +mv dockstat.db.new dockstat.db +``` + + +3. **Restore from backup:** + +```bash +# If backups exist +cp /backup/dockstat.db.bak dockstat.db +``` + +## Authentication Issues + +### DockNode Authentication Failed + +**Symptoms:** + +* 401 Unauthorized responses +* "Invalid PSK" errors +* Authentication header missing + +**Solutions:** + + +1. **Verify PSK is set:** + +```bash +# Check environment variable +echo $DOCKNODE_DOCKSTACK_AUTH_PSK +``` + + +2. **Check authentication priority:** + +```bash +# For production +DOCKNODE_DOCKSTACK_AUTH_PRIORITY=psk + +# For development +DOCKNODE_DOCKSTACK_AUTH_PRIORITY=dev +``` + + +3. **Include authentication header:** + +```typescript +const response = await fetch(`${DOCKNODE_URL}/api/dockstack/deploy`, { + method: "POST", + headers: { + "Content-Type": "application/json", + "Authorization": `Bearer ${AUTH_TOKEN}`, + "X-DockNode-PSK": PSK_VALUE // If using PSK auth + }, + body: JSON.stringify(data) +}); +``` + +## Build Issues + +### TypeScript Compilation Errors + +```mermaidjs + +flowchart TD + START["TypeScript Error"] --> CHECK_TYPES{"Check type imports"} + CHECK_TYPES -->|"Import error"| FIX_IMPORT["Fix import path"] + CHECK_TYPES -->|"Imports OK"| CHECK_DEPS{"Check package dependencies"} + CHECK_DEPS -->|"Missing package"| INSTALL_PKG["Install missing package"] + CHECK_DEPS -->|"Deps OK"| CHECK_TSCONFIG{"Check tsconfig.json"} + CHECK_TSCONFIG -->|"Config issue"| FIX_TSCONFIG["Fix TypeScript config"] + CHECK_TSCONFIG -->|"Config OK"| CHECK_VERSION{"Check TS version"} + CHECK_VERSION -->|"Version mismatch"| UPDATE_TS["Update TypeScript"] + CHECK_VERSION -->|"Version OK"| CLEAN_BUILD["Clean and rebuild"] + FIX_IMPORT --> SUCCESS["Build successful"] + INSTALL_PKG --> SUCCESS + FIX_TSCONFIG --> SUCCESS + UPDATE_TS --> SUCCESS + CLEAN_BUILD --> SUCCESS +``` + +**Solutions:** + + +1. **Clear build cache:** + +```bash +rm -rf node_modules/.cache + +rm -rf .turbo + +rm -rf dist + +bun install +``` + + +2. **Check type imports:** + +```typescript +// Correct import from @dockstat/typings + +import type { DOCKER, PLUGIN, THEME } from "@dockstat/typings"; + +// Not default import +// import DOCKER from "@dockstat/typings"; // Wrong! +``` + + +3. **Verify tsconfig extends base:** + +```json +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "./dist" + } +} +``` + +### Turborepo Build Failures + +**Symptoms:** + +* "Task not found" errors +* Dependency resolution issues +* Cache invalidation problems + +**Solutions:** + + +1. **Clear Turborepo cache:** + +```bash +bun run turbo clean +# or + +rm -rf .turbo node_modules/.cache +``` + + +2. **Force rebuild:** + +```bash +bun run build --force +``` + + +3. **Check turbo.json pipeline:** + +```json +{ + "pipeline": { + "build": { + "dependsOn": ["^build"], + "outputs": ["dist/**", "build/**"] + } + } +} +``` + +## Logging and Debugging + +### Enable Verbose Logging + +```bash +# Show all loggers +unset DOCKSTAT_LOGGER_DISABLED_LOGGERS +unset DOCKSTAT_LOGGER_ONLY_SHOW + +# Show full file paths +DOCKSTAT_LOGGER_FULL_FILE_PATH=true + +# Run in development mode +bun run dev +``` + +### Custom Debug Logger + +```typescript +import Logger from "@dockstat/logger"; + +const debug = new Logger("Debug"); + +// Add debug points throughout code +debug.debug("Entering function X"); +debug.debug(`Variable state: ${JSON.stringify(data)}`); +debug.debug("Exiting function X"); +``` + +### Request Tracing + +```typescript +import Logger from "@dockstat/logger"; + +const log = new Logger("API"); + +// Middleware for request tracing +app.onRequest(({ request }) => { + const reqId = request.headers.get("x-request-id") || crypto.randomUUID(); + log.setReqFrom(reqId, request.headers.get("x-forwarded-for") || "unknown"); + log.info(`${request.method} ${new URL(request.url).pathname}`, reqId); +}); + +app.onAfterResponse(({ request }) => { + const reqId = request.headers.get("x-request-id"); + if (reqId) { + log.clearReqFrom(reqId); + } +}); +``` + +## Common Error Messages + +| Error Message | Likely Cause | Solution | +|----|----|----| +| `ENOENT: no such file or directory` | File/socket doesn't exist | Check path, start Docker daemon | +| `ECONNREFUSED` | Service not running | Start the service | +| `ETIMEDOUT` | Network/firewall issue | Check network connectivity | +| `SQLITE_BUSY` | Database locked | Enable WAL mode, check connections | +| `SQLITE_CORRUPT` | Database corruption | Restore from backup or recover | +| `TypeError: Cannot read property` | Null/undefined access | Add null checks | +| `Plugin validation failed` | Invalid plugin config | Check plugin manifest | +| `CORS error` | Cross-origin blocked | Configure CORS properly | + +## Getting Help + +### Collecting Diagnostic Information + +When reporting issues, collect: + + +1. **System information:** + +```bash +bun --version +docker --version +uname -a +``` + + +2. **Application logs:** + +```bash +DOCKSTAT_LOGGER_FULL_FILE_PATH=true bun run dev 2>&1 | tee debug.log +``` + + +3. **API status:** + +```bash +curl http://localhost:9876/api/v2/docker/status > status.json +curl http://localhost:9876/api/v2/plugins/status > plugins.json +``` + + +4. **Database schema:** + +```typescript +const schema = db.getSchema(); +console.log(JSON.stringify(schema, null, 2)); +``` + +### Support Channels + +* **GitHub Issues**: [github.com/Its4Nik/DockStat/issues](https://github.com/Its4Nik/DockStat/issues) +* **Wiki**: [outline.itsnik.de](https://outline.itsnik.de/s/9d88c471-373e-4ef2-a955-b1058eb7dc99) + +## Related Documentation + +| Section | Description | +|----|----| +| [Configuration](/doc/dec1cb2c-9a13-4e67-a31c-d3a685391208) | Configuration options and settings | +| [Architecture](/doc/d56ca448-563a-4206-9585-c45f8f6be5cf) | System design for debugging context | +| [API Reference](/doc/b174143d-f906-4f8d-8cb5-9fc96512e575) | API endpoints for diagnostics | +| [Integration guide](/doc/e4e04545-fd9f-4fbf-becb-94da81f48bc5) | Component interaction details | \ No newline at end of file diff --git a/bun.lock b/bun.lock index 5b79e8bb..954c993d 100644 --- a/bun.lock +++ b/bun.lock @@ -165,8 +165,8 @@ }, }, "packages/outline-sync": { - "name": "outline-sync", - "version": "1.2.0", + "name": "@dockstat/outline-sync", + "version": "1.2.4", "bin": { "outline-sync": "dist/index.js", }, @@ -400,6 +400,8 @@ "@dockstat/logger": ["@dockstat/logger@workspace:packages/logger"], + "@dockstat/outline-sync": ["@dockstat/outline-sync@workspace:packages/outline-sync"], + "@dockstat/plugin-handler": ["@dockstat/plugin-handler@workspace:packages/plugin-handler"], "@dockstat/sqlite-wrapper": ["@dockstat/sqlite-wrapper@workspace:packages/sqlite-wrapper"], @@ -418,57 +420,57 @@ "@elysiajs/server-timing": ["@elysiajs/server-timing@1.4.0", "", { "peerDependencies": { "elysia": ">= 1.4.0" } }, "sha512-vDFdHyi8Q43vgA5MaTQMA9v4/bgKrtqPrpVqVuHlMCRQgfOpvYGXPj3okSttyendG5r2bRHfyPG11lTWWIrzrQ=="], - "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="], + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw=="], - "@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="], + "@esbuild/android-arm": ["@esbuild/android-arm@0.27.2", "", { "os": "android", "cpu": "arm" }, "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA=="], - "@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.12", "", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="], + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.2", "", { "os": "android", "cpu": "arm64" }, "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA=="], - "@esbuild/android-x64": ["@esbuild/android-x64@0.25.12", "", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="], + "@esbuild/android-x64": ["@esbuild/android-x64@0.27.2", "", { "os": "android", "cpu": "x64" }, "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A=="], - "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="], + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg=="], - "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="], + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA=="], - "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="], + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g=="], - "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="], + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA=="], - "@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.12", "", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="], + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.2", "", { "os": "linux", "cpu": "arm" }, "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw=="], - "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="], + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw=="], - "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="], + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.2", "", { "os": "linux", "cpu": "ia32" }, "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w=="], - "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="], + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.2", "", { "os": "linux", "cpu": "none" }, "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg=="], - "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="], + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.2", "", { "os": "linux", "cpu": "none" }, "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw=="], - "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="], + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ=="], - "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="], + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.2", "", { "os": "linux", "cpu": "none" }, "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA=="], - "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="], + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w=="], - "@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.12", "", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="], + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.2", "", { "os": "linux", "cpu": "x64" }, "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA=="], - "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg=="], + "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.2", "", { "os": "none", "cpu": "arm64" }, "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw=="], - "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.12", "", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="], + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.2", "", { "os": "none", "cpu": "x64" }, "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA=="], - "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.12", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A=="], + "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.2", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA=="], - "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="], + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.2", "", { "os": "openbsd", "cpu": "x64" }, "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg=="], - "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg=="], + "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.2", "", { "os": "none", "cpu": "arm64" }, "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag=="], - "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="], + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.2", "", { "os": "sunos", "cpu": "x64" }, "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg=="], - "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="], + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg=="], - "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="], + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ=="], - "@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="], + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.2", "", { "os": "win32", "cpu": "x64" }, "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ=="], "@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.0", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g=="], @@ -482,13 +484,13 @@ "@eslint/eslintrc": ["@eslint/eslintrc@3.3.3", "", { "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.1", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" } }, "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ=="], - "@eslint/js": ["@eslint/js@9.39.1", "", {}, "sha512-S26Stp4zCy88tH94QbBv3XCuzRQiZ9yXofEILmglYTh/Ug/a9/umqvgFtYBAo3Lp0nsI/5/qH1CCrbdK3AP1Tw=="], + "@eslint/js": ["@eslint/js@9.39.2", "", {}, "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA=="], "@eslint/object-schema": ["@eslint/object-schema@2.1.7", "", {}, "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA=="], "@eslint/plugin-kit": ["@eslint/plugin-kit@0.4.1", "", { "dependencies": { "@eslint/core": "^0.17.0", "levn": "^0.4.1" } }, "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA=="], - "@grpc/grpc-js": ["@grpc/grpc-js@1.14.2", "", { "dependencies": { "@grpc/proto-loader": "^0.8.0", "@js-sdsl/ordered-map": "^4.4.2" } }, "sha512-QzVUtEFyu05UNx2xr0fCQmStUO17uVQhGNowtxs00IgTZT6/W2PBLfUkj30s0FKJ29VtTa3ArVNIhNP6akQhqA=="], + "@grpc/grpc-js": ["@grpc/grpc-js@1.14.3", "", { "dependencies": { "@grpc/proto-loader": "^0.8.0", "@js-sdsl/ordered-map": "^4.4.2" } }, "sha512-Iq8QQQ/7X3Sac15oB6p0FmUg/klxQvXLeileoqrTRGJYLV+/9tubbr9ipz0GKHjmXVsgFPo/+W+2cA8eNcR+XA=="], "@grpc/proto-loader": ["@grpc/proto-loader@0.7.15", "", { "dependencies": { "lodash.camelcase": "^4.3.0", "long": "^5.0.0", "protobufjs": "^7.2.5", "yargs": "^17.7.2" }, "bin": { "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" } }, "sha512-tMXdRCfYVixjuFK+Hk0Q1s38gV9zDiDJfWL3h1rv4Qc39oILCu1TRTDt7+fGUI8K4G1Fj125Hx/ru3azECWTyQ=="], @@ -500,9 +502,13 @@ "@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.3", "", {}, "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ=="], + "@isaacs/balanced-match": ["@isaacs/balanced-match@4.0.1", "", {}, "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ=="], + + "@isaacs/brace-expansion": ["@isaacs/brace-expansion@5.0.0", "", { "dependencies": { "@isaacs/balanced-match": "^4.0.1" } }, "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA=="], + "@isaacs/cliui": ["@isaacs/cliui@8.0.2", "", { "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="], - "@joshwooding/vite-plugin-react-docgen-typescript": ["@joshwooding/vite-plugin-react-docgen-typescript@0.6.1", "", { "dependencies": { "glob": "^10.0.0", "magic-string": "^0.30.0", "react-docgen-typescript": "^2.2.2" }, "peerDependencies": { "typescript": ">= 4.3.x", "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" }, "optionalPeers": ["typescript"] }, "sha512-J4BaTocTOYFkMHIra1JDWrMWpNmBl4EkplIwHEsV8aeUOtdWjwSnln9U7twjMFTAEB7mptNtSKyVi1Y2W9sDJw=="], + "@joshwooding/vite-plugin-react-docgen-typescript": ["@joshwooding/vite-plugin-react-docgen-typescript@0.6.3", "", { "dependencies": { "glob": "^11.1.0", "react-docgen-typescript": "^2.2.2" }, "peerDependencies": { "typescript": ">= 4.3.x", "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" }, "optionalPeers": ["typescript"] }, "sha512-9TGZuAX+liGkNKkwuo3FYJu7gHWT0vkBcf7GkOe7s7fmC19XwH/4u5u7sDIFrMooe558ORcmuBvBz7Ur5PlbHw=="], "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], @@ -520,8 +526,6 @@ "@pinojs/redact": ["@pinojs/redact@0.4.0", "", {}, "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg=="], - "@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="], - "@protobufjs/aspromise": ["@protobufjs/aspromise@1.1.2", "", {}, "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ=="], "@protobufjs/base64": ["@protobufjs/base64@1.1.2", "", {}, "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg=="], @@ -542,71 +546,71 @@ "@protobufjs/utf8": ["@protobufjs/utf8@1.1.0", "", {}, "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw=="], - "@react-router/dev": ["@react-router/dev@7.10.0", "", { "dependencies": { "@babel/core": "^7.27.7", "@babel/generator": "^7.27.5", "@babel/parser": "^7.27.7", "@babel/plugin-syntax-jsx": "^7.27.1", "@babel/preset-typescript": "^7.27.1", "@babel/traverse": "^7.27.7", "@babel/types": "^7.27.7", "@react-router/node": "7.10.0", "@remix-run/node-fetch-server": "^0.9.0", "arg": "^5.0.1", "babel-dead-code-elimination": "^1.0.6", "chokidar": "^4.0.0", "dedent": "^1.5.3", "es-module-lexer": "^1.3.1", "exit-hook": "2.2.1", "isbot": "^5.1.11", "jsesc": "3.0.2", "lodash": "^4.17.21", "p-map": "^7.0.3", "pathe": "^1.1.2", "picocolors": "^1.1.1", "pkg-types": "^2.3.0", "prettier": "^3.6.2", "react-refresh": "^0.14.0", "semver": "^7.3.7", "tinyglobby": "^0.2.14", "valibot": "^1.1.0", "vite-node": "^3.2.2" }, "peerDependencies": { "@react-router/serve": "^7.10.0", "@vitejs/plugin-rsc": "*", "react-router": "^7.10.0", "typescript": "^5.1.0", "vite": "^5.1.0 || ^6.0.0 || ^7.0.0", "wrangler": "^3.28.2 || ^4.0.0" }, "optionalPeers": ["@react-router/serve", "@vitejs/plugin-rsc", "typescript", "wrangler"], "bin": { "react-router": "bin.js" } }, "sha512-3UgkV0N5lp3+Ol3q64L4ZHgPXv2XA4KHJ59MVLSK2prokrOrPaYvqbdx40r602M+hRZp/u04ln2A6cOfBW6kxA=="], + "@react-router/dev": ["@react-router/dev@7.10.1", "", { "dependencies": { "@babel/core": "^7.27.7", "@babel/generator": "^7.27.5", "@babel/parser": "^7.27.7", "@babel/plugin-syntax-jsx": "^7.27.1", "@babel/preset-typescript": "^7.27.1", "@babel/traverse": "^7.27.7", "@babel/types": "^7.27.7", "@react-router/node": "7.10.1", "@remix-run/node-fetch-server": "^0.9.0", "arg": "^5.0.1", "babel-dead-code-elimination": "^1.0.6", "chokidar": "^4.0.0", "dedent": "^1.5.3", "es-module-lexer": "^1.3.1", "exit-hook": "2.2.1", "isbot": "^5.1.11", "jsesc": "3.0.2", "lodash": "^4.17.21", "p-map": "^7.0.3", "pathe": "^1.1.2", "picocolors": "^1.1.1", "pkg-types": "^2.3.0", "prettier": "^3.6.2", "react-refresh": "^0.14.0", "semver": "^7.3.7", "tinyglobby": "^0.2.14", "valibot": "^1.2.0", "vite-node": "^3.2.2" }, "peerDependencies": { "@react-router/serve": "^7.10.1", "@vitejs/plugin-rsc": "*", "react-router": "^7.10.1", "typescript": "^5.1.0", "vite": "^5.1.0 || ^6.0.0 || ^7.0.0", "wrangler": "^3.28.2 || ^4.0.0" }, "optionalPeers": ["@react-router/serve", "@vitejs/plugin-rsc", "typescript", "wrangler"], "bin": { "react-router": "bin.js" } }, "sha512-kap9O8rTN6b3vxjd+0SGjhm5vqiAZHMmOX1Hc7Y4KXRVVdusn+0+hxs44cDSfbW6Z6fCLw6GXXe0Kr+DJIRezw=="], - "@react-router/express": ["@react-router/express@7.10.0", "", { "dependencies": { "@react-router/node": "7.10.0" }, "peerDependencies": { "express": "^4.17.1 || ^5", "react-router": "7.10.0", "typescript": "^5.1.0" }, "optionalPeers": ["typescript"] }, "sha512-3cBJ2cyHn5J+wSNFn+XdNSpXVAlQ+nbj7CMa3OsiEpFb+d0GLthirvSESqRjX2Eid94xNHICqKpYS9bR4QqIxg=="], + "@react-router/express": ["@react-router/express@7.10.1", "", { "dependencies": { "@react-router/node": "7.10.1" }, "peerDependencies": { "express": "^4.17.1 || ^5", "react-router": "7.10.1", "typescript": "^5.1.0" }, "optionalPeers": ["typescript"] }, "sha512-O7xjg6wWHfrsnPyVWgQG+tCamIE09SqLqtHwa1tAFzKPjcDpCw4S4+/OkJvNXLtBL60H3VhZ1r2OQgXBgGOMpw=="], - "@react-router/fs-routes": ["@react-router/fs-routes@7.10.0", "", { "dependencies": { "minimatch": "^9.0.0" }, "peerDependencies": { "@react-router/dev": "^7.10.0", "typescript": "^5.1.0" }, "optionalPeers": ["typescript"] }, "sha512-1lbSj/P/xg7mBlsUR9XJbjMjlA0P/YuN67fv9GHTZDitYjMwGra7M05+V38xW/l4saK/neVrIU8GfYCRnxEXCQ=="], + "@react-router/fs-routes": ["@react-router/fs-routes@7.10.1", "", { "dependencies": { "minimatch": "^9.0.0" }, "peerDependencies": { "@react-router/dev": "^7.10.1", "typescript": "^5.1.0" }, "optionalPeers": ["typescript"] }, "sha512-iqMibGPehjHN0biBjz/SZ/Q1NyRsUsKYvP86TiIQv5vi8YRUUm80CEugBLrzu2FsuMybIGpwHcMpAB/QwVz2cw=="], - "@react-router/node": ["@react-router/node@7.10.0", "", { "dependencies": { "@mjackson/node-fetch-server": "^0.2.0" }, "peerDependencies": { "react-router": "7.10.0", "typescript": "^5.1.0" }, "optionalPeers": ["typescript"] }, "sha512-pff3Xz3gASrIUUX54QdlPzasdN9XRLnzoFEwUVsH5y2sZ6vijQdjZExLS6aQhPiuUr/uVPwN2WngO0Ryfrxulg=="], + "@react-router/node": ["@react-router/node@7.10.1", "", { "dependencies": { "@mjackson/node-fetch-server": "^0.2.0" }, "peerDependencies": { "react-router": "7.10.1", "typescript": "^5.1.0" }, "optionalPeers": ["typescript"] }, "sha512-RLmjlR1zQu+ve8ibI0lu91pJrXGcmfkvsrQl7z/eTc5V5FZgl0OvQVWL5JDWBlBZyzdLMQQekUOX5WcPhCP1FQ=="], - "@react-router/serve": ["@react-router/serve@7.10.0", "", { "dependencies": { "@mjackson/node-fetch-server": "^0.2.0", "@react-router/express": "7.10.0", "@react-router/node": "7.10.0", "compression": "^1.7.4", "express": "^4.19.2", "get-port": "5.1.1", "morgan": "^1.10.0", "source-map-support": "^0.5.21" }, "peerDependencies": { "react-router": "7.10.0" }, "bin": { "react-router-serve": "bin.js" } }, "sha512-tgdbw1lmDkzF3gCMj//iNklgUrYHUxz35rj0sbyLeti8K2gVsNxaZWyt5omanFgkeZ7WYfi0wzLHviqxl228eA=="], + "@react-router/serve": ["@react-router/serve@7.10.1", "", { "dependencies": { "@mjackson/node-fetch-server": "^0.2.0", "@react-router/express": "7.10.1", "@react-router/node": "7.10.1", "compression": "^1.7.4", "express": "^4.19.2", "get-port": "5.1.1", "morgan": "^1.10.0", "source-map-support": "^0.5.21" }, "peerDependencies": { "react-router": "7.10.1" }, "bin": { "react-router-serve": "bin.js" } }, "sha512-qYco7sFpbRgoKJKsCgJmFBQwaLVsLv255K8vbPodnXe13YBEzV/ugIqRCYVz2hghvlPiEKgaHh2On0s/5npn6w=="], "@remix-run/node-fetch-server": ["@remix-run/node-fetch-server@0.9.0", "", {}, "sha512-SoLMv7dbH+njWzXnOY6fI08dFMI5+/dQ+vY3n8RnnbdG7MdJEgiP28Xj/xWlnRnED/aB6SFw56Zop+LbmaaKqA=="], - "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.47", "", {}, "sha512-8QagwMH3kNCuzD8EWL8R2YPW5e4OrHNSAHRFDdmFqEwEaD/KcNKjVoumo+gP2vW5eKB2UPbM6vTYiGZX0ixLnw=="], + "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.53", "", {}, "sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ=="], "@rollup/pluginutils": ["@rollup/pluginutils@5.3.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^2.0.2", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q=="], - "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.53.3", "", { "os": "android", "cpu": "arm" }, "sha512-mRSi+4cBjrRLoaal2PnqH82Wqyb+d3HsPUN/W+WslCXsZsyHa9ZeQQX/pQsZaVIWDkPcpV6jJ+3KLbTbgnwv8w=="], + "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.53.5", "", { "os": "android", "cpu": "arm" }, "sha512-iDGS/h7D8t7tvZ1t6+WPK04KD0MwzLZrG0se1hzBjSi5fyxlsiggoJHwh18PCFNn7tG43OWb6pdZ6Y+rMlmyNQ=="], - "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.53.3", "", { "os": "android", "cpu": "arm64" }, "sha512-CbDGaMpdE9sh7sCmTrTUyllhrg65t6SwhjlMJsLr+J8YjFuPmCEjbBSx4Z/e4SmDyH3aB5hGaJUP2ltV/vcs4w=="], + "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.53.5", "", { "os": "android", "cpu": "arm64" }, "sha512-wrSAViWvZHBMMlWk6EJhvg8/rjxzyEhEdgfMMjREHEq11EtJ6IP6yfcCH57YAEca2Oe3FNCE9DSTgU70EIGmVw=="], - "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.53.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Nr7SlQeqIBpOV6BHHGZgYBuSdanCXuw09hon14MGOLGmXAFYjx1wNvquVPmpZnl0tLjg25dEdr4IQ6GgyToCUA=="], + "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.53.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-S87zZPBmRO6u1YXQLwpveZm4JfPpAa6oHBX7/ghSiGH3rz/KDgAu1rKdGutV+WUI6tKDMbaBJomhnT30Y2t4VQ=="], - "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.53.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-DZ8N4CSNfl965CmPktJ8oBnfYr3F8dTTNBQkRlffnUarJ2ohudQD17sZBa097J8xhQ26AwhHJ5mvUyQW8ddTsQ=="], + "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.53.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-YTbnsAaHo6VrAczISxgpTva8EkfQus0VPEVJCEaboHtZRIb6h6j0BNxRBOwnDciFTZLDPW5r+ZBmhL/+YpTZgA=="], - "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.53.3", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-yMTrCrK92aGyi7GuDNtGn2sNW+Gdb4vErx4t3Gv/Tr+1zRb8ax4z8GWVRfr3Jw8zJWvpGHNpss3vVlbF58DZ4w=="], + "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.53.5", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-1T8eY2J8rKJWzaznV7zedfdhD1BqVs1iqILhmHDq/bqCUZsrMt+j8VCTHhP0vdfbHK3e1IQ7VYx3jlKqwlf+vw=="], - "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.53.3", "", { "os": "freebsd", "cpu": "x64" }, "sha512-lMfF8X7QhdQzseM6XaX0vbno2m3hlyZFhwcndRMw8fbAGUGL3WFMBdK0hbUBIUYcEcMhVLr1SIamDeuLBnXS+Q=="], + "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.53.5", "", { "os": "freebsd", "cpu": "x64" }, "sha512-sHTiuXyBJApxRn+VFMaw1U+Qsz4kcNlxQ742snICYPrY+DDL8/ZbaC4DVIB7vgZmp3jiDaKA0WpBdP0aqPJoBQ=="], - "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.53.3", "", { "os": "linux", "cpu": "arm" }, "sha512-k9oD15soC/Ln6d2Wv/JOFPzZXIAIFLp6B+i14KhxAfnq76ajt0EhYc5YPeX6W1xJkAdItcVT+JhKl1QZh44/qw=="], + "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.53.5", "", { "os": "linux", "cpu": "arm" }, "sha512-dV3T9MyAf0w8zPVLVBptVlzaXxka6xg1f16VAQmjg+4KMSTWDvhimI/Y6mp8oHwNrmnmVl9XxJ/w/mO4uIQONA=="], - "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.53.3", "", { "os": "linux", "cpu": "arm" }, "sha512-vTNlKq+N6CK/8UktsrFuc+/7NlEYVxgaEgRXVUVK258Z5ymho29skzW1sutgYjqNnquGwVUObAaxae8rZ6YMhg=="], + "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.53.5", "", { "os": "linux", "cpu": "arm" }, "sha512-wIGYC1x/hyjP+KAu9+ewDI+fi5XSNiUi9Bvg6KGAh2TsNMA3tSEs+Sh6jJ/r4BV/bx/CyWu2ue9kDnIdRyafcQ=="], - "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.53.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-RGrFLWgMhSxRs/EWJMIFM1O5Mzuz3Xy3/mnxJp/5cVhZ2XoCAxJnmNsEyeMJtpK+wu0FJFWz+QF4mjCA7AUQ3w=="], + "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.53.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-Y+qVA0D9d0y2FRNiG9oM3Hut/DgODZbU9I8pLLPwAsU0tUKZ49cyV1tzmB/qRbSzGvY8lpgGkJuMyuhH7Ma+Vg=="], - "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.53.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-kASyvfBEWYPEwe0Qv4nfu6pNkITLTb32p4yTgzFCocHnJLAHs+9LjUu9ONIhvfT/5lv4YS5muBHyuV84epBo/A=="], + "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.53.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-juaC4bEgJsyFVfqhtGLz8mbopaWD+WeSOYr5E16y+1of6KQjc0BpwZLuxkClqY1i8sco+MdyoXPNiCkQou09+g=="], - "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.53.3", "", { "os": "linux", "cpu": "none" }, "sha512-JiuKcp2teLJwQ7vkJ95EwESWkNRFJD7TQgYmCnrPtlu50b4XvT5MOmurWNrCj3IFdyjBQ5p9vnrX4JM6I8OE7g=="], + "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.53.5", "", { "os": "linux", "cpu": "none" }, "sha512-rIEC0hZ17A42iXtHX+EPJVL/CakHo+tT7W0pbzdAGuWOt2jxDFh7A/lRhsNHBcqL4T36+UiAgwO8pbmn3dE8wA=="], - "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.53.3", "", { "os": "linux", "cpu": "ppc64" }, "sha512-EoGSa8nd6d3T7zLuqdojxC20oBfNT8nexBbB/rkxgKj5T5vhpAQKKnD+h3UkoMuTyXkP5jTjK/ccNRmQrPNDuw=="], + "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.53.5", "", { "os": "linux", "cpu": "ppc64" }, "sha512-T7l409NhUE552RcAOcmJHj3xyZ2h7vMWzcwQI0hvn5tqHh3oSoclf9WgTl+0QqffWFG8MEVZZP1/OBglKZx52Q=="], - "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.53.3", "", { "os": "linux", "cpu": "none" }, "sha512-4s+Wped2IHXHPnAEbIB0YWBv7SDohqxobiiPA1FIWZpX+w9o2i4LezzH/NkFUl8LRci/8udci6cLq+jJQlh+0g=="], + "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.53.5", "", { "os": "linux", "cpu": "none" }, "sha512-7OK5/GhxbnrMcxIFoYfhV/TkknarkYC1hqUw1wU2xUN3TVRLNT5FmBv4KkheSG2xZ6IEbRAhTooTV2+R5Tk0lQ=="], - "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.53.3", "", { "os": "linux", "cpu": "none" }, "sha512-68k2g7+0vs2u9CxDt5ktXTngsxOQkSEV/xBbwlqYcUrAVh6P9EgMZvFsnHy4SEiUl46Xf0IObWVbMvPrr2gw8A=="], + "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.53.5", "", { "os": "linux", "cpu": "none" }, "sha512-GwuDBE/PsXaTa76lO5eLJTyr2k8QkPipAyOrs4V/KJufHCZBJ495VCGJol35grx9xryk4V+2zd3Ri+3v7NPh+w=="], - "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.53.3", "", { "os": "linux", "cpu": "s390x" }, "sha512-VYsFMpULAz87ZW6BVYw3I6sWesGpsP9OPcyKe8ofdg9LHxSbRMd7zrVrr5xi/3kMZtpWL/wC+UIJWJYVX5uTKg=="], + "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.53.5", "", { "os": "linux", "cpu": "s390x" }, "sha512-IAE1Ziyr1qNfnmiQLHBURAD+eh/zH1pIeJjeShleII7Vj8kyEm2PF77o+lf3WTHDpNJcu4IXJxNO0Zluro8bOw=="], - "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.53.3", "", { "os": "linux", "cpu": "x64" }, "sha512-3EhFi1FU6YL8HTUJZ51imGJWEX//ajQPfqWLI3BQq4TlvHy4X0MOr5q3D2Zof/ka0d5FNdPwZXm3Yyib/UEd+w=="], + "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.53.5", "", { "os": "linux", "cpu": "x64" }, "sha512-Pg6E+oP7GvZ4XwgRJBuSXZjcqpIW3yCBhK4BcsANvb47qMvAbCjR6E+1a/U2WXz1JJxp9/4Dno3/iSJLcm5auw=="], - "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.53.3", "", { "os": "linux", "cpu": "x64" }, "sha512-eoROhjcc6HbZCJr+tvVT8X4fW3/5g/WkGvvmwz/88sDtSJzO7r/blvoBDgISDiCjDRZmHpwud7h+6Q9JxFwq1Q=="], + "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.53.5", "", { "os": "linux", "cpu": "x64" }, "sha512-txGtluxDKTxaMDzUduGP0wdfng24y1rygUMnmlUJ88fzCCULCLn7oE5kb2+tRB+MWq1QDZT6ObT5RrR8HFRKqg=="], - "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.53.3", "", { "os": "none", "cpu": "arm64" }, "sha512-OueLAWgrNSPGAdUdIjSWXw+u/02BRTcnfw9PN41D2vq/JSEPnJnVuBgw18VkN8wcd4fjUs+jFHVM4t9+kBSNLw=="], + "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.53.5", "", { "os": "none", "cpu": "arm64" }, "sha512-3DFiLPnTxiOQV993fMc+KO8zXHTcIjgaInrqlG8zDp1TlhYl6WgrOHuJkJQ6M8zHEcntSJsUp1XFZSY8C1DYbg=="], - "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.53.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-GOFuKpsxR/whszbF/bzydebLiXIHSgsEUp6M0JI8dWvi+fFa1TD6YQa4aSZHtpmh2/uAlj/Dy+nmby3TJ3pkTw=="], + "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.53.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-nggc/wPpNTgjGg75hu+Q/3i32R00Lq1B6N1DO7MCU340MRKL3WZJMjA9U4K4gzy3dkZPXm9E1Nc81FItBVGRlA=="], - "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.53.3", "", { "os": "win32", "cpu": "ia32" }, "sha512-iah+THLcBJdpfZ1TstDFbKNznlzoxa8fmnFYK4V67HvmuNYkVdAywJSoteUszvBQ9/HqN2+9AZghbajMsFT+oA=="], + "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.53.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-U/54pTbdQpPLBdEzCT6NBCFAfSZMvmjr0twhnD9f4EIvlm9wy3jjQ38yQj1AGznrNO65EWQMgm/QUjuIVrYF9w=="], - "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.53.3", "", { "os": "win32", "cpu": "x64" }, "sha512-J9QDiOIZlZLdcot5NXEepDkstocktoVjkaKUtqzgzpt2yWjGlbYiKyp05rWwk4nypbYUNoFAztEgixoLaSETkg=="], + "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.53.5", "", { "os": "win32", "cpu": "x64" }, "sha512-2NqKgZSuLH9SXBBV2dWNRCZmocgSOx8OJSdpRaEcRlIfX8YrKxUT6z0F1NpvDVhOsl190UFTRh2F2WDWWCYp3A=="], - "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.53.3", "", { "os": "win32", "cpu": "x64" }, "sha512-UhTd8u31dXadv0MopwGgNOBpUVROFKWVQgAg5N1ESyCz8AuBcMqm4AuTjrwgQKGDfoFuz02EuMRHQIw/frmYKQ=="], + "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.53.5", "", { "os": "win32", "cpu": "x64" }, "sha512-JRpZUhCfhZ4keB5v0fe02gQJy05GqboPOaxvjugW04RLSYYoB/9t2lx2u/tMs/Na/1NXfY8QYjgRljRpN+MjTQ=="], "@sinclair/typebox": ["@sinclair/typebox@0.34.41", "", {}, "sha512-6gS8pZzSXdyRHTIqoqSVknxolr1kzfy4/CeDnrzsVz8TTIWUbOBr6gnzOmTYJ3eXQNh4IYHIGi5aIL7sOZ2G/g=="], - "@storybook/addon-themes": ["@storybook/addon-themes@10.1.4", "", { "dependencies": { "ts-dedent": "^2.0.0" }, "peerDependencies": { "storybook": "^10.1.4" } }, "sha512-KQR+GAQ9X+eZOWhALuGvAFeLexlGdZcf7c2nBiLLPbTuPzcIl/ifTN4cZ0LqmGeXI/bfLCjTeqbk+B0E/IHZ5g=="], + "@storybook/addon-themes": ["@storybook/addon-themes@10.1.9", "", { "dependencies": { "ts-dedent": "^2.0.0" }, "peerDependencies": { "storybook": "^10.1.9" } }, "sha512-3TSUvGrMW9T69lOaRaFtFFuZDtkqGHYxfQsRL+n2vRTG6J3cgSLawdxQ414nVBdkwbEU6m3KOkVV5Pf9C+v4RQ=="], - "@storybook/builder-vite": ["@storybook/builder-vite@10.1.4", "", { "dependencies": { "@storybook/csf-plugin": "10.1.4", "@vitest/mocker": "3.2.4", "ts-dedent": "^2.0.0" }, "peerDependencies": { "storybook": "^10.1.4", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-3mUQoCzMuhqAIjj8fdbGlwh+GgHaFpCvU+sxL8kIxnZqflW09SuwM5kS47Y5QDzYbHAPYCPqcBFyJ4EfRuf0rw=="], + "@storybook/builder-vite": ["@storybook/builder-vite@10.1.9", "", { "dependencies": { "@storybook/csf-plugin": "10.1.9", "@vitest/mocker": "3.2.4", "ts-dedent": "^2.0.0" }, "peerDependencies": { "storybook": "^10.1.9", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-rUILpjGV7gKfXrUeZzpNAer9PspB3LJI1d+gJHISx2Gs24bdneA3y/gu0fWw46ccOSIcwb91xoK5QxliJcWsWg=="], "@storybook/channels": ["@storybook/channels@7.6.17", "", { "dependencies": { "@storybook/client-logger": "7.6.17", "@storybook/core-events": "7.6.17", "@storybook/global": "^5.0.0", "qs": "^6.10.0", "telejson": "^7.2.0", "tiny-invariant": "^1.3.1" } }, "sha512-GFG40pzaSxk1hUr/J/TMqW5AFDDPUSu+HkeE/oqSWJbOodBOLJzHN6CReJS6y1DjYSZLNFt1jftPWZZInG/XUA=="], @@ -618,7 +622,7 @@ "@storybook/csf": ["@storybook/csf@0.1.13", "", { "dependencies": { "type-fest": "^2.19.0" } }, "sha512-7xOOwCLGB3ebM87eemep89MYRFTko+D8qE7EdAAq74lgdqRR5cOUtYWJLjO2dLtP94nqoOdHJo6MdLLKzg412Q=="], - "@storybook/csf-plugin": ["@storybook/csf-plugin@10.1.4", "", { "dependencies": { "unplugin": "^2.3.5" }, "peerDependencies": { "esbuild": "*", "rollup": "*", "storybook": "^10.1.4", "vite": "*", "webpack": "*" }, "optionalPeers": ["esbuild", "rollup", "vite", "webpack"] }, "sha512-nudIBYx8fBz+1j2Xn1pdfGcgMJ78N/1NFB4MYAxI3YEzxGnQwUjihOO1x3siAXPbjFGmnVHoBx7+6IpO3F70GA=="], + "@storybook/csf-plugin": ["@storybook/csf-plugin@10.1.9", "", { "dependencies": { "unplugin": "^2.3.5" }, "peerDependencies": { "esbuild": "*", "rollup": "*", "storybook": "^10.1.9", "vite": "*", "webpack": "*" }, "optionalPeers": ["esbuild", "rollup", "vite", "webpack"] }, "sha512-17LXUqpbVvsMt7KJwgr0bPUX+uEGArc6EOi+DC5X/CQ+i0nXxxLMpDHdTyrsdKxCZIT087OpSNbTEWP5ACEAlA=="], "@storybook/global": ["@storybook/global@5.0.0", "", {}, "sha512-FcOqPAXACP0I3oJ/ws6/rrPT9WGhu915Cg8D02a9YxLo0DE9zI+a9A5gRGvmQ09fiWPukqI8ZAEoQEdWUKMQdQ=="], @@ -626,43 +630,43 @@ "@storybook/preview-api": ["@storybook/preview-api@7.6.17", "", { "dependencies": { "@storybook/channels": "7.6.17", "@storybook/client-logger": "7.6.17", "@storybook/core-events": "7.6.17", "@storybook/csf": "^0.1.2", "@storybook/global": "^5.0.0", "@storybook/types": "7.6.17", "@types/qs": "^6.9.5", "dequal": "^2.0.2", "lodash": "^4.17.21", "memoizerific": "^1.11.3", "qs": "^6.10.0", "synchronous-promise": "^2.0.15", "ts-dedent": "^2.0.0", "util-deprecate": "^1.0.2" } }, "sha512-wLfDdI9RWo1f2zzFe54yRhg+2YWyxLZvqdZnSQ45mTs4/7xXV5Wfbv3QNTtcdw8tT3U5KRTrN1mTfTCiRJc0Kw=="], - "@storybook/react": ["@storybook/react@10.1.4", "", { "dependencies": { "@storybook/global": "^5.0.0", "@storybook/react-dom-shim": "10.1.4", "react-docgen": "^8.0.2" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "storybook": "^10.1.4", "typescript": ">= 4.9.x" }, "optionalPeers": ["typescript"] }, "sha512-ZBMPdQ99QBv/UtlIZBerDGNsQB30ffxk6twe45FIPutSlKXD6W9r0z7rGa5UWnqmmxa9HjARRhclOFsNGkhs9g=="], + "@storybook/react": ["@storybook/react@10.1.9", "", { "dependencies": { "@storybook/global": "^5.0.0", "@storybook/react-dom-shim": "10.1.9", "react-docgen": "^8.0.2" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "storybook": "^10.1.9", "typescript": ">= 4.9.x" }, "optionalPeers": ["typescript"] }, "sha512-NqiFp5rJmxzs0teNAGqgGH0nD8q1aYR8AxQl9OYSHULYoLJR1/RqcyBPTTBjxAOpWF/pZgLBrRW+FjZbjKLLMQ=="], - "@storybook/react-dom-shim": ["@storybook/react-dom-shim@10.1.4", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "storybook": "^10.1.4" } }, "sha512-PARu2HA5nYU1AkioNJNc430pz0oyaHFSSAdN3NEaWwkoGrCOo9ZpAXP9V7wlJANCi1pndbC84gSuHVnBXJBG6g=="], + "@storybook/react-dom-shim": ["@storybook/react-dom-shim@10.1.9", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "storybook": "^10.1.9" } }, "sha512-gJsR6fI1gG4DSin6sQx8RmGDQF8Lije0cZbxHyVedNleBsveGXIPFUKFVi+pRNdwBPni1Z2g/gYyHzkOEqPD2w=="], - "@storybook/react-vite": ["@storybook/react-vite@10.1.4", "", { "dependencies": { "@joshwooding/vite-plugin-react-docgen-typescript": "0.6.1", "@rollup/pluginutils": "^5.0.2", "@storybook/builder-vite": "10.1.4", "@storybook/react": "10.1.4", "empathic": "^2.0.0", "magic-string": "^0.30.0", "react-docgen": "^8.0.0", "resolve": "^1.22.8", "tsconfig-paths": "^4.2.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "storybook": "^10.1.4", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-PneYbxBGArczDtDAvQu6Ug5oeDYM5SQiEDSF0i+TNN0ZKO2ROsmbGSI9/7YTFontXR2CqweIO8GyOGQOcz5K9A=="], + "@storybook/react-vite": ["@storybook/react-vite@10.1.9", "", { "dependencies": { "@joshwooding/vite-plugin-react-docgen-typescript": "^0.6.3", "@rollup/pluginutils": "^5.0.2", "@storybook/builder-vite": "10.1.9", "@storybook/react": "10.1.9", "empathic": "^2.0.0", "magic-string": "^0.30.0", "react-docgen": "^8.0.0", "resolve": "^1.22.8", "tsconfig-paths": "^4.2.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "storybook": "^10.1.9", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-2f2mLGTDKYzIFi5Xnu5TEBpnDXazSAKMliVsUKrCr+gunfk8uPApj0njATvZoRB3xTZ44Aacf7l9EZQaTYxB/Q=="], "@storybook/types": ["@storybook/types@7.6.17", "", { "dependencies": { "@storybook/channels": "7.6.17", "@types/babel__core": "^7.0.0", "@types/express": "^4.7.0", "file-system-cache": "2.3.0" } }, "sha512-GRY0xEJQ0PrL7DY2qCNUdIfUOE0Gsue6N+GBJw9ku1IUDFLJRDOF+4Dx2BvYcVCPI5XPqdWKlEyZdMdKjiQN7Q=="], - "@tailwindcss/node": ["@tailwindcss/node@4.1.17", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "enhanced-resolve": "^5.18.3", "jiti": "^2.6.1", "lightningcss": "1.30.2", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.1.17" } }, "sha512-csIkHIgLb3JisEFQ0vxr2Y57GUNYh447C8xzwj89U/8fdW8LhProdxvnVH6U8M2Y73QKiTIH+LWbK3V2BBZsAg=="], + "@tailwindcss/node": ["@tailwindcss/node@4.1.18", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "enhanced-resolve": "^5.18.3", "jiti": "^2.6.1", "lightningcss": "1.30.2", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.1.18" } }, "sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ=="], - "@tailwindcss/oxide": ["@tailwindcss/oxide@4.1.17", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.1.17", "@tailwindcss/oxide-darwin-arm64": "4.1.17", "@tailwindcss/oxide-darwin-x64": "4.1.17", "@tailwindcss/oxide-freebsd-x64": "4.1.17", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.17", "@tailwindcss/oxide-linux-arm64-gnu": "4.1.17", "@tailwindcss/oxide-linux-arm64-musl": "4.1.17", "@tailwindcss/oxide-linux-x64-gnu": "4.1.17", "@tailwindcss/oxide-linux-x64-musl": "4.1.17", "@tailwindcss/oxide-wasm32-wasi": "4.1.17", "@tailwindcss/oxide-win32-arm64-msvc": "4.1.17", "@tailwindcss/oxide-win32-x64-msvc": "4.1.17" } }, "sha512-F0F7d01fmkQhsTjXezGBLdrl1KresJTcI3DB8EkScCldyKp3Msz4hub4uyYaVnk88BAS1g5DQjjF6F5qczheLA=="], + "@tailwindcss/oxide": ["@tailwindcss/oxide@4.1.18", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.1.18", "@tailwindcss/oxide-darwin-arm64": "4.1.18", "@tailwindcss/oxide-darwin-x64": "4.1.18", "@tailwindcss/oxide-freebsd-x64": "4.1.18", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.18", "@tailwindcss/oxide-linux-arm64-gnu": "4.1.18", "@tailwindcss/oxide-linux-arm64-musl": "4.1.18", "@tailwindcss/oxide-linux-x64-gnu": "4.1.18", "@tailwindcss/oxide-linux-x64-musl": "4.1.18", "@tailwindcss/oxide-wasm32-wasi": "4.1.18", "@tailwindcss/oxide-win32-arm64-msvc": "4.1.18", "@tailwindcss/oxide-win32-x64-msvc": "4.1.18" } }, "sha512-EgCR5tTS5bUSKQgzeMClT6iCY3ToqE1y+ZB0AKldj809QXk1Y+3jB0upOYZrn9aGIzPtUsP7sX4QQ4XtjBB95A=="], - "@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.1.17", "", { "os": "android", "cpu": "arm64" }, "sha512-BMqpkJHgOZ5z78qqiGE6ZIRExyaHyuxjgrJ6eBO5+hfrfGkuya0lYfw8fRHG77gdTjWkNWEEm+qeG2cDMxArLQ=="], + "@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.1.18", "", { "os": "android", "cpu": "arm64" }, "sha512-dJHz7+Ugr9U/diKJA0W6N/6/cjI+ZTAoxPf9Iz9BFRF2GzEX8IvXxFIi/dZBloVJX/MZGvRuFA9rqwdiIEZQ0Q=="], - "@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.1.17", "", { "os": "darwin", "cpu": "arm64" }, "sha512-EquyumkQweUBNk1zGEU/wfZo2qkp/nQKRZM8bUYO0J+Lums5+wl2CcG1f9BgAjn/u9pJzdYddHWBiFXJTcxmOg=="], + "@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.1.18", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Gc2q4Qhs660bhjyBSKgq6BYvwDz4G+BuyJ5H1xfhmDR3D8HnHCmT/BSkvSL0vQLy/nkMLY20PQ2OoYMO15Jd0A=="], - "@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.1.17", "", { "os": "darwin", "cpu": "x64" }, "sha512-gdhEPLzke2Pog8s12oADwYu0IAw04Y2tlmgVzIN0+046ytcgx8uZmCzEg4VcQh+AHKiS7xaL8kGo/QTiNEGRog=="], + "@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.1.18", "", { "os": "darwin", "cpu": "x64" }, "sha512-FL5oxr2xQsFrc3X9o1fjHKBYBMD1QZNyc1Xzw/h5Qu4XnEBi3dZn96HcHm41c/euGV+GRiXFfh2hUCyKi/e+yw=="], - "@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.1.17", "", { "os": "freebsd", "cpu": "x64" }, "sha512-hxGS81KskMxML9DXsaXT1H0DyA+ZBIbyG/sSAjWNe2EDl7TkPOBI42GBV3u38itzGUOmFfCzk1iAjDXds8Oh0g=="], + "@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.1.18", "", { "os": "freebsd", "cpu": "x64" }, "sha512-Fj+RHgu5bDodmV1dM9yAxlfJwkkWvLiRjbhuO2LEtwtlYlBgiAT4x/j5wQr1tC3SANAgD+0YcmWVrj8R9trVMA=="], - "@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.1.17", "", { "os": "linux", "cpu": "arm" }, "sha512-k7jWk5E3ldAdw0cNglhjSgv501u7yrMf8oeZ0cElhxU6Y2o7f8yqelOp3fhf7evjIS6ujTI3U8pKUXV2I4iXHQ=="], + "@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.1.18", "", { "os": "linux", "cpu": "arm" }, "sha512-Fp+Wzk/Ws4dZn+LV2Nqx3IilnhH51YZoRaYHQsVq3RQvEl+71VGKFpkfHrLM/Li+kt5c0DJe/bHXK1eHgDmdiA=="], - "@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.1.17", "", { "os": "linux", "cpu": "arm64" }, "sha512-HVDOm/mxK6+TbARwdW17WrgDYEGzmoYayrCgmLEw7FxTPLcp/glBisuyWkFz/jb7ZfiAXAXUACfyItn+nTgsdQ=="], + "@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.1.18", "", { "os": "linux", "cpu": "arm64" }, "sha512-S0n3jboLysNbh55Vrt7pk9wgpyTTPD0fdQeh7wQfMqLPM/Hrxi+dVsLsPrycQjGKEQk85Kgbx+6+QnYNiHalnw=="], - "@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.1.17", "", { "os": "linux", "cpu": "arm64" }, "sha512-HvZLfGr42i5anKtIeQzxdkw/wPqIbpeZqe7vd3V9vI3RQxe3xU1fLjss0TjyhxWcBaipk7NYwSrwTwK1hJARMg=="], + "@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.1.18", "", { "os": "linux", "cpu": "arm64" }, "sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg=="], - "@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.1.17", "", { "os": "linux", "cpu": "x64" }, "sha512-M3XZuORCGB7VPOEDH+nzpJ21XPvK5PyjlkSFkFziNHGLc5d6g3di2McAAblmaSUNl8IOmzYwLx9NsE7bplNkwQ=="], + "@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.1.18", "", { "os": "linux", "cpu": "x64" }, "sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g=="], - "@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.1.17", "", { "os": "linux", "cpu": "x64" }, "sha512-k7f+pf9eXLEey4pBlw+8dgfJHY4PZ5qOUFDyNf7SI6lHjQ9Zt7+NcscjpwdCEbYi6FI5c2KDTDWyf2iHcCSyyQ=="], + "@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.1.18", "", { "os": "linux", "cpu": "x64" }, "sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ=="], - "@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.1.17", "", { "dependencies": { "@emnapi/core": "^1.6.0", "@emnapi/runtime": "^1.6.0", "@emnapi/wasi-threads": "^1.1.0", "@napi-rs/wasm-runtime": "^1.0.7", "@tybys/wasm-util": "^0.10.1", "tslib": "^2.4.0" }, "cpu": "none" }, "sha512-cEytGqSSoy7zK4JRWiTCx43FsKP/zGr0CsuMawhH67ONlH+T79VteQeJQRO/X7L0juEUA8ZyuYikcRBf0vsxhg=="], + "@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.1.18", "", { "dependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1", "@emnapi/wasi-threads": "^1.1.0", "@napi-rs/wasm-runtime": "^1.1.0", "@tybys/wasm-util": "^0.10.1", "tslib": "^2.4.0" }, "cpu": "none" }, "sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA=="], - "@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.1.17", "", { "os": "win32", "cpu": "arm64" }, "sha512-JU5AHr7gKbZlOGvMdb4722/0aYbU+tN6lv1kONx0JK2cGsh7g148zVWLM0IKR3NeKLv+L90chBVYcJ8uJWbC9A=="], + "@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.1.18", "", { "os": "win32", "cpu": "arm64" }, "sha512-HjSA7mr9HmC8fu6bdsZvZ+dhjyGCLdotjVOgLA2vEqxEBZaQo9YTX4kwgEvPCpRh8o4uWc4J/wEoFzhEmjvPbA=="], - "@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.1.17", "", { "os": "win32", "cpu": "x64" }, "sha512-SKWM4waLuqx0IH+FMDUw6R66Hu4OuTALFgnleKbqhgGU30DY20NORZMZUKgLRjQXNN2TLzKvh48QXTig4h4bGw=="], + "@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.1.18", "", { "os": "win32", "cpu": "x64" }, "sha512-bJWbyYpUlqamC8dpR7pfjA0I7vdF6t5VpUGMWRkXVE3AXgIZjYUYAK7II1GNaxR8J1SSrSrppRar8G++JekE3Q=="], - "@tailwindcss/vite": ["@tailwindcss/vite@4.1.17", "", { "dependencies": { "@tailwindcss/node": "4.1.17", "@tailwindcss/oxide": "4.1.17", "tailwindcss": "4.1.17" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7" } }, "sha512-4+9w8ZHOiGnpcGI6z1TVVfWaX/koK7fKeSYF3qlYg2xpBtbteP2ddBxiarL+HVgfSJGeK5RIxRQmKm4rTJJAwA=="], + "@tailwindcss/vite": ["@tailwindcss/vite@4.1.18", "", { "dependencies": { "@tailwindcss/node": "4.1.18", "@tailwindcss/oxide": "4.1.18", "tailwindcss": "4.1.18" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7" } }, "sha512-jVA+/UpKL1vRLg6Hkao5jldawNmRo7mQYrZtNHMIVpLfLhDml5nMRUo/8MwoX2vNXvnaXNNMedrMfMugAVX1nA=="], "@testing-library/dom": ["@testing-library/dom@10.4.1", "", { "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", "@types/aria-query": "^5.0.1", "aria-query": "5.3.0", "dom-accessibility-api": "^0.5.9", "lz-string": "^1.5.0", "picocolors": "1.1.1", "pretty-format": "^27.0.2" } }, "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg=="], @@ -688,7 +692,7 @@ "@types/body-parser": ["@types/body-parser@1.19.6", "", { "dependencies": { "@types/connect": "*", "@types/node": "*" } }, "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g=="], - "@types/bun": ["@types/bun@1.3.3", "", { "dependencies": { "bun-types": "1.3.3" } }, "sha512-ogrKbJ2X5N0kWLLFKeytG0eHDleBYtngtlbu9cyBKFtNL3cnpDZkNdQj8flVf6WTZUX5ulI9AY1oa7ljhSrp+g=="], + "@types/bun": ["@types/bun@1.3.4", "", { "dependencies": { "bun-types": "1.3.4" } }, "sha512-EEPTKXHP+zKGPkhRLv+HI0UEX8/o+65hqARxLy8Ov5rIxMBPNTjeZww00CIihrIQGEQBYg+0roO5qOnS/7boGA=="], "@types/chai": ["@types/chai@5.2.3", "", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="], @@ -718,7 +722,7 @@ "@types/mime": ["@types/mime@1.3.5", "", {}, "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w=="], - "@types/node": ["@types/node@22.19.1", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-LCCV0HdSZZZb34qifBsyWlUmok6W7ouER+oQIGBScS8EsZsQbrtFTUrDX4hOl+CS6p7cnNC4td+qrSVGSCTUfQ=="], + "@types/node": ["@types/node@22.19.3", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-1N9SBnWYOJTrNZCdh/yJE+t910Y128BoyY+zBLWhL3r0TYzlTmFdXrPwHL9DyFZmlEXNQQolTZh3KHV31QDhyA=="], "@types/qs": ["@types/qs@6.14.0", "", {}, "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ=="], @@ -738,27 +742,27 @@ "@types/ssh2": ["@types/ssh2@1.15.5", "", { "dependencies": { "@types/node": "^18.11.18" } }, "sha512-N1ASjp/nXH3ovBHddRJpli4ozpk6UdDYIX4RJWFa9L1YKnzdhTlVmiGHm4DZnj/jLbqZpes4aeR30EFGQtvhQQ=="], - "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.48.1", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.48.1", "@typescript-eslint/type-utils": "8.48.1", "@typescript-eslint/utils": "8.48.1", "@typescript-eslint/visitor-keys": "8.48.1", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.48.1", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-X63hI1bxl5ohelzr0LY5coufyl0LJNthld+abwxpCoo6Gq+hSqhKwci7MUWkXo67mzgUK6YFByhmaHmUcuBJmA=="], + "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.50.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.50.0", "@typescript-eslint/type-utils": "8.50.0", "@typescript-eslint/utils": "8.50.0", "@typescript-eslint/visitor-keys": "8.50.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.50.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-O7QnmOXYKVtPrfYzMolrCTfkezCJS9+ljLdKW/+DCvRsc3UAz+sbH6Xcsv7p30+0OwUbeWfUDAQE0vpabZ3QLg=="], - "@typescript-eslint/parser": ["@typescript-eslint/parser@8.48.1", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.48.1", "@typescript-eslint/types": "8.48.1", "@typescript-eslint/typescript-estree": "8.48.1", "@typescript-eslint/visitor-keys": "8.48.1", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-PC0PDZfJg8sP7cmKe6L3QIL8GZwU5aRvUFedqSIpw3B+QjRSUZeeITC2M5XKeMXEzL6wccN196iy3JLwKNvDVA=="], + "@typescript-eslint/parser": ["@typescript-eslint/parser@8.50.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.50.0", "@typescript-eslint/types": "8.50.0", "@typescript-eslint/typescript-estree": "8.50.0", "@typescript-eslint/visitor-keys": "8.50.0", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-6/cmF2piao+f6wSxUsJLZjck7OQsYyRtcOZS02k7XINSNlz93v6emM8WutDQSXnroG2xwYlEVHJI+cPA7CPM3Q=="], - "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.48.1", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.48.1", "@typescript-eslint/types": "^8.48.1", "debug": "^4.3.4" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-HQWSicah4s9z2/HifRPQ6b6R7G+SBx64JlFQpgSSHWPKdvCZX57XCbszg/bapbRsOEv42q5tayTYcEFpACcX1w=="], + "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.50.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.50.0", "@typescript-eslint/types": "^8.50.0", "debug": "^4.3.4" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-Cg/nQcL1BcoTijEWyx4mkVC56r8dj44bFDvBdygifuS20f3OZCHmFbjF34DPSi07kwlFvqfv/xOLnJ5DquxSGQ=="], - "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.48.1", "", { "dependencies": { "@typescript-eslint/types": "8.48.1", "@typescript-eslint/visitor-keys": "8.48.1" } }, "sha512-rj4vWQsytQbLxC5Bf4XwZ0/CKd362DkWMUkviT7DCS057SK64D5lH74sSGzhI6PDD2HCEq02xAP9cX68dYyg1w=="], + "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.50.0", "", { "dependencies": { "@typescript-eslint/types": "8.50.0", "@typescript-eslint/visitor-keys": "8.50.0" } }, "sha512-xCwfuCZjhIqy7+HKxBLrDVT5q/iq7XBVBXLn57RTIIpelLtEIZHXAF/Upa3+gaCpeV1NNS5Z9A+ID6jn50VD4A=="], - "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.48.1", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-k0Jhs4CpEffIBm6wPaCXBAD7jxBtrHjrSgtfCjUvPp9AZ78lXKdTR8fxyZO5y4vWNlOvYXRtngSZNSn+H53Jkw=="], + "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.50.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-vxd3G/ybKTSlm31MOA96gqvrRGv9RJ7LGtZCn2Vrc5htA0zCDvcMqUkifcjrWNNKXHUU3WCkYOzzVSFBd0wa2w=="], - "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.48.1", "", { "dependencies": { "@typescript-eslint/types": "8.48.1", "@typescript-eslint/typescript-estree": "8.48.1", "@typescript-eslint/utils": "8.48.1", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-1jEop81a3LrJQLTf/1VfPQdhIY4PlGDBc/i67EVWObrtvcziysbLN3oReexHOM6N3jyXgCrkBsZpqwH0hiDOQg=="], + "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.50.0", "", { "dependencies": { "@typescript-eslint/types": "8.50.0", "@typescript-eslint/typescript-estree": "8.50.0", "@typescript-eslint/utils": "8.50.0", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-7OciHT2lKCewR0mFoBrvZJ4AXTMe/sYOe87289WAViOocEmDjjv8MvIOT2XESuKj9jp8u3SZYUSh89QA4S1kQw=="], - "@typescript-eslint/types": ["@typescript-eslint/types@8.48.1", "", {}, "sha512-+fZ3LZNeiELGmimrujsDCT4CRIbq5oXdHe7chLiW8qzqyPMnn1puNstCrMNVAqwcl2FdIxkuJ4tOs/RFDBVc/Q=="], + "@typescript-eslint/types": ["@typescript-eslint/types@8.50.0", "", {}, "sha512-iX1mgmGrXdANhhITbpp2QQM2fGehBse9LbTf0sidWK6yg/NE+uhV5dfU1g6EYPlcReYmkE9QLPq/2irKAmtS9w=="], - "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.48.1", "", { "dependencies": { "@typescript-eslint/project-service": "8.48.1", "@typescript-eslint/tsconfig-utils": "8.48.1", "@typescript-eslint/types": "8.48.1", "@typescript-eslint/visitor-keys": "8.48.1", "debug": "^4.3.4", "minimatch": "^9.0.4", "semver": "^7.6.0", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-/9wQ4PqaefTK6POVTjJaYS0bynCgzh6ClJHGSBj06XEHjkfylzB+A3qvyaXnErEZSaxhIo4YdyBgq6j4RysxDg=="], + "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.50.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.50.0", "@typescript-eslint/tsconfig-utils": "8.50.0", "@typescript-eslint/types": "8.50.0", "@typescript-eslint/visitor-keys": "8.50.0", "debug": "^4.3.4", "minimatch": "^9.0.4", "semver": "^7.6.0", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-W7SVAGBR/IX7zm1t70Yujpbk+zdPq/u4soeFSknWFdXIFuWsBGBOUu/Tn/I6KHSKvSh91OiMuaSnYp3mtPt5IQ=="], - "@typescript-eslint/utils": ["@typescript-eslint/utils@8.48.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.48.1", "@typescript-eslint/types": "8.48.1", "@typescript-eslint/typescript-estree": "8.48.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-fAnhLrDjiVfey5wwFRwrweyRlCmdz5ZxXz2G/4cLn0YDLjTapmN4gcCsTBR1N2rWnZSDeWpYtgLDsJt+FpmcwA=="], + "@typescript-eslint/utils": ["@typescript-eslint/utils@8.50.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.50.0", "@typescript-eslint/types": "8.50.0", "@typescript-eslint/typescript-estree": "8.50.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-87KgUXET09CRjGCi2Ejxy3PULXna63/bMYv72tCAlDJC3Yqwln0HiFJ3VJMst2+mEtNtZu5oFvX4qJGjKsnAgg=="], - "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.48.1", "", { "dependencies": { "@typescript-eslint/types": "8.48.1", "eslint-visitor-keys": "^4.2.1" } }, "sha512-BmxxndzEWhE4TIEEMBs8lP3MBWN3jFPs/p6gPm/wkv02o41hI6cq9AuSmGAaTTHPtA1FTi2jBre4A9rm5ZmX+Q=="], + "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.50.0", "", { "dependencies": { "@typescript-eslint/types": "8.50.0", "eslint-visitor-keys": "^4.2.1" } }, "sha512-Xzmnb58+Db78gT/CCj/PVCvK+zxbnsw6F+O1oheYszJbBSdEjVhQi3C/Xttzxgi/GLmpvOggRs1RFpiJ8+c34Q=="], - "@vitejs/plugin-react": ["@vitejs/plugin-react@5.1.1", "", { "dependencies": { "@babel/core": "^7.28.5", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-beta.47", "@types/babel__core": "^7.20.5", "react-refresh": "^0.18.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-WQfkSw0QbQ5aJ2CHYw23ZGkqnRwqKHD/KYsMeTkZzPT4Jcf0DcBxBtwMJxnu6E7oxw5+JC6ZAiePgh28uJ1HBA=="], + "@vitejs/plugin-react": ["@vitejs/plugin-react@5.1.2", "", { "dependencies": { "@babel/core": "^7.28.5", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-beta.53", "@types/babel__core": "^7.20.5", "react-refresh": "^0.18.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-EcA07pHJouywpzsoTUqNh5NwGayl2PPVEJKUSinGGSxFGYn+shYbqMGBg6FXDqgXum9Ou/ecb+411ssw8HImJQ=="], "@vitest/expect": ["@vitest/expect@3.2.4", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/spy": "3.2.4", "@vitest/utils": "3.2.4", "chai": "^5.2.0", "tinyrainbow": "^2.0.0" } }, "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig=="], @@ -804,7 +808,7 @@ "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], - "baseline-browser-mapping": ["baseline-browser-mapping@2.9.0", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-Mh++g+2LPfzZToywfE1BUzvZbfOY52Nil0rn9H1CPC5DJ7fX+Vir7nToBeoiSbB1zTNeGYbELEvJESujgGrzXw=="], + "baseline-browser-mapping": ["baseline-browser-mapping@2.9.8", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-Y1fOuNDowLfgKOypdc9SPABfoWXuZHBOyCS4cD52IeZBhr4Md6CLLs6atcxVrzRmQ06E7hSlm5bHHApPKR/byA=="], "basic-auth": ["basic-auth@2.0.1", "", { "dependencies": { "safe-buffer": "5.1.2" } }, "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg=="], @@ -826,7 +830,9 @@ "bun-plugin-dts": ["bun-plugin-dts@0.3.0", "", { "dependencies": { "common-path-prefix": "^3.0.0", "dts-bundle-generator": "^9.5.1", "get-tsconfig": "^4.8.1" } }, "sha512-QpiAOKfPcdOToxySOqRY8FwL+brTvyXEHWzrSCRKt4Pv7Z4pnUrhK9tFtM7Ndm7ED09B/0cGXnHJKqmekr/ERw=="], - "bun-types": ["bun-types@1.3.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-z3Xwlg7j2l9JY27x5Qn3Wlyos8YAp0kKRlrePAOjgjMGS5IG6E7Jnlx736vH9UVI4wUICwwhC9anYL++XeOgTQ=="], + "bun-types": ["bun-types@1.3.4", "", { "dependencies": { "@types/node": "*" } }, "sha512-5ua817+BZPZOlNaRgGBpZJOSAQ9RQ17pkwPD0yR7CfJg+r8DgIILByFifDTa+IPDDxzf5VNhtNlcKqFzDgJvlQ=="], + + "bundle-name": ["bundle-name@4.1.0", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q=="], "bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="], @@ -838,7 +844,7 @@ "callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="], - "caniuse-lite": ["caniuse-lite@1.0.30001759", "", {}, "sha512-Pzfx9fOKoKvevQf8oCXoyNRQ5QyxJj+3O0Rqx2V5oxT61KGx8+n6hV/IUyJeifUci2clnmmKVpvtiqRzgiWjSw=="], + "caniuse-lite": ["caniuse-lite@1.0.30001760", "", {}, "sha512-7AAMPcueWELt1p3mi13HR/LHH0TJLT11cnwDJEs3xA4+CK/PLKeO9Kl1oru24htkyUKtkGCvAx4ohB0Ttry8Dw=="], "chai": ["chai@5.3.3", "", { "dependencies": { "assertion-error": "^2.0.1", "check-error": "^2.1.1", "deep-eql": "^5.0.1", "loupe": "^3.1.0", "pathval": "^2.0.0" } }, "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw=="], @@ -892,12 +898,18 @@ "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], - "dedent": ["dedent@1.7.0", "", { "peerDependencies": { "babel-plugin-macros": "^3.1.0" }, "optionalPeers": ["babel-plugin-macros"] }, "sha512-HGFtf8yhuhGhqO07SV79tRp+br4MnbdjeVxotpn1QBl30pcLLCQjX5b2295ll0fv8RKDKsmWYrl05usHM9CewQ=="], + "dedent": ["dedent@1.7.1", "", { "peerDependencies": { "babel-plugin-macros": "^3.1.0" }, "optionalPeers": ["babel-plugin-macros"] }, "sha512-9JmrhGZpOlEgOLdQgSm0zxFaYoQon408V1v49aqTWuXENVlnCuY9JBZcXZiCsZQWDjTm5Qf/nIvAy77mXDAjEg=="], "deep-eql": ["deep-eql@5.0.2", "", {}, "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q=="], "deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="], + "default-browser": ["default-browser@5.4.0", "", { "dependencies": { "bundle-name": "^4.1.0", "default-browser-id": "^5.0.0" } }, "sha512-XDuvSq38Hr1MdN47EDvYtx3U0MTqpCEn+F6ft8z2vYDzMrvQhVp0ui9oQdqW3MvK3vqUETglt1tVGgjLuJ5izg=="], + + "default-browser-id": ["default-browser-id@5.0.1", "", {}, "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q=="], + + "define-lazy-prop": ["define-lazy-prop@3.0.0", "", {}, "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg=="], + "depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="], "dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="], @@ -928,7 +940,7 @@ "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], - "electron-to-chromium": ["electron-to-chromium@1.5.263", "", {}, "sha512-DrqJ11Knd+lo+dv+lltvfMDLU27g14LMdH2b0O3Pio4uk0x+z7OR+JrmyacTPN2M8w3BrZ7/RTwG3R9B7irPlg=="], + "electron-to-chromium": ["electron-to-chromium@1.5.267", "", {}, "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw=="], "elysia": ["elysia@1.4.12", "", { "dependencies": { "cookie": "^1.0.2", "exact-mirror": "0.2.2", "fast-decode-uri-component": "^1.0.1" }, "peerDependencies": { "@sinclair/typebox": ">= 0.34.0 < 1", "file-type": ">= 20.0.0", "openapi-types": ">= 12.0.0", "typescript": ">= 5.0.0" }, "optionalPeers": ["typescript"] }, "sha512-wbd0BkrobsjWSloIfYeF3f7G7rR0UWMa6tuLUhf6ZvwjiCEX3FVfhDsM+KaqqRRxkZpPDw42q4yIZlBLyE32ww=="], @@ -942,7 +954,7 @@ "end-of-stream": ["end-of-stream@1.4.5", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg=="], - "enhanced-resolve": ["enhanced-resolve@5.18.3", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww=="], + "enhanced-resolve": ["enhanced-resolve@5.18.4", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q=="], "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], @@ -952,7 +964,7 @@ "es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="], - "esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="], + "esbuild": ["esbuild@0.27.2", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.2", "@esbuild/android-arm": "0.27.2", "@esbuild/android-arm64": "0.27.2", "@esbuild/android-x64": "0.27.2", "@esbuild/darwin-arm64": "0.27.2", "@esbuild/darwin-x64": "0.27.2", "@esbuild/freebsd-arm64": "0.27.2", "@esbuild/freebsd-x64": "0.27.2", "@esbuild/linux-arm": "0.27.2", "@esbuild/linux-arm64": "0.27.2", "@esbuild/linux-ia32": "0.27.2", "@esbuild/linux-loong64": "0.27.2", "@esbuild/linux-mips64el": "0.27.2", "@esbuild/linux-ppc64": "0.27.2", "@esbuild/linux-riscv64": "0.27.2", "@esbuild/linux-s390x": "0.27.2", "@esbuild/linux-x64": "0.27.2", "@esbuild/netbsd-arm64": "0.27.2", "@esbuild/netbsd-x64": "0.27.2", "@esbuild/openbsd-arm64": "0.27.2", "@esbuild/openbsd-x64": "0.27.2", "@esbuild/openharmony-arm64": "0.27.2", "@esbuild/sunos-x64": "0.27.2", "@esbuild/win32-arm64": "0.27.2", "@esbuild/win32-ia32": "0.27.2", "@esbuild/win32-x64": "0.27.2" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw=="], "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], @@ -960,13 +972,13 @@ "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], - "eslint": ["eslint@9.39.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.1", "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "9.39.1", "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g=="], + "eslint": ["eslint@9.39.2", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.1", "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "9.39.2", "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw=="], "eslint-plugin-react-hooks": ["eslint-plugin-react-hooks@5.2.0", "", { "peerDependencies": { "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" } }, "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg=="], - "eslint-plugin-react-refresh": ["eslint-plugin-react-refresh@0.4.24", "", { "peerDependencies": { "eslint": ">=8.40" } }, "sha512-nLHIW7TEq3aLrEYWpVaJ1dRgFR+wLDPN8e8FpYAql/bMV2oBEfC37K0gLEGgv9fy66juNShSMV8OkTqzltcG/w=="], + "eslint-plugin-react-refresh": ["eslint-plugin-react-refresh@0.4.26", "", { "peerDependencies": { "eslint": ">=8.40" } }, "sha512-1RETEylht2O6FM/MvgnyvT+8K21wLqDNg4qD51Zj3guhjt433XbnnkVttHMyaVyAFD03QSV4LPS5iE3VQmO7XQ=="], - "eslint-plugin-storybook": ["eslint-plugin-storybook@10.1.4", "", { "dependencies": { "@typescript-eslint/utils": "^8.8.1" }, "peerDependencies": { "eslint": ">=8", "storybook": "^10.1.4" } }, "sha512-itG2eLrWyuP5RGIL3TMGA5KSGoBOX3aTnQd43qLJu36ZMzd9H4RHN1I8WTVvyiaInppYJMGB4nnXzSdNXUUeTQ=="], + "eslint-plugin-storybook": ["eslint-plugin-storybook@10.1.9", "", { "dependencies": { "@typescript-eslint/utils": "^8.8.1" }, "peerDependencies": { "eslint": ">=8", "storybook": "^10.1.9" } }, "sha512-2XCnHhu+9ShW8U/MsvnlT4ZkzADIPtlfYVD/GBBbs8loWu0x9IZ3EfNg1LEImjvvNVDhwpd5K04lK4CAP+2bWA=="], "eslint-scope": ["eslint-scope@8.4.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg=="], @@ -996,7 +1008,7 @@ "exsolve": ["exsolve@1.0.8", "", {}, "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA=="], - "fast-copy": ["fast-copy@4.0.0", "", {}, "sha512-/oA0gx1xyXE9R2YlV4FXwZJXngFdm9Du0zN8FhY38jnLkhp1u35h6bCyKgRhlsA6C9I+1vfXE4KISdt7xc6M9w=="], + "fast-copy": ["fast-copy@4.0.2", "", {}, "sha512-ybA6PDXIXOXivLJK/z9e+Otk7ve13I4ckBvGO5I2RRmBU1gMHLVDJYEuJYhGwez7YNlYji2M2DvVU+a9mSFDlw=="], "fast-decode-uri-component": ["fast-decode-uri-component@1.0.1", "", {}, "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg=="], @@ -1054,7 +1066,7 @@ "get-tsconfig": ["get-tsconfig@4.13.0", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ=="], - "glob": ["glob@10.5.0", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg=="], + "glob": ["glob@11.1.0", "", { "dependencies": { "foreground-child": "^3.3.1", "jackspeak": "^4.1.1", "minimatch": "^10.1.1", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^2.0.0" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw=="], "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], @@ -1066,8 +1078,6 @@ "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], - "graphemer": ["graphemer@1.4.0", "", {}, "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag=="], - "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], @@ -1096,17 +1106,23 @@ "is-core-module": ["is-core-module@2.16.1", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w=="], + "is-docker": ["is-docker@3.0.0", "", { "bin": { "is-docker": "cli.js" } }, "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ=="], + "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], + "is-inside-container": ["is-inside-container@1.0.0", "", { "dependencies": { "is-docker": "^3.0.0" }, "bin": { "is-inside-container": "cli.js" } }, "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA=="], + + "is-wsl": ["is-wsl@3.1.0", "", { "dependencies": { "is-inside-container": "^1.0.0" } }, "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw=="], + "isbot": ["isbot@5.1.32", "", {}, "sha512-VNfjM73zz2IBZmdShMfAUg10prm6t7HFUQmNAEOAVS4YH92ZrZcvkMcGX6cIgBJAzWDzPent/EeAtYEHNPNPBQ=="], "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], - "jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="], + "jackspeak": ["jackspeak@4.1.1", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" } }, "sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ=="], "jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="], @@ -1232,12 +1248,12 @@ "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], + "open": ["open@10.2.0", "", { "dependencies": { "default-browser": "^5.2.1", "define-lazy-prop": "^3.0.0", "is-inside-container": "^1.0.0", "wsl-utils": "^0.1.0" } }, "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA=="], + "openapi-types": ["openapi-types@12.1.3", "", {}, "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw=="], "optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="], - "outline-sync": ["outline-sync@workspace:packages/outline-sync"], - "p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="], "p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="], @@ -1256,7 +1272,7 @@ "path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="], - "path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="], + "path-scurry": ["path-scurry@2.0.1", "", { "dependencies": { "lru-cache": "^11.0.0", "minipass": "^7.1.2" } }, "sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA=="], "path-to-regexp": ["path-to-regexp@0.1.12", "", {}, "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ=="], @@ -1306,7 +1322,7 @@ "raw-body": ["raw-body@2.5.3", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.4.24", "unpipe": "~1.0.0" } }, "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA=="], - "react": ["react@19.2.1", "", {}, "sha512-DGrYcCWK7tvYMnWh79yrPHt+vdx9tY+1gPZa7nJQtO/p8bLTDaHp4dzwEhQB7pZ4Xe3ok4XKuEPrVuc+wlpkmw=="], + "react": ["react@19.2.3", "", {}, "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA=="], "react-collapsible": ["react-collapsible@2.10.0", "", { "peerDependencies": { "react": "~15 || ~16 || ~17 || ~18", "react-dom": "~15 || ~16 || ~17 || ~18" } }, "sha512-kEVsmlFfXBMTCnU5gwIv19MdmPAhbIPzz5Er37TiJSzRKS0IHrqAKQyQeHEmtoGIQMTcVI46FzE4z3NlVTx77A=="], @@ -1314,15 +1330,15 @@ "react-docgen-typescript": ["react-docgen-typescript@2.4.0", "", { "peerDependencies": { "typescript": ">= 4.3.x" } }, "sha512-ZtAp5XTO5HRzQctjPU0ybY0RRCQO19X/8fxn3w7y2VVTUbGHDKULPTL4ky3vB05euSgG5NpALhEhDPvQ56wvXg=="], - "react-dom": ["react-dom@19.2.1", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.1" } }, "sha512-ibrK8llX2a4eOskq1mXKu/TGZj9qzomO+sNfO98M6d9zIPOEhlBkMkBUBLd1vgS0gQsLDBzA+8jJBVXDnfHmJg=="], + "react-dom": ["react-dom@19.2.3", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.3" } }, "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg=="], "react-is": ["react-is@17.0.2", "", {}, "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="], "react-refresh": ["react-refresh@0.14.2", "", {}, "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA=="], - "react-router": ["react-router@7.10.0", "", { "dependencies": { "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" }, "optionalPeers": ["react-dom"] }, "sha512-FVyCOH4IZ0eDDRycODfUqoN8ZSR2LbTvtx6RPsBgzvJ8xAXlMZNCrOFpu+jb8QbtZnpAd/cEki2pwE848pNGxw=="], + "react-router": ["react-router@7.10.1", "", { "dependencies": { "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" }, "optionalPeers": ["react-dom"] }, "sha512-gHL89dRa3kwlUYtRQ+m8NmxGI6CgqN+k4XyGjwcFoQwwCWF6xXpOCUlDovkXClS0d0XJN/5q7kc5W3kiFEd0Yw=="], - "react-router-dom": ["react-router-dom@7.10.0", "", { "dependencies": { "react-router": "7.10.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" } }, "sha512-Q4haR150pN/5N75O30iIsRJcr3ef7p7opFaKpcaREy0GQit6uCRu1NEiIFIwnHJQy0bsziRFBweR/5EkmHgVUQ=="], + "react-router-dom": ["react-router-dom@7.10.1", "", { "dependencies": { "react-router": "7.10.1" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" } }, "sha512-JNBANI6ChGVjA5bwsUIwJk7LHKmqB4JYnYfzFwyp2t12Izva11elds2jx7Yfoup2zssedntwU0oZ5DEmk5Sdaw=="], "readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], @@ -1344,7 +1360,9 @@ "resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="], - "rollup": ["rollup@4.53.3", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.53.3", "@rollup/rollup-android-arm64": "4.53.3", "@rollup/rollup-darwin-arm64": "4.53.3", "@rollup/rollup-darwin-x64": "4.53.3", "@rollup/rollup-freebsd-arm64": "4.53.3", "@rollup/rollup-freebsd-x64": "4.53.3", "@rollup/rollup-linux-arm-gnueabihf": "4.53.3", "@rollup/rollup-linux-arm-musleabihf": "4.53.3", "@rollup/rollup-linux-arm64-gnu": "4.53.3", "@rollup/rollup-linux-arm64-musl": "4.53.3", "@rollup/rollup-linux-loong64-gnu": "4.53.3", "@rollup/rollup-linux-ppc64-gnu": "4.53.3", "@rollup/rollup-linux-riscv64-gnu": "4.53.3", "@rollup/rollup-linux-riscv64-musl": "4.53.3", "@rollup/rollup-linux-s390x-gnu": "4.53.3", "@rollup/rollup-linux-x64-gnu": "4.53.3", "@rollup/rollup-linux-x64-musl": "4.53.3", "@rollup/rollup-openharmony-arm64": "4.53.3", "@rollup/rollup-win32-arm64-msvc": "4.53.3", "@rollup/rollup-win32-ia32-msvc": "4.53.3", "@rollup/rollup-win32-x64-gnu": "4.53.3", "@rollup/rollup-win32-x64-msvc": "4.53.3", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA=="], + "rollup": ["rollup@4.53.5", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.53.5", "@rollup/rollup-android-arm64": "4.53.5", "@rollup/rollup-darwin-arm64": "4.53.5", "@rollup/rollup-darwin-x64": "4.53.5", "@rollup/rollup-freebsd-arm64": "4.53.5", "@rollup/rollup-freebsd-x64": "4.53.5", "@rollup/rollup-linux-arm-gnueabihf": "4.53.5", "@rollup/rollup-linux-arm-musleabihf": "4.53.5", "@rollup/rollup-linux-arm64-gnu": "4.53.5", "@rollup/rollup-linux-arm64-musl": "4.53.5", "@rollup/rollup-linux-loong64-gnu": "4.53.5", "@rollup/rollup-linux-ppc64-gnu": "4.53.5", "@rollup/rollup-linux-riscv64-gnu": "4.53.5", "@rollup/rollup-linux-riscv64-musl": "4.53.5", "@rollup/rollup-linux-s390x-gnu": "4.53.5", "@rollup/rollup-linux-x64-gnu": "4.53.5", "@rollup/rollup-linux-x64-musl": "4.53.5", "@rollup/rollup-openharmony-arm64": "4.53.5", "@rollup/rollup-win32-arm64-msvc": "4.53.5", "@rollup/rollup-win32-ia32-msvc": "4.53.5", "@rollup/rollup-win32-x64-gnu": "4.53.5", "@rollup/rollup-win32-x64-msvc": "4.53.5", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-iTNAbFSlRpcHeeWu73ywU/8KuU/LZmNCSxp6fjQkJBD3ivUb8tpDrXhIxEzA05HlYMEwmtaUnb3RP+YNv162OQ=="], + + "run-applescript": ["run-applescript@7.1.0", "", {}, "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q=="], "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], @@ -1358,9 +1376,9 @@ "semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], - "send": ["send@0.19.1", "", { "dependencies": { "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "fresh": "0.5.2", "http-errors": "2.0.0", "mime": "1.6.0", "ms": "2.1.3", "on-finished": "2.4.1", "range-parser": "~1.2.1", "statuses": "2.0.1" } }, "sha512-p4rRk4f23ynFEfcD9LA0xRYngj+IyGiEYyqqOak8kaN0TvNmuxC2dcVeBn62GpCeR2CpWqyHCNScTP91QbAVFg=="], + "send": ["send@0.19.2", "", { "dependencies": { "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "fresh": "~0.5.2", "http-errors": "~2.0.1", "mime": "1.6.0", "ms": "2.1.3", "on-finished": "~2.4.1", "range-parser": "~1.2.1", "statuses": "~2.0.2" } }, "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg=="], - "serve-static": ["serve-static@1.16.2", "", { "dependencies": { "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", "send": "0.19.0" } }, "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw=="], + "serve-static": ["serve-static@1.16.3", "", { "dependencies": { "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", "send": "~0.19.1" } }, "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA=="], "set-cookie-parser": ["set-cookie-parser@2.7.2", "", {}, "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw=="], @@ -1400,7 +1418,7 @@ "statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="], - "storybook": ["storybook@10.1.4", "", { "dependencies": { "@storybook/global": "^5.0.0", "@storybook/icons": "^2.0.0", "@testing-library/jest-dom": "^6.6.3", "@testing-library/user-event": "^14.6.1", "@vitest/expect": "3.2.4", "@vitest/spy": "3.2.4", "esbuild": "^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0 || ^0.24.0 || ^0.25.0 || ^0.26.0 || ^0.27.0", "recast": "^0.23.5", "semver": "^7.6.2", "use-sync-external-store": "^1.5.0", "ws": "^8.18.0" }, "peerDependencies": { "prettier": "^2 || ^3" }, "optionalPeers": ["prettier"], "bin": "./dist/bin/dispatcher.js" }, "sha512-FrBjm8I8O+pYEOPHcdW9xWwgXSZxte7lza9q2lN3jFN4vuW79m5j0OnTQeR8z9MmIbBTvkIpp3yMBebl53Yt5Q=="], + "storybook": ["storybook@10.1.9", "", { "dependencies": { "@storybook/global": "^5.0.0", "@storybook/icons": "^2.0.0", "@testing-library/jest-dom": "^6.6.3", "@testing-library/user-event": "^14.6.1", "@vitest/expect": "3.2.4", "@vitest/spy": "3.2.4", "esbuild": "^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0 || ^0.24.0 || ^0.25.0 || ^0.26.0 || ^0.27.0", "open": "^10.2.0", "recast": "^0.23.5", "semver": "^7.6.2", "use-sync-external-store": "^1.5.0", "ws": "^8.18.0" }, "peerDependencies": { "prettier": "^2 || ^3" }, "optionalPeers": ["prettier"], "bin": "./dist/bin/dispatcher.js" }, "sha512-gHW/jOxLNzVw/Ys1XJovgrMFyh37ftMsLIw0l0h4fLsEyXhUABwrgjDp5bWrUmbQqemAIYVAAtw7UjPEdcHgkA=="], "string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], @@ -1426,7 +1444,7 @@ "synchronous-promise": ["synchronous-promise@2.0.17", "", {}, "sha512-AsS729u2RHUfEra9xJrE39peJcc2stq2+poBXX8bcM08Y6g9j/i/PUzwNQqkaJde7Ntg1TO7bSREbR5sdosQ+g=="], - "tailwindcss": ["tailwindcss@4.1.17", "", {}, "sha512-j9Ee2YjuQqYT9bbRTfTZht9W/ytp5H+jJpZKiYdP/bpnXARAuELt9ofP0lPnmHjbga7SNQIxdTAXCmtKVYjN+Q=="], + "tailwindcss": ["tailwindcss@4.1.18", "", {}, "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw=="], "tapable": ["tapable@2.3.0", "", {}, "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="], @@ -1460,19 +1478,19 @@ "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - "turbo": ["turbo@2.6.2", "", { "optionalDependencies": { "turbo-darwin-64": "2.6.2", "turbo-darwin-arm64": "2.6.2", "turbo-linux-64": "2.6.2", "turbo-linux-arm64": "2.6.2", "turbo-windows-64": "2.6.2", "turbo-windows-arm64": "2.6.2" }, "bin": { "turbo": "bin/turbo" } }, "sha512-LiQAFS6iWvnY8ViGtoPgduWBeuGH9B32XR4p8H8jxU5PudwyHiiyf1jQW0fCC8gCCTz9itkIbqZLIyUu5AG33w=="], + "turbo": ["turbo@2.6.3", "", { "optionalDependencies": { "turbo-darwin-64": "2.6.3", "turbo-darwin-arm64": "2.6.3", "turbo-linux-64": "2.6.3", "turbo-linux-arm64": "2.6.3", "turbo-windows-64": "2.6.3", "turbo-windows-arm64": "2.6.3" }, "bin": { "turbo": "bin/turbo" } }, "sha512-bf6YKUv11l5Xfcmg76PyWoy/e2vbkkxFNBGJSnfdSXQC33ZiUfutYh6IXidc5MhsnrFkWfdNNLyaRk+kHMLlwA=="], - "turbo-darwin-64": ["turbo-darwin-64@2.6.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-nF9d/YAyrNkyXn9lp3ZtgXPb7fZsik3cUNe/sBvUO0G5YezUS/kDYYw77IdjizDzairz8pL2ITCTUreG2d5iZQ=="], + "turbo-darwin-64": ["turbo-darwin-64@2.6.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-BlJJDc1CQ7SK5Y5qnl7AzpkvKSnpkfPmnA+HeU/sgny3oHZckPV2776ebO2M33CYDSor7+8HQwaodY++IINhYg=="], - "turbo-darwin-arm64": ["turbo-darwin-arm64@2.6.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-mmm0jFaVramST26XE1Lk2qjkjvLJHOe9f3TFjqY+aByjMK/ZmKE5WFPuCWo4L3xhwx+16T37rdPP//76loB3oA=="], + "turbo-darwin-arm64": ["turbo-darwin-arm64@2.6.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-MwVt7rBKiOK7zdYerenfCRTypefw4kZCue35IJga9CH1+S50+KTiCkT6LBqo0hHeoH2iKuI0ldTF2a0aB72z3w=="], - "turbo-linux-64": ["turbo-linux-64@2.6.2", "", { "os": "linux", "cpu": "x64" }, "sha512-IUMHjkVRJDUABGpi+iS1Le59aOl5DX88U5UT/mKaE7nNEjG465+a8UtYno56cZnLP+C6BkX4I93LFgYf9syjGQ=="], + "turbo-linux-64": ["turbo-linux-64@2.6.3", "", { "os": "linux", "cpu": "x64" }, "sha512-cqpcw+dXxbnPtNnzeeSyWprjmuFVpHJqKcs7Jym5oXlu/ZcovEASUIUZVN3OGEM6Y/OTyyw0z09tOHNt5yBAVg=="], - "turbo-linux-arm64": ["turbo-linux-arm64@2.6.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-0qQdZiimMUZj2Gfq87thYu0E02NaNcsB3lcEK/TD70Zzi7AxQoxye664Gis0Uao2j2L9/+05wC2btZ7SoFX3Gw=="], + "turbo-linux-arm64": ["turbo-linux-arm64@2.6.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-MterpZQmjXyr4uM7zOgFSFL3oRdNKeflY7nsjxJb2TklsYqiu3Z9pQ4zRVFFH8n0mLGna7MbQMZuKoWqqHb45w=="], - "turbo-windows-64": ["turbo-windows-64@2.6.2", "", { "os": "win32", "cpu": "x64" }, "sha512-BmMfFmt0VaoZL4NbtDq/dzGfjHsPoGU2+vFiZtkiYsttHY3fd/Dmgnu9PuRyJN1pv2M22q88rXO+dqYRHztLMw=="], + "turbo-windows-64": ["turbo-windows-64@2.6.3", "", { "os": "win32", "cpu": "x64" }, "sha512-biDU70v9dLwnBdLf+daoDlNJVvqOOP8YEjqNipBHzgclbQlXbsi6Gqqelp5er81Qo3BiRgmTNx79oaZQTPb07Q=="], - "turbo-windows-arm64": ["turbo-windows-arm64@2.6.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-0r4s4M/FgLxfjrdLPdqQUur8vZAtaWEi4jhkQ6wCIN2xzA9aee9IKwM53w7CQcjaLvWhT0AU7LTQHjFaHwxiKw=="], + "turbo-windows-arm64": ["turbo-windows-arm64@2.6.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-dDHVKpSeukah3VsI/xMEKeTnV9V9cjlpFSUs4bmsUiLu3Yv2ENlgVEZv65wxbeE0bh0jjpmElDT+P1KaCxArQQ=="], "tweetnacl": ["tweetnacl@0.14.5", "", {}, "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA=="], @@ -1484,7 +1502,7 @@ "typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="], - "typescript-eslint": ["typescript-eslint@8.48.1", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.48.1", "@typescript-eslint/parser": "8.48.1", "@typescript-eslint/typescript-estree": "8.48.1", "@typescript-eslint/utils": "8.48.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-FbOKN1fqNoXp1hIl5KYpObVrp0mCn+CLgn479nmu2IsRMrx2vyv74MmsBLVlhg8qVwNFGbXSp8fh1zp8pEoC2A=="], + "typescript-eslint": ["typescript-eslint@8.50.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.50.0", "@typescript-eslint/parser": "8.50.0", "@typescript-eslint/typescript-estree": "8.50.0", "@typescript-eslint/utils": "8.50.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-Q1/6yNUmCpH94fbgMUMg2/BSAr/6U7GBk61kZTv1/asghQOWOjTlp9K8mixS5NcJmm2creY+UFfGeW/+OcA64A=="], "uint8array-extras": ["uint8array-extras@1.5.0", "", {}, "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A=="], @@ -1496,7 +1514,7 @@ "unplugin": ["unplugin@2.3.11", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "acorn": "^8.15.0", "picomatch": "^4.0.3", "webpack-virtual-modules": "^0.6.2" } }, "sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww=="], - "update-browserslist-db": ["update-browserslist-db@1.2.0", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Dn+NlSF/7+0lVSEZ57SYQg6/E44arLzsVOGgrElBn/BlG1B8WKdbLppOocFrXwRNTkNlgdGNaBgH1o0lggDPiw=="], + "update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="], "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], @@ -1512,7 +1530,7 @@ "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="], - "vite": ["vite@7.2.6", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-tI2l/nFHC5rLh7+5+o7QjKjSR04ivXDF4jcgV0f/bTQ+OJiITy5S6gaynVsEM+7RqzufMnVbIon6Sr5x1SDYaQ=="], + "vite": ["vite@7.3.0", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg=="], "vite-node": ["vite-node@3.2.4", "", { "dependencies": { "cac": "^6.7.14", "debug": "^4.4.1", "es-module-lexer": "^1.7.0", "pathe": "^2.0.3", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "bin": { "vite-node": "vite-node.mjs" } }, "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg=="], @@ -1532,6 +1550,8 @@ "ws": ["ws@8.18.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg=="], + "wsl-utils": ["wsl-utils@0.1.0", "", { "dependencies": { "is-wsl": "^3.1.0" } }, "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw=="], + "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], "yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], @@ -1552,15 +1572,9 @@ "@dockstat/create-rr-elysia/typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], - "@dockstat/db/@types/bun": ["@types/bun@1.3.4", "", { "dependencies": { "bun-types": "1.3.4" } }, "sha512-EEPTKXHP+zKGPkhRLv+HI0UEX8/o+65hqARxLy8Ov5rIxMBPNTjeZww00CIihrIQGEQBYg+0roO5qOnS/7boGA=="], - - "@dockstat/logger/@types/bun": ["@types/bun@1.3.4", "", { "dependencies": { "bun-types": "1.3.4" } }, "sha512-EEPTKXHP+zKGPkhRLv+HI0UEX8/o+65hqARxLy8Ov5rIxMBPNTjeZww00CIihrIQGEQBYg+0roO5qOnS/7boGA=="], - - "@dockstat/sqlite-wrapper/@types/bun": ["@types/bun@1.3.4", "", { "dependencies": { "bun-types": "1.3.4" } }, "sha512-EEPTKXHP+zKGPkhRLv+HI0UEX8/o+65hqARxLy8Ov5rIxMBPNTjeZww00CIihrIQGEQBYg+0roO5qOnS/7boGA=="], + "@dockstat/outline-sync/@types/node": ["@types/node@20.19.27", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-N2clP5pJhB2YnZJ3PIHFk5RkygRX5WO/5f0WC08tp0wd+sv0rsJk3MqWn3CbNmT2J505a5336jaQj4ph1AdMug=="], - "@dockstat/typings/@types/bun": ["@types/bun@1.3.4", "", { "dependencies": { "bun-types": "1.3.4" } }, "sha512-EEPTKXHP+zKGPkhRLv+HI0UEX8/o+65hqARxLy8Ov5rIxMBPNTjeZww00CIihrIQGEQBYg+0roO5qOnS/7boGA=="], - - "@dockstat/ui/@types/node": ["@types/node@24.10.1", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ=="], + "@dockstat/ui/@types/node": ["@types/node@24.10.4", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-vnDVpYPMzs4wunl27jHrfmwojOGKya0xyM3sH+UE5iv5uPS6vX7UIoh6m+vQc5LGBq52HBKPIn/zcSZVzeDEZg=="], "@dockstat/ui/typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], @@ -1596,19 +1610,19 @@ "@testing-library/dom/dom-accessibility-api": ["dom-accessibility-api@0.5.16", "", {}, "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg=="], - "@types/body-parser/@types/node": ["@types/node@24.10.1", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ=="], + "@types/body-parser/@types/node": ["@types/node@24.10.4", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-vnDVpYPMzs4wunl27jHrfmwojOGKya0xyM3sH+UE5iv5uPS6vX7UIoh6m+vQc5LGBq52HBKPIn/zcSZVzeDEZg=="], - "@types/connect/@types/node": ["@types/node@24.10.1", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ=="], + "@types/connect/@types/node": ["@types/node@24.10.4", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-vnDVpYPMzs4wunl27jHrfmwojOGKya0xyM3sH+UE5iv5uPS6vX7UIoh6m+vQc5LGBq52HBKPIn/zcSZVzeDEZg=="], - "@types/docker-modem/@types/node": ["@types/node@24.10.1", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ=="], + "@types/docker-modem/@types/node": ["@types/node@24.10.4", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-vnDVpYPMzs4wunl27jHrfmwojOGKya0xyM3sH+UE5iv5uPS6vX7UIoh6m+vQc5LGBq52HBKPIn/zcSZVzeDEZg=="], - "@types/dockerode/@types/node": ["@types/node@24.10.1", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ=="], + "@types/dockerode/@types/node": ["@types/node@24.10.4", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-vnDVpYPMzs4wunl27jHrfmwojOGKya0xyM3sH+UE5iv5uPS6vX7UIoh6m+vQc5LGBq52HBKPIn/zcSZVzeDEZg=="], - "@types/express-serve-static-core/@types/node": ["@types/node@24.10.1", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ=="], + "@types/express-serve-static-core/@types/node": ["@types/node@24.10.4", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-vnDVpYPMzs4wunl27jHrfmwojOGKya0xyM3sH+UE5iv5uPS6vX7UIoh6m+vQc5LGBq52HBKPIn/zcSZVzeDEZg=="], - "@types/send/@types/node": ["@types/node@24.10.1", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ=="], + "@types/send/@types/node": ["@types/node@24.10.4", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-vnDVpYPMzs4wunl27jHrfmwojOGKya0xyM3sH+UE5iv5uPS6vX7UIoh6m+vQc5LGBq52HBKPIn/zcSZVzeDEZg=="], - "@types/serve-static/@types/node": ["@types/node@24.10.1", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ=="], + "@types/serve-static/@types/node": ["@types/node@24.10.4", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-vnDVpYPMzs4wunl27jHrfmwojOGKya0xyM3sH+UE5iv5uPS6vX7UIoh6m+vQc5LGBq52HBKPIn/zcSZVzeDEZg=="], "@types/ssh2/@types/node": ["@types/node@18.19.130", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg=="], @@ -1626,12 +1640,10 @@ "body-parser/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], - "bun-types/@types/node": ["@types/node@24.10.1", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ=="], + "bun-types/@types/node": ["@types/node@24.10.4", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-vnDVpYPMzs4wunl27jHrfmwojOGKya0xyM3sH+UE5iv5uPS6vX7UIoh6m+vQc5LGBq52HBKPIn/zcSZVzeDEZg=="], "compression/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], - "docknode/bun-types": ["bun-types@1.3.4", "", { "dependencies": { "@types/node": "*" } }, "sha512-5ua817+BZPZOlNaRgGBpZJOSAQ9RQ17pkwPD0yR7CfJg+r8DgIILByFifDTa+IPDDxzf5VNhtNlcKqFzDgJvlQ=="], - "dockstat/typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], "dts-bundle-generator/typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], @@ -1650,7 +1662,7 @@ "front-matter/js-yaml": ["js-yaml@3.14.2", "", { "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg=="], - "glob/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], + "glob/minimatch": ["minimatch@10.1.1", "", { "dependencies": { "@isaacs/brace-expansion": "^5.0.0" } }, "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ=="], "mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], @@ -1658,11 +1670,7 @@ "morgan/on-finished": ["on-finished@2.3.0", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww=="], - "outline-sync/@types/node": ["@types/node@20.19.26", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-0l6cjgF0XnihUpndDhk+nyD3exio3iKaYROSgvh/qSevPXax3L8p5DBRFjbvalnwatGgHEQn2R88y2fA3g4irg=="], - - "outline-sync/bun-types": ["bun-types@1.3.4", "", { "dependencies": { "@types/node": "*" } }, "sha512-5ua817+BZPZOlNaRgGBpZJOSAQ9RQ17pkwPD0yR7CfJg+r8DgIILByFifDTa+IPDDxzf5VNhtNlcKqFzDgJvlQ=="], - - "path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], + "path-scurry/lru-cache": ["lru-cache@11.2.4", "", {}, "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg=="], "pino-pretty/pino-abstract-transport": ["pino-abstract-transport@3.0.0", "", { "dependencies": { "split2": "^4.0.0" } }, "sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg=="], @@ -1672,28 +1680,14 @@ "pretty-format/ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="], - "protobufjs/@types/node": ["@types/node@24.10.1", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ=="], + "protobufjs/@types/node": ["@types/node@24.10.4", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-vnDVpYPMzs4wunl27jHrfmwojOGKya0xyM3sH+UE5iv5uPS6vX7UIoh6m+vQc5LGBq52HBKPIn/zcSZVzeDEZg=="], "redent/strip-indent": ["strip-indent@3.0.0", "", { "dependencies": { "min-indent": "^1.0.0" } }, "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ=="], "send/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], - "send/http-errors": ["http-errors@2.0.0", "", { "dependencies": { "depd": "2.0.0", "inherits": "2.0.4", "setprototypeof": "1.2.0", "statuses": "2.0.1", "toidentifier": "1.0.1" } }, "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ=="], - - "send/statuses": ["statuses@2.0.1", "", {}, "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ=="], - - "serve-static/send": ["send@0.19.0", "", { "dependencies": { "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "encodeurl": "~1.0.2", "escape-html": "~1.0.3", "etag": "~1.8.1", "fresh": "0.5.2", "http-errors": "2.0.0", "mime": "1.6.0", "ms": "2.1.3", "on-finished": "2.4.1", "range-parser": "~1.2.1", "statuses": "2.0.1" } }, "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw=="], - "vite-node/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], - "@dockstat/db/@types/bun/bun-types": ["bun-types@1.3.4", "", { "dependencies": { "@types/node": "*" } }, "sha512-5ua817+BZPZOlNaRgGBpZJOSAQ9RQ17pkwPD0yR7CfJg+r8DgIILByFifDTa+IPDDxzf5VNhtNlcKqFzDgJvlQ=="], - - "@dockstat/logger/@types/bun/bun-types": ["bun-types@1.3.4", "", { "dependencies": { "@types/node": "*" } }, "sha512-5ua817+BZPZOlNaRgGBpZJOSAQ9RQ17pkwPD0yR7CfJg+r8DgIILByFifDTa+IPDDxzf5VNhtNlcKqFzDgJvlQ=="], - - "@dockstat/sqlite-wrapper/@types/bun/bun-types": ["bun-types@1.3.4", "", { "dependencies": { "@types/node": "*" } }, "sha512-5ua817+BZPZOlNaRgGBpZJOSAQ9RQ17pkwPD0yR7CfJg+r8DgIILByFifDTa+IPDDxzf5VNhtNlcKqFzDgJvlQ=="], - - "@dockstat/typings/@types/bun/bun-types": ["bun-types@1.3.4", "", { "dependencies": { "@types/node": "*" } }, "sha512-5ua817+BZPZOlNaRgGBpZJOSAQ9RQ17pkwPD0yR7CfJg+r8DgIILByFifDTa+IPDDxzf5VNhtNlcKqFzDgJvlQ=="], - "@dockstat/ui/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], "@eslint/eslintrc/ajv/json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], @@ -1730,8 +1724,6 @@ "compression/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], - "docknode/bun-types/@types/node": ["@types/node@24.10.1", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ=="], - "elysia-basic-auth/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], "eslint/ajv/json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], @@ -1742,44 +1734,10 @@ "front-matter/js-yaml/argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="], - "glob/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], - "morgan/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], - "outline-sync/bun-types/@types/node": ["@types/node@24.10.1", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ=="], - "protobufjs/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], "send/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], - - "serve-static/send/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], - - "serve-static/send/encodeurl": ["encodeurl@1.0.2", "", {}, "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w=="], - - "serve-static/send/http-errors": ["http-errors@2.0.0", "", { "dependencies": { "depd": "2.0.0", "inherits": "2.0.4", "setprototypeof": "1.2.0", "statuses": "2.0.1", "toidentifier": "1.0.1" } }, "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ=="], - - "serve-static/send/statuses": ["statuses@2.0.1", "", {}, "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ=="], - - "@dockstat/db/@types/bun/bun-types/@types/node": ["@types/node@24.10.1", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ=="], - - "@dockstat/logger/@types/bun/bun-types/@types/node": ["@types/node@24.10.1", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ=="], - - "@dockstat/sqlite-wrapper/@types/bun/bun-types/@types/node": ["@types/node@24.10.1", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ=="], - - "@dockstat/typings/@types/bun/bun-types/@types/node": ["@types/node@24.10.1", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ=="], - - "docknode/bun-types/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], - - "outline-sync/bun-types/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], - - "serve-static/send/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], - - "@dockstat/db/@types/bun/bun-types/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], - - "@dockstat/logger/@types/bun/bun-types/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], - - "@dockstat/sqlite-wrapper/@types/bun/bun-types/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], - - "@dockstat/typings/@types/bun/bun-types/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], } } diff --git a/package.json b/package.json index 048fad1f..78c21405 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "lint:all": "biome check .", "lint:all:ci": "biome check . --reporter=github", - "lint:fix:all": "biome check --write .", + "lint:fix:all": "biome check --write --assist-enabled=true .", "format:all": "biome format --write .", "format:check:all": "biome format --check ." }, diff --git a/packages/docker-client/src/docker-client.ts b/packages/docker-client/src/docker-client.ts index 84795325..5697a27e 100644 --- a/packages/docker-client/src/docker-client.ts +++ b/packages/docker-client/src/docker-client.ts @@ -215,7 +215,7 @@ class DockerClient { )}` ) // Replace any existing instance - this.dockerInstances.delete(Number(Number(host.id))) + this.dockerInstances.delete(Number(host.id)) const dockerInstance = new Dockerode(instanceCfg) this.dockerInstances.set(Number(host.id), dockerInstance) } catch (err) { diff --git a/packages/outline-sync/README.md b/packages/outline-sync/README.md index c35c9b2b..82024a8c 100644 --- a/packages/outline-sync/README.md +++ b/packages/outline-sync/README.md @@ -9,6 +9,39 @@ bun install -g @dockstat/outline-sync ``` ## Features +<<<<<<< HEAD + +- ✅ Sync Outline → Local +- ✅ Sync Local → Outline +- ✅ Push local changes to Outline (new `push` command) +- ✅ Compare modification dates (mtime) and frontmatter `updatedAt` to detect changes +- ✅ Custom path mapping for specific documents +- ✅ Collection filtering (include/exclude) +- ✅ Frontmatter metadata preservation +- ✅ CI/CD integration +- ✅ File watching + +## What's new + +Three main improvements were added: + +1. Push command + - `outline-sync push` lets you push local changes up to Outline without performing a full `sync`/`syncDown` first. + - It compares local file timestamps against the remote document `updatedAt` and only pushes files that appear newer locally. + +2. Modification-date comparison + - When detecting local changes, the tool now prefers a frontmatter `updatedAt` timestamp (if present) and falls back to the file system modification time (`mtime`). + - This allows more accurate determination of whether a local edit should be pushed. + +3. Duplicate-folder fix (collection/document name collision) + - Fixed an issue where a collection and a top-level document shared the same title (for example: collection "DockStat" and a root document titled "DockStat") and the sync created a nested duplicate folder like `./dockstat/dockstat/README.md`. + - Now, when a root document's sanitized title equals the collection folder name, the file is written as `.//README.md` (no extra nested folder). This produces a single `./dockstat/README.md` for the example above and avoids confusing directory nesting. + - This behavior preserves existing custom paths while preventing accidental duplicate folders when collection and document titles collide. + +## Configuration + +Create `outline-sync.config.json` (recommended): +======= - ✅ Sync Outline → Local - ✅ Sync Local → Outline @@ -30,6 +63,7 @@ outline-sync init ``` Then edit the generated file: +>>>>>>> fc102f80d85485391e48e88e7431dd52b58e3cc7 ```json { @@ -37,35 +71,22 @@ Then edit the generated file: "token": "your_api_token", "outputDir": "./outline-docs", "includeCollections": ["Engineering", "Product"], +<<<<<<< HEAD + "excludeCollections": [], + "customPaths": { + "example-doc-id": "../../README.md", + "another-doc-id": "custom/path/document.md" +======= "excludeCollections": ["Archive", "Private"], "customPaths": { "doc-id-abc123": "../../README.md", "doc-id-xyz789": "custom/important-doc.md" +>>>>>>> fc102f80d85485391e48e88e7431dd52b58e3cc7 } } ``` -**Collection Filtering:** -- `includeCollections`: Array of collection names or IDs to sync. If specified, only these collections are synced. -- `excludeCollections`: Array of collection names or IDs to exclude from sync. Applies when `includeCollections` is not set. -- Collections can be matched by name (case-insensitive) or by ID -- If both are specified, `includeCollections` takes precedence - -**Custom Paths:** -- Document IDs can be found in the Outline URL or via the API -- Paths starting with `..` are relative to current working directory -- Other paths are relative to `outputDir` - -**Finding Collection Names:** -1. Look at your Outline sidebar for collection names -2. Or run initial sync without filters to see all collections - -**Finding Document IDs:** -1. Open document in Outline -2. Check URL: `https://outline.com/doc/my-document-abc123` (ID is `abc123`) -3. Or run initial sync and check frontmatter in generated files - -### Option 2: Environment Variables +You can also use environment variables: ```bash export OUTLINE_URL=https://your-outline.com @@ -73,213 +94,98 @@ export OUTLINE_TOKEN=your_api_token export OUTLINE_OUTPUT_DIR=./outline-docs ``` -### Option 3: CLI Arguments - -Pass configuration via command line (takes precedence over other methods). +Or pass options via CLI (these take precedence over env/config file). ## Usage ### Commands -**Initialize config file:** - -```bash -outline-sync init -``` - -**One-time sync (down only):** +- `outline-sync init` + Create a sample `outline-sync.config.json` in the current directory. -```bash -# Sync all collections -outline-sync sync +- `outline-sync sync` + One-time sync from Outline → local. Pulls documents and writes frontmatter (including `updatedAt`) into each `README.md`. -# Sync specific collections -outline-sync sync --include "Engineering,Product" +- `outline-sync watch` + Watches your `outputDir` for local changes and pushes them to Outline as they happen (bidirectional). -# Sync all except specific collections -outline-sync sync --exclude "Archive,Private" +- `outline-sync ci` + CI/CD friendly flow: performs a `syncDown` (pulls remote docs and caches their `updatedAt`), finds local files newer than the cached remote timestamps, and pushes those changes. -# With config file -outline-sync sync --config custom-config.json -``` +- `outline-sync push` (new) + Push local changes to Outline by comparing each local file against the actual remote document `updatedAt`. This does NOT perform an initial `syncDown`. For each local markdown file: + - The tool reads frontmatter for an `id` (document ID). If missing, the file is skipped. + - It prefers frontmatter `updatedAt` (if present) when comparing timestamps. + - Otherwise it falls back to the file system `mtime`. + - It fetches the remote document's `updatedAt` (if not already cached) and compares. + - If the local timestamp is later than remote `updatedAt`, the file is pushed. + - If the remote document cannot be fetched (deleted/permission issues), the file is included in the push list so you can inspect it. -**Watch mode (bidirectional):** +### Examples +Sync all collections (uses config/env or CLI args for credentials if required): ```bash -outline-sync watch - -# With filters -outline-sync watch --include "Engineering" -``` - -**CI/CD mode:** - -```bash -outline-sync ci - -# With filters -outline-sync ci --exclude "Draft,Private" -``` - -## API Reference (CLI Commands) - -### `outline-sync init` - -Creates a sample `outline-sync.config.json` configuration file in the current directory. - -**Example:** -```bash -outline-sync init -``` - -### `outline-sync sync [options]` - -Performs a one-time synchronization from your Outline wiki to your local folder. This command only pulls changes from Outline. - -**Options:** - -- `--url `: The URL of your Outline instance. Can also be set via `OUTLINE_URL` env var or config file. -- `--token `: Your Outline API token. Can also be set via `OUTLINE_TOKEN` env var or config file. -- `--output `: The local directory where documents will be stored. Default: `./outline-docs` -- `--config `: Path to config file. Default: `./outline-sync.config.json` -- `--include `: Comma-separated list of collection names/IDs to include (e.g., `"Engineering,Product"`). If set, only these collections are synced. -- `--exclude `: Comma-separated list of collection names/IDs to exclude (e.g., `"Archive,Private"`). Ignored if `--include` is set. - -**Examples:** -```bash -# Sync all collections -outline-sync sync --url https://my.outline.app --token - -# Sync only Engineering and Product collections -outline-sync sync --include "Engineering,Product" - -# Sync all except Archive -outline-sync sync --exclude "Archive" +outline-sync sync ``` -### `outline-sync watch [options]` - -Starts a persistent process that watches for changes in your local directory and syncs them to Outline, and also pulls new changes from Outline periodically. This enables bidirectional synchronization. - -**Options:** - -- `--url `: The URL of your Outline instance. -- `--token `: Your Outline API token. -- `--output `: The local directory being watched. Default: `./outline-docs` -- `--config `: Path to config file. Default: `./outline-sync.config.json` -- `--include `: Comma-separated list of collection names/IDs to include. -- `--exclude `: Comma-separated list of collection names/IDs to exclude. - -**Example:** +Watch and auto-push local edits: ```bash outline-sync watch --include "Engineering" ``` -### `outline-sync ci [options]` - -Executes a CI/CD-friendly synchronization. This command first pulls all documents from Outline, then checks for any local changes in your configured output directory, and finally pushes those local changes back to Outline. Designed for automated environments. - -**Options:** - -- `--url `: The URL of your Outline instance. -- `--token `: Your Outline API token. -- `--output `: The local directory to synchronize. Default: `./outline-docs` -- `--config `: Path to config file. Default: `./outline-sync.config.json` -- `--include `: Comma-separated list of collection names/IDs to include. -- `--exclude `: Comma-separated list of collection names/IDs to exclude. - -**Example:** +CI job (pull then push any local changes that are newer than remote): ```bash outline-sync ci --exclude "Private,Draft" ``` ---- - -## Examples - -### Example 1: Sync Only Engineering Docs - -**Config file:** -```json -{ - "url": "https://outline.example.com", - "token": "your_token", - "includeCollections": ["Engineering", "API Reference"] -} -``` - -**CLI:** +Push local changes (no initial sync; queries remote per-file): ```bash -outline-sync sync --include "Engineering,API Reference" +outline-sync push ``` -### Example 2: Exclude Private Collections - -**Config file:** -```json -{ - "url": "https://outline.example.com", - "token": "your_token", - "excludeCollections": ["Private", "Archive", "Draft"] -} -``` - -**CLI:** +Push while specifying config/credentials inline: ```bash -outline-sync sync --exclude "Private,Archive,Draft" -``` - -### Example 3: Root README with Collection Filter - -Place a specific document as your project's main README while only syncing specific collections: - -```json -{ - "includeCollections": ["Public Docs"], - "customPaths": { - "doc-abc123": "../../README.md" - } -} +outline-sync push --url https://my.outline.app --token --output ./outline-docs ``` -### Example 4: Mixed Structure +## How change detection works -Combine auto-organized docs with custom locations and collection filtering: +When deciding whether a local file should be pushed upward, the tool uses this priority: -```json -{ - "outputDir": "./docs", - "includeCollections": ["Engineering", "Product"], - "customPaths": { - "doc-home": "../../README.md", - "doc-api": "../../API.md", - "doc-contrib": "../../CONTRIBUTING.md" - } -} -``` +1. frontmatter `updatedAt` (if present in the markdown file's frontmatter) +2. file system `mtime` (the file's modification time on disk) -Other documents from included collections will be organized in `./docs` by collection. +That local timestamp is compared against the remote document's `updatedAt`. If the local timestamp is newer, the file is scheduled to be pushed. -### Example 5: Getting Collection Names +Notes: +- `outline-sync ci` will populate an internal cache of remote `updatedAt` values during the initial `syncDown` and use that to avoid fetching each remote doc again. +- `outline-sync push` will fetch remote `updatedAt` per-document as it evaluates each file (useful for CI runs where you didn't perform a `syncDown` first). +- Frontmatter must contain a valid `id` (document ID) for a file to be considered for push. If `id` is missing the file will be skipped. -After first sync without filters, you'll see output like: +## Frontmatter format -```text -Found 5 collections -📚 Syncing collection: Engineering -✓ API Documentation -✓ System Architecture +Each synced file includes frontmatter with metadata similar to: -📚 Syncing collection: Product -✓ Roadmap -✓ Feature Specs +```yaml +--- +id: doc-id-123 +title: My Document +collectionId: col-456 +parentDocumentId: null +updatedAt: 2025-01-01T12:34:56.000Z +urlId: my-document-urlid +--- ``` -Use these names in your `includeCollections` or `excludeCollections` array. +The `updatedAt` field is written by `sync` when pulling from Outline. If you edit a file and update the frontmatter `updatedAt` manually to a newer timestamp, the tool will honor that value when deciding to push. -## CI/CD Integration +## Safety notes & best practices -### GitHub Actions +- Always ensure your frontmatter `id` is present if you expect a file to be pushed back to Outline. +- When using `push` in CI, be aware it will fetch remote metadata for every document being considered; this will incur API calls. +- If a file cannot be matched to a remote document (missing id or fetch error), it will be surfaced so you can investigate before pushing. + +## CI/CD example (GitHub Actions) ```yaml name: Outline Sync @@ -288,7 +194,11 @@ on: push: branches: [main] schedule: +<<<<<<< HEAD + - cron: "0 */6 * * *" +======= - cron: "0 */6 * * *" # Every 6 hours +>>>>>>> fc102f80d85485391e48e88e7431dd52b58e3cc7 jobs: sync: @@ -305,3 +215,12 @@ jobs: with: commit_message: "docs: sync from Outline" ``` +<<<<<<< HEAD + +## Troubleshooting + +- If you see files being pushed unexpectedly, inspect their frontmatter `updatedAt` values and file `mtime`. +- If push fails due to permissions, confirm the API token has the required document update scope. +- For large repos, consider running `sync` in a scheduled job and using `push` only when necessary to reduce API calls. +======= +>>>>>>> fc102f80d85485391e48e88e7431dd52b58e3cc7 diff --git a/packages/outline-sync/package.json b/packages/outline-sync/package.json index 2d6bf595..002fc316 100644 --- a/packages/outline-sync/package.json +++ b/packages/outline-sync/package.json @@ -1,6 +1,6 @@ { "name": "@dockstat/outline-sync", - "version": "1.2.3", + "version": "1.2.4", "type": "module", "main": "dist/index.js", "bin": { diff --git a/packages/outline-sync/src/client.ts b/packages/outline-sync/src/client.ts index d1c455a2..eb106334 100644 --- a/packages/outline-sync/src/client.ts +++ b/packages/outline-sync/src/client.ts @@ -1,4 +1,4 @@ -import type { OutlineConfig, Document, Collection, DocumentMetadata } from "./types" +import type { Collection, Document, OutlineConfig } from "./types" export class OutlineClient { private baseUrl: string diff --git a/packages/outline-sync/src/index.ts b/packages/outline-sync/src/index.ts index fcebb947..177588ec 100644 --- a/packages/outline-sync/src/index.ts +++ b/packages/outline-sync/src/index.ts @@ -1,10 +1,9 @@ -#!/usr/bin/env bun +import { existsSync } from "node:fs" +import { readFile, writeFile } from "node:fs/promises" +import { join } from "node:path" import { Command } from "commander" import { OutlineSync } from "./sync" import type { OutlineConfig } from "./types" -import { existsSync } from "fs" -import { readFile, writeFile } from "fs/promises" -import { join } from "path" const program = new Command() @@ -28,7 +27,18 @@ function parseArrayOption(value: string): string[] { .filter(Boolean) } -async function getConfig(options: any, loadConfigFile = true): Promise { +async function getConfig( + options: { + url: string + include: string + exclude: string + token?: string + outputDir: string + output: string + config: string + }, + loadConfigFile = true +): Promise { const fileConfig = loadConfigFile ? await loadConfig(options.config) : {} const config: OutlineConfig = { @@ -104,6 +114,21 @@ program await sync.ciSync() }) +program + .command("push") + .description("Push local changes to Outline") + .option("-u, --url ", "Outline URL") + .option("-t, --token ", "API token") + .option("-o, --output ", "Output directory") + .option("-c, --config ", "Config file path") + .option("-i, --include ", "Comma-separated list of collections to include") + .option("-e, --exclude ", "Comma-separated list of collections to exclude") + .action(async (options) => { + const config = await getConfig(options) + const sync = new OutlineSync(config) + await sync.push() + }) + program .command("init") .description("Create a sample configuration file") diff --git a/packages/outline-sync/src/sync.ts b/packages/outline-sync/src/sync.ts index d7d54957..528e023e 100644 --- a/packages/outline-sync/src/sync.ts +++ b/packages/outline-sync/src/sync.ts @@ -1,10 +1,10 @@ -import { mkdir, writeFile, readFile, readdir, stat } from "fs/promises" -import { join, dirname } from "path" +import { mkdir, readdir, readFile, stat, writeFile } from "node:fs/promises" +import { join } from "node:path" import { watch } from "chokidar" import fm from "front-matter" import YAML from "yaml" -import type { OutlineConfig, Document, DocumentMetadata, Collection } from "./types" import { OutlineClient } from "./client" +import type { Document, DocumentMetadata, OutlineConfig } from "./types" interface DocumentNode { document: Document @@ -100,12 +100,22 @@ export class OutlineSync { const docPath = this.sanitizePath(doc.title) // Build path based on hierarchy - if (parentPath) { + // Avoid creating a duplicate nested folder when the document title equals the collection name. + // Example: collection "DockStat" with a root doc titled "DockStat" should produce: + // ./dockstat/README.md + // not: + // ./dockstat/dockstat/README.md + if (!parentPath) { + // If root-level and the sanitized document name equals the collection folder name, + // place the README directly inside the collection folder. + if (docPath === collectionPath) { + return join(this.outputDir, collectionPath) + } + // Root level document with a different name -> subfolder under collection + return join(this.outputDir, collectionPath, docPath) + } else { // Nested document return join(parentPath, docPath) - } else { - // Root level document in collection - return join(this.outputDir, collectionPath, docPath) } } @@ -268,11 +278,19 @@ export class OutlineSync { continue } - // Compare content or modification time + // Prefer frontmatter updatedAt if present, otherwise fall back to file mtime. + // Compare those timestamps against the cached remote updatedAt (from last syncDown). + const fileFrontUpdated = parsed.attributes.updatedAt + ? new Date(parsed.attributes.updatedAt) + : null const fileStat = await stat(file) + const fileMtime = fileStat.mtime const cachedTime = new Date(cached.updatedAt) - if (fileStat.mtime > cachedTime) { + if ( + (fileFrontUpdated && fileFrontUpdated > cachedTime) || + (!fileFrontUpdated && fileMtime > cachedTime) + ) { changed.push(file) } } @@ -301,4 +319,73 @@ export class OutlineSync { return files } + + /** + * Compare local files against the remote Outline documents by fetching the remote + * document for each file ID and comparing timestamps. This method is used by the + * `push` flow where we want to push local changes up without first doing a syncDown. + */ + private async findChangedFilesAgainstRemote(): Promise { + const changed: string[] = [] + const files = await this.getAllMarkdownFiles(this.outputDir) + + for (const file of files) { + const content = await readFile(file, "utf-8") + const parsed = fm(content) + + if (!parsed.attributes?.id) continue + + const id = parsed.attributes.id + + try { + const remote = await this.client.getDocument(id) + const remoteTime = new Date(remote.updatedAt) + + const fileFrontUpdated = parsed.attributes.updatedAt + ? new Date(parsed.attributes.updatedAt) + : null + const fileStat = await stat(file) + const fileMtime = fileStat.mtime + + if ( + (fileFrontUpdated && fileFrontUpdated > remoteTime) || + (!fileFrontUpdated && fileMtime > remoteTime) + ) { + changed.push(file) + } + } catch (_) { + // If we can't fetch the remote document (deleted/permission/etc.), mark for push so the user can inspect. + changed.push(file) + } + } + + return changed + } + + /** + * Push local changes to Outline by comparing each local file to the remote document. + * This does NOT perform a syncDown first; it queries the server for each document ID + * and pushes anything that appears newer locally. + */ + async push(): Promise { + console.log("📤 Pushing local changes to Outline...") + + const changedFiles = await this.findChangedFilesAgainstRemote() + + if (changedFiles.length === 0) { + console.log("✅ No local changes to push") + return + } + + console.log(`\n📤 Pushing ${changedFiles.length} changed file(s) to Outline...`) + for (const file of changedFiles) { + try { + await this.syncUp(file) + } catch (error) { + console.error(`Error pushing ${file}:`, error) + } + } + + console.log("\n✅ Push complete!") + } } diff --git a/packages/outline-sync/src/types.ts b/packages/outline-sync/src/types.ts index 02d01259..08598c6f 100644 --- a/packages/outline-sync/src/types.ts +++ b/packages/outline-sync/src/types.ts @@ -1,36 +1,36 @@ export interface OutlineConfig { - url: string; - token: string; - outputDir?: string; - customPaths?: Record; - includeCollections?: string[]; - excludeCollections?: string[]; + url: string + token: string + outputDir?: string + customPaths?: Record + includeCollections?: string[] + excludeCollections?: string[] } export interface Document { - id: string; - title: string; - text: string; - collectionId: string; - parentDocumentId: string | null; - publishedAt: string | null; - updatedAt: string; - createdAt: string; - urlId: string; + id: string + title: string + text: string + collectionId: string + parentDocumentId: string | null + publishedAt: string | null + updatedAt: string + createdAt: string + urlId: string } export interface Collection { - id: string; - name: string; - description: string; - sort: Record; + id: string + name: string + description: string + sort: Record } export interface DocumentMetadata { - id: string; - title: string; - collectionId: string; - parentDocumentId: string | null; - updatedAt: string; - urlId: string; + id: string + title: string + collectionId: string + parentDocumentId: string | null + updatedAt: string + urlId: string } diff --git a/packages/react-router-elysia/app/routes/example.tsx b/packages/react-router-elysia/app/routes/example.tsx index fd53f756..53b08e98 100644 --- a/packages/react-router-elysia/app/routes/example.tsx +++ b/packages/react-router-elysia/app/routes/example.tsx @@ -208,6 +208,7 @@ export default function ExampleRoute() { fill="currentColor" aria-hidden > + Send Send @@ -287,6 +288,7 @@ function PostCard({ obj }: { obj: { name: string; url: string; description: stri stroke="currentColor" aria-hidden > + icon diff --git a/packages/react-router-elysia/app/welcome/logo-dark.svg b/packages/react-router-elysia/app/welcome/logo-dark.svg index dd820289..dc663c21 100644 --- a/packages/react-router-elysia/app/welcome/logo-dark.svg +++ b/packages/react-router-elysia/app/welcome/logo-dark.svg @@ -1,23 +1 @@ - - - - - - - - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/packages/react-router-elysia/app/welcome/logo-light.svg b/packages/react-router-elysia/app/welcome/logo-light.svg index 73284929..c1142dee 100644 --- a/packages/react-router-elysia/app/welcome/logo-light.svg +++ b/packages/react-router-elysia/app/welcome/logo-light.svg @@ -1,23 +1 @@ - - - - - - - - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/packages/react-router-elysia/app/welcome/welcome.tsx b/packages/react-router-elysia/app/welcome/welcome.tsx index 67ecdbc0..1e5769fc 100644 --- a/packages/react-router-elysia/app/welcome/welcome.tsx +++ b/packages/react-router-elysia/app/welcome/welcome.tsx @@ -16,7 +16,7 @@ export function Welcome() { ElysiaJS Logo -
+
-
+