Compare commits

Author SHA1 Message Date
alexz 0e053b0936 probe: architect bundle transfer (encoding test) 2026-06-18 05:26:35 +00:00
32 changed files with 24 additions and 1184 deletions
+1
View File
@@ -0,0 +1 @@
HELLO-ARCHITECT-PROBE-1234
-34
View File
@@ -1,34 +0,0 @@
# be9700-api — Build Notes
## Image
`git.alexzaw.dev/alexz/be9700-api:<tag>`
## Source
`/home/alexz/be9700-api` (also mirrored to `https://git.alexzaw.dev/alexz/be9700-api`)
The Dockerfile lives in the root of the source directory alongside `server.js`.
## Architecture
- **amd64 only** — the app is pure Node.js (no native addons) but the image is
only built and tested on the amd64 host. Add `--platform linux/arm64` to the
build command and push a separate tag to add arm64 support.
## Security
`.dockerignore` excludes `.env`, `.env.*`, `.git`, `scripts/`, `*.log`, `*.curl`,
`network-captures.js`. The image contains no router credentials.
## Build
```bash
# From this directory:
bash build.sh
# Or with overrides:
IMAGE_TAG=1.0.1 bash build.sh --no-cache
```
Requires the Docker daemon to be authenticated to `git.alexzaw.dev`.
-26
View File
@@ -1,26 +0,0 @@
#!/bin/bash
# Build and push the be9700-api Docker image from the source in /home/alexz/be9700-api.
# The Dockerfile lives alongside the source (not in this build/ dir).
set -euo pipefail
IMAGE_NAME="${IMAGE_NAME:-git.alexzaw.dev/alexz/be9700-api}"
IMAGE_TAG="${IMAGE_TAG:-1.0.0}"
SOURCE_DIR="${SOURCE_DIR:-/home/alexz/be9700-api}"
echo "==> Building ${IMAGE_NAME}:${IMAGE_TAG}"
docker build "$@" \
-t "${IMAGE_NAME}:${IMAGE_TAG}" \
-t "${IMAGE_NAME}:latest" \
"${SOURCE_DIR}"
echo "==> Pushing ${IMAGE_NAME}:${IMAGE_TAG}"
docker push "${IMAGE_NAME}:${IMAGE_TAG}"
echo "==> Pushing ${IMAGE_NAME}:latest"
docker push "${IMAGE_NAME}:latest"
echo ""
echo "Done. To update on Runtipi:"
echo " cd /etc/runtipi/repos/runtipi && sudo git add . && sudo git commit -m 'be9700-api: bump' && sudo git push origin main"
echo " sudo /etc/runtipi/runtipi-cli appstore update"
echo " sudo /etc/runtipi/runtipi-cli app update be9700-api:runtipi"
-21
View File
@@ -1,21 +0,0 @@
{
"$schema": "https://schemas.runtipi.io/app-info.json",
"name": "BE9700 Router API",
"id": "be9700-api",
"available": true,
"short_desc": "Stateless proxy + Scalar API docs for the TP-Link BE9700 WiFi 7 router",
"author": "alexz",
"port": 8347,
"categories": ["network", "utilities"],
"description": "A stateless Node.js proxy server that exposes the TP-Link BE9700 WiFi 7 router's internal API as a clean REST surface with interactive Scalar docs at /docs. Credentials are never stored — callers pass X-Router-Host and X-Router-Password (or X-Router-Session) per request. Covers 203 endpoints: wireless bands (2.4 GHz / 5 GHz / 6 GHz), LAN/DHCP, NAT, UPnP, time, and diagnostics.",
"tipi_version": 1,
"version": "1.1.0",
"source": "https://git.alexzaw.dev/alexz/be9700-api",
"website": "https://git.alexzaw.dev/alexz/be9700-api",
"exposable": true,
"dynamic_config": true,
"no_gui": false,
"form_fields": [],
"supported_architectures": ["amd64"],
"min_tipi_version": "4.0.0"
}
-24
View File
@@ -1,24 +0,0 @@
{
"$schema": "https://schemas.runtipi.io/dynamic-compose.json",
"schemaVersion": 2,
"services": [
{
"name": "be9700-api",
"image": "git.alexzaw.dev/alexz/be9700-api:1.1.0",
"isMain": true,
"internalPort": 3000,
"environment": [
{ "key": "PORT", "value": "3000" },
{ "key": "TZ", "value": "${TZ}" },
{ "key": "NODE_ENV", "value": "production" }
],
"healthCheck": {
"test": "wget -qO- http://127.0.0.1:3000/ >/dev/null 2>&1 || exit 1",
"interval": "30s",
"timeout": "5s",
"retries": 3,
"startPeriod": "15s"
}
}
]
}
-42
View File
@@ -1,42 +0,0 @@
# BE9700 Router API
A stateless Node.js proxy and interactive API documentation server for the
**TP-Link BE9700 WiFi 7** router.
## What it does
- Exposes the router's internal API as a clean, resource-oriented REST surface
(`/api/*`) covering **203 endpoints** across wireless bands (2.4 GHz / 5 GHz /
6 GHz), LAN/DHCP, NAT, UPnP, system time, and diagnostics.
- Serves **interactive Scalar API docs** at `/docs` — explore and try every
endpoint directly in the browser.
- Returns the OpenAPI 3.1 specification at `/openapi.json` for use with any
compatible tooling.
- Health/status JSON at `/`.
## Stateless header-auth model
No router credentials are stored in the container or in Runtipi form fields.
Every request to `/api/*` must carry:
| Header | Purpose |
|--------|---------|
| `X-Router-Host` | Router IP or hostname (e.g. `192.168.0.1`) |
| `X-Router-Password` | Router admin password (for fresh sessions) |
| `X-Router-Session` | Reuse a session token returned by a previous call (JSON) |
The container holds no stored secrets, but the `/api/*` surface controls your
router and CORS is open — anyone who can reach the service and supply a valid
router password can change router settings. Keep this app LAN-only unless you
place an auth middleware (e.g. Traefik forward-auth / Authentik) in front of it
before exposing it to the internet.
## Quick start
Once installed, open the **Scalar docs** at `http://<your-server>:8347/docs`
and authenticate each request with the `X-Router-Host` and
`X-Router-Password` headers.
## Source
`https://git.alexzaw.dev/alexz/be9700-api`
Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

-16
View File
@@ -1,16 +0,0 @@
# costco-watch image
Dependency-free Node service (node:22-alpine + curl for the healthcheck).
Source: https://git.alexzaw.dev/alexz/costco-watch (working copy:
~/projects/costco-watch). Build + push with ./build.sh.
Gotchas:
- Costco stalls (no response, just a hang) on requests lacking Sec-Fetch-*
headers. watch.js sends a browser header set in BROWSER_HEADERS - do not
strip it. Any single Sec-Fetch-* header is sufficient; UA/Accept alone are
not. Cloudflare Transform Rules CANNOT substitute for this: Sec-* headers are
on Cloudflare's restricted list, and cloudflared can't add headers at all.
- TARGET_URL goes through the costco-tire cloudflared ingress, which sets
httpHostHeader: tires.costco.com.
- State (last stock value, transition history) lives in ${APP_DATA_DIR}/data;
wiping it means the next check can re-alert.
-7
View File
@@ -1,7 +0,0 @@
#!/bin/bash
set -euo pipefail
IMAGE_NAME="${IMAGE_NAME:-git.alexzaw.dev/alexz/costco-watch}"
IMAGE_TAG="${IMAGE_TAG:-latest}"
SRC="${SRC:-$HOME/projects/costco-watch}"
docker build "$@" -t "${IMAGE_NAME}:${IMAGE_TAG}" "$SRC"
docker push "${IMAGE_NAME}:${IMAGE_TAG}"
-84
View File
@@ -1,84 +0,0 @@
{
"$schema": "../app-info-schema.json",
"name": "Costco Stock Watch",
"id": "costco-watch",
"available": true,
"port": 8592,
"exposable": false,
"dynamic_config": true,
"no_gui": true,
"tipi_version": 1,
"version": "1.0.0",
"categories": [
"automation",
"utilities"
],
"description": "Polls a Costco tire product page on an interval and pushes an ntfy alert the moment it flips from out-of-stock to in-stock. Sends browser fetch-metadata headers itself - Costco stalls requests without Sec-Fetch-*, and neither cloudflared nor Cloudflare Transform Rules can supply them (Sec-* is on Cloudflare's restricted list). No Cloudflare-side configuration needed.",
"short_desc": "Alerts when a Costco tire is back in stock",
"author": "alexz",
"source": "https://git.alexzaw.dev/alexz/costco-watch",
"form_fields": [
{
"type": "text",
"label": "Product URL",
"env_variable": "TARGET_URL",
"required": true,
"default": "https://costco-tire.alexzaw.dev/SearchResultsByItemOrPart?ItemNo=1273727",
"hint": "Full product page URL to watch, via the costco-tire proxy"
},
{
"type": "text",
"label": "Item Label",
"env_variable": "ITEM_LABEL",
"required": false,
"default": "Michelin Primacy Tour A/S 235/45R18",
"hint": "Friendly name used in the notification text"
},
{
"type": "text",
"label": "ntfy Topic",
"env_variable": "NTFY_TOPIC",
"required": true,
"hint": "Topic to publish to, e.g. costco-tire. Subscribe to it in the ntfy app."
},
{
"type": "text",
"label": "ntfy URL",
"env_variable": "NTFY_URL",
"required": false,
"default": "https://ntfy.alexzaw.dev",
"hint": "ntfy server base URL"
},
{
"type": "password",
"label": "ntfy Token",
"env_variable": "NTFY_TOKEN",
"required": false,
"hint": "Only needed if the topic requires auth. Leave blank for an open topic."
},
{
"type": "text",
"label": "Check Interval (minutes)",
"env_variable": "INTERVAL_MINUTES",
"required": false,
"default": "60",
"hint": "Keep at 60 or more - this is a courtesy check, not a scraper"
},
{
"type": "random",
"label": "API Key",
"env_variable": "API_KEY",
"required": true,
"min": 24,
"encoding": "hex",
"hint": "Guards /status and /check. Auto-generated."
}
],
"supported_architectures": [
"amd64"
],
"created_at": 1754995000000,
"updated_at": 1754995000000,
"deprecated": false,
"min_tipi_version": "4.5.0"
}
-37
View File
@@ -1,37 +0,0 @@
{
"schemaVersion": 2,
"$schema": "https://schemas.runtipi.io/dynamic-compose.json",
"services": [
{
"name": "costco-watch",
"image": "git.alexzaw.dev/alexz/costco-watch:latest",
"isMain": true,
"internalPort": 8080,
"environment": [
{ "key": "PORT", "value": "8080" },
{ "key": "DATA_DIR", "value": "/data" },
{ "key": "TARGET_URL", "value": "${TARGET_URL}" },
{ "key": "ITEM_LABEL", "value": "${ITEM_LABEL:-Costco item}" },
{ "key": "NTFY_URL", "value": "${NTFY_URL:-https://ntfy.alexzaw.dev}" },
{ "key": "NTFY_TOPIC", "value": "${NTFY_TOPIC}" },
{ "key": "NTFY_TOKEN", "value": "${NTFY_TOKEN:-}" },
{ "key": "INTERVAL_MINUTES", "value": "${INTERVAL_MINUTES:-60}" },
{ "key": "API_KEY", "value": "${API_KEY}" }
],
"volumes": [
{
"hostPath": "${APP_DATA_DIR}/data",
"containerPath": "/data",
"readOnly": false
}
],
"healthCheck": {
"test": "curl -f http://127.0.0.1:8080/health || exit 1",
"interval": "60s",
"timeout": "5s",
"retries": 3,
"startPeriod": "10s"
}
}
]
}
-49
View File
@@ -1,49 +0,0 @@
# costco-watch
Polls a Costco tire product page on an interval and pushes an ntfy alert when
the item flips from out-of-stock to in-stock.
## The header gotcha
Costco's bot mitigation silently stalls requests that lack browser
fetch-metadata headers - TLS completes, then nothing comes back, no 403, just a
hang until timeout. Bisecting against the live site showed the gate is the
`Sec-Fetch-*` family: **any single one** of `Sec-Fetch-Site`, `Sec-Fetch-Mode`
or `Sec-Fetch-Dest` is enough to get a normal response, even with no
`User-Agent` at all. User-Agent, Accept and Accept-Language on their own make
no difference.
This service therefore sends a browser header set itself (see
`BROWSER_HEADERS` in `watch.js`). Two things that do **not** work as fixes:
- cloudflared cannot add headers - its `originRequest` block has no such
option, it only forwards what the client sent.
- Cloudflare Transform Rules cannot set `Sec-*` headers - they're on
Cloudflare's restricted list, so the edge can't supply the one header family
that matters.
`TARGET_URL` still points at the `costco-tire.alexzaw.dev` cloudflared ingress
(which sets `httpHostHeader: tires.costco.com`), but no Cloudflare-side
configuration is required.
## Config
| Env | Purpose |
|-----|---------|
| `TARGET_URL` | Product page to watch (required) |
| `ITEM_LABEL` | Friendly name used in notifications |
| `NTFY_URL` / `NTFY_TOPIC` | ntfy server and topic (topic required) |
| `NTFY_TOKEN` | Bearer token, if the topic is protected |
| `INTERVAL_MINUTES` | Poll interval, default 60 |
| `API_KEY` | Guards `/status` and `/check` |
## Endpoints
- `GET /health` - liveness
- `GET /status` - current stock state, last check, transition history
- `POST /check` - force a check now
## Etiquette
`tires.costco.com/robots.txt` allows `User-agent: *` on all paths, so a personal
low-frequency check is within its terms. Keep the interval at an hour or more.
-13
View File
@@ -1,13 +0,0 @@
# find-my-iphone image
Node/Express bridge exposing Alexa + REST endpoints over the find-my-iphone
module (icloudjs SRP auth). Dockerfile lives in the source repo -
https://git.alexzaw.dev/alexz/find-my-iphone (working copy:
~/projects/find-my-iphone). Build + push with ./build.sh.
Gotchas:
- APPLE_ID/APPLE_PASSWORD/API_KEY are required at boot or the container exits.
- The 2FA trust token is written to /data (mounted from ${APP_DATA_DIR}/data);
wiping that volume forces a new MFA round via POST /api/mfa.
- /alexa signature verification requires the raw request body - never put a
JSON body parser in front of it (server.js mounts express.json on /api only).
-7
View File
@@ -1,7 +0,0 @@
#!/bin/bash
set -euo pipefail
IMAGE_NAME="${IMAGE_NAME:-git.alexzaw.dev/alexz/find-my-iphone}"
IMAGE_TAG="${IMAGE_TAG:-latest}"
SRC="${SRC:-$HOME/projects/find-my-iphone}"
docker build "$@" -t "${IMAGE_NAME}:${IMAGE_TAG}" "$SRC"
docker push "${IMAGE_NAME}:${IMAGE_TAG}"
-81
View File
@@ -1,81 +0,0 @@
{
"$schema": "../app-info-schema.json",
"name": "Find My iPhone Bridge",
"id": "find-my-iphone",
"available": true,
"port": 8590,
"exposable": true,
"dynamic_config": true,
"no_gui": true,
"tipi_version": 1,
"version": "1.2.0",
"categories": [
"automation",
"utilities"
],
"description": "Self-hosted bridge that makes your iPhone ring via iCloud Find My. Exposes an Alexa custom-skill endpoint (/alexa, signature-verified) and API-key-protected REST endpoints (/api/alert, /api/devices, /api/location). Apple credentials stay on this server; 2FA trust token persists in the app volume.",
"short_desc": "Ring your iPhone from Alexa or curl",
"author": "alexz (fork of matt-kruse/find-my-iphone)",
"source": "https://git.alexzaw.dev/alexz/find-my-iphone",
"form_fields": [
{
"type": "text",
"label": "Apple ID",
"env_variable": "APPLE_ID",
"required": true,
"hint": "The iCloud account email that owns the devices",
"placeholder": "",
"default": ""
},
{
"type": "password",
"label": "Apple Password",
"env_variable": "APPLE_PASSWORD",
"required": true,
"hint": "Stored only in Runtipi's app env - never leaves this server",
"placeholder": "",
"default": ""
},
{
"type": "random",
"label": "API Key",
"env_variable": "API_KEY",
"required": true,
"min": 32,
"encoding": "hex",
"hint": "Auto-generated. Send as X-Api-Key header on /api/* requests. View in Settings tab.",
"placeholder": "",
"default": ""
},
{
"type": "text",
"label": "2FA Trust Token",
"env_variable": "APPLE_2FA_TOKEN",
"required": false,
"hint": "Optional. If you have a 2FA trust token, paste it",
"placeholder": "",
"default": ""
},
{
"type": "text",
"label": "Alexa Skill ID",
"env_variable": "ALEXA_SKILL_ID",
"required": false,
"hint": "amzn1.ask.skill.xxxx from the Alexa developer console. Locks /alexa to your skill. Leave blank until the skill exists, then fill in and save."
},
{
"type": "text",
"label": "Default Device Name",
"env_variable": "DEFAULT_DEVICE",
"required": false,
"hint": "Device to ring when none is specified (e.g. Alex's iPhone). Blank = first Find-My-capable device."
}
],
"supported_architectures": [
"amd64"
],
"created_at": 1754993000000,
"updated_at": 1754993000000,
"deprecated": false,
"min_tipi_version": "4.5.0"
}
-35
View File
@@ -1,35 +0,0 @@
{
"schemaVersion": 2,
"$schema": "https://schemas.runtipi.io/dynamic-compose.json",
"services": [
{
"name": "find-my-iphone",
"image": "git.alexzaw.dev/alexz/find-my-iphone:latest",
"isMain": true,
"internalPort": 8080,
"environment": [
{ "key": "PORT", "value": "8080" },
{ "key": "DATA_DIR", "value": "/data" },
{ "key": "APPLE_ID", "value": "${APPLE_ID}" },
{ "key": "APPLE_PASSWORD", "value": "${APPLE_PASSWORD}" },
{ "key": "API_KEY", "value": "${API_KEY}" },
{ "key": "ALEXA_SKILL_ID", "value": "${ALEXA_SKILL_ID:-}" },
{ "key": "DEFAULT_DEVICE", "value": "${DEFAULT_DEVICE:-}" }
],
"volumes": [
{
"hostPath": "${APP_DATA_DIR}/data",
"containerPath": "/data",
"readOnly": false
}
],
"healthCheck": {
"test": "curl -f http://127.0.0.1:8080/health || exit 1",
"interval": "30s",
"timeout": "5s",
"retries": 3,
"startPeriod": "10s"
}
}
]
}
@@ -1,23 +0,0 @@
# Find My iPhone Bridge
Ring your iPhone by voice ("Alexa, open phone finder") or curl, self-hosted.
## Endpoints
| Route | Auth | Purpose |
|-------|------|---------|
| `POST /alexa` | Alexa signature + skill ID | Custom skill endpoint |
| `GET /api/status` | X-Api-Key | Auth state: ready / mfa_required / error |
| `POST /api/mfa` | X-Api-Key | One-time 2FA bootstrap `{"code":"123456"}` |
| `GET /api/devices` | X-Api-Key | Device list with battery/status/location |
| `POST /api/alert` | X-Api-Key | Ring a device `{"device":"name"}` (optional) |
| `GET /api/location?device=` | X-Api-Key | Reverse-geocoded address |
## First run (one time)
1. Install with your Apple ID + password.
2. `GET /api/status` shows `mfa_required`; a code push arrives on your Apple devices.
3. `curl -X POST https://<domain>/api/mfa -H 'X-Api-Key: <key>' -H 'Content-Type: application/json' -d '{"code":"123456"}'`
4. Trust token persists in the app volume - no more codes needed.
Source: https://git.alexzaw.dev/alexz/find-my-iphone
Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.4 KiB

+1 -1
View File
@@ -2,7 +2,7 @@
The Dockerfile and full source for the **MergeDrop** image live in a separate Gitea repo: The Dockerfile and full source for the **MergeDrop** image live in a separate Gitea repo:
- Source: <https://git.alexzaw.dev/alexz/vid-merger> - Source: <https://git.alexzaw.dev/alexz/video-merger>
- Published image: `git.alexzaw.dev/alexz/mergedrop:<tag>` - Published image: `git.alexzaw.dev/alexz/mergedrop:<tag>`
This folder holds only a thin helper script (`build.sh`) that clones the source This folder holds only a thin helper script (`build.sh`) that clones the source
+2 -2
View File
@@ -6,8 +6,8 @@
set -euo pipefail set -euo pipefail
IMAGE_NAME="${IMAGE_NAME:-git.alexzaw.dev/alexz/mergedrop}" IMAGE_NAME="${IMAGE_NAME:-git.alexzaw.dev/alexz/mergedrop}"
IMAGE_TAG="${IMAGE_TAG:-1.0.2}" IMAGE_TAG="${IMAGE_TAG:-1.0.0}"
SOURCE_REPO="${SOURCE_REPO:-https://git.alexzaw.dev/alexz/vid-merger.git}" SOURCE_REPO="${SOURCE_REPO:-https://git.alexzaw.dev/alexz/video-merger.git}"
WORKDIR="${WORKDIR:-/tmp/mergedrop-build}" WORKDIR="${WORKDIR:-/tmp/mergedrop-build}"
echo "==> Preparing source at ${WORKDIR}" echo "==> Preparing source at ${WORKDIR}"
+18 -18
View File
@@ -1,20 +1,19 @@
{ {
"$schema": "https://schema.runtipi.io/app.json",
"id": "mergedrop",
"name": "MergeDrop", "name": "MergeDrop",
"id": "mergedrop",
"available": true, "available": true,
"exposable": true, "short_desc": "Drag-and-drop video merger — drop clips, reorder, build one MP4.",
"author": "alexz",
"port": 8765, "port": 8765,
"id_name": "mergedrop",
"description": "Self-hosted drag-and-drop video merger. Drop clips, reorder, and build a single MP4 with ffmpeg.",
"tipi_version": 5,
"version": "1.0.2",
"dynamic_config": true,
"supported_architectures": ["amd64"],
"categories": ["media", "utilities"], "categories": ["media", "utilities"],
"short_desc": "Drag, sort, merge — your videos into one MP4.", "description": "Self-hosted, drag-and-drop video merger. Upload multiple clips, reorder them, and produce a single normalized MP4 (h.264 + AAC) via ffmpeg. Handles mixed codecs, resolutions and framerates. Live progress streaming, in-browser preview, and downloadable result. No accounts, no telemetry, single container.",
"author": "MergeDrop", "tipi_version": 1,
"source": "https://git.alexzaw.dev/alexz/vid-merger", "version": "1.0.0",
"source": "https://git.alexzaw.dev/alexz/video-merger",
"website": "https://git.alexzaw.dev/alexz/video-merger",
"exposable": true,
"dynamic_config": true,
"no_gui": false,
"form_fields": [ "form_fields": [
{ {
"type": "text", "type": "text",
@@ -28,16 +27,17 @@
"label": "Max per-file size (bytes)", "label": "Max per-file size (bytes)",
"default": "2147483648", "default": "2147483648",
"required": false, "required": false,
"env_variable": "MERGEDROP_MAX_FILE_SIZE" "env_variable": "MERGEDROP_MAX_FILE_SIZE",
"hint": "Default is 2 GiB (2147483648). Raise for larger source clips."
}, },
{ {
"type": "number", "type": "text",
"label": "Output retention (hours)", "label": "Output retention (hours)",
"hint": "Merged output files older than this are cleaned up", "default": "24",
"default": 24,
"required": false, "required": false,
"env_variable": "MERGEDROP_OUTPUT_MAX_AGE_HOURS" "env_variable": "MERGEDROP_OUTPUT_MAX_AGE_HOURS",
"hint": "How long merged MP4s stay on disk before the janitor deletes them."
} }
], ],
"updated_at": 1787332264747 "supported_architectures": ["amd64"]
} }
+2 -25
View File
@@ -4,7 +4,7 @@
"services": [ "services": [
{ {
"name": "mergedrop", "name": "mergedrop",
"image": "git.alexzaw.dev/alexz/mergedrop:1.0.2", "image": "git.alexzaw.dev/alexz/mergedrop:1.0.0",
"isMain": true, "isMain": true,
"internalPort": 8000, "internalPort": 8000,
"environment": [ "environment": [
@@ -16,9 +16,7 @@
{ "key": "MERGEDROP_MAX_FILES", "value": "${MERGEDROP_MAX_FILES}" }, { "key": "MERGEDROP_MAX_FILES", "value": "${MERGEDROP_MAX_FILES}" },
{ "key": "MERGEDROP_MAX_FILE_SIZE", "value": "${MERGEDROP_MAX_FILE_SIZE}" }, { "key": "MERGEDROP_MAX_FILE_SIZE", "value": "${MERGEDROP_MAX_FILE_SIZE}" },
{ "key": "MERGEDROP_OUTPUT_MAX_AGE_HOURS", "value": "${MERGEDROP_OUTPUT_MAX_AGE_HOURS}" }, { "key": "MERGEDROP_OUTPUT_MAX_AGE_HOURS", "value": "${MERGEDROP_OUTPUT_MAX_AGE_HOURS}" },
{ "key": "CORS_ORIGINS", "value": "${APP_PROTOCOL}://${APP_DOMAIN}" }, { "key": "CORS_ORIGINS", "value": "*" }
{ "key": "MONGO_URL", "value": "mongodb://mergedrop-db:27017" },
{ "key": "DB_NAME", "value": "mergedrop" }
], ],
"volumes": [ "volumes": [
{ {
@@ -43,27 +41,6 @@
"timeout": "5s", "timeout": "5s",
"retries": 3, "retries": 3,
"startPeriod": "20s" "startPeriod": "20s"
},
"dependsOn": {
"mergedrop-db": { "condition": "service_healthy" }
}
},
{
"name": "mergedrop-db",
"image": "mongo:7.0",
"volumes": [
{
"hostPath": "${APP_DATA_DIR}/db",
"containerPath": "/data/db",
"readOnly": false
}
],
"healthCheck": {
"test": "mongosh --quiet --eval \"db.adminCommand('ping')\"",
"interval": "10s",
"timeout": "5s",
"retries": 5,
"startPeriod": "20s"
} }
} }
] ]
-69
View File
@@ -1,69 +0,0 @@
{
"$schema": "../app-info-schema.json",
"name": "RAGFlow",
"id": "ragflow",
"available": true,
"short_desc": "Open-source RAG engine with deep document understanding",
"author": "alexz",
"port": 9380,
"categories": ["ai"],
"tipi_version": 1,
"version": "0.26.4",
"source": "https://github.com/infiniflow/ragflow",
"website": "https://ragflow.io",
"exposable": true,
"dynamic_config": true,
"supported_architectures": ["amd64"],
"min_tipi_version": "4.0.0",
"form_fields": [
{
"type": "random",
"label": "MySQL Root Password",
"hint": "Root password for the bundled MySQL 8.0 database that stores RAGFlow's metadata",
"env_variable": "RAGFLOW_MYSQL_ROOT_PASSWORD",
"min": 24,
"encoding": "hex"
},
{
"type": "random",
"label": "Elasticsearch Password",
"hint": "Password for the built-in 'elastic' user on the Elasticsearch 8.11.3 document engine",
"env_variable": "RAGFLOW_ELASTIC_PASSWORD",
"min": 24,
"encoding": "hex"
},
{
"type": "random",
"label": "MinIO Root Password",
"hint": "Root password for the bundled MinIO object storage service (file/blob storage)",
"env_variable": "RAGFLOW_MINIO_ROOT_PASSWORD",
"min": 24,
"encoding": "hex"
},
{
"type": "random",
"label": "Redis Password",
"hint": "Password (requirepass) for the bundled Valkey/Redis cache and task queue",
"env_variable": "RAGFLOW_REDIS_PASSWORD",
"min": 24,
"encoding": "hex"
},
{
"type": "random",
"label": "Session/JWT Signing Secret",
"hint": "Signs RAGFlow's session cookies and API JWTs (RAGFLOW_SECRET_KEY). Without this, the image auto-generates and persists a secret in Redis on first boot, but a per-install random value here is stronger and survives a Redis data wipe.",
"env_variable": "RAGFLOW_SECRET_KEY",
"min": 32,
"encoding": "hex"
},
{
"type": "boolean",
"label": "Enable Self-Registration",
"hint": "Leave ON while this app is LAN-only so you can create the first admin account. Once the admin account exists, turn this OFF (re-save this form) BEFORE exposing the app on a public domain — see description for the full bootstrap sequence.",
"env_variable": "RAGFLOW_REGISTER_ENABLED",
"default": true
}
],
"created_at": 1784530800000,
"updated_at": 1784553280814
}
-196
View File
@@ -1,196 +0,0 @@
{
"schemaVersion": 2,
"$schema": "https://schemas.runtipi.io/v2/dynamic-compose.json",
"services": [
{
"name": "ragflow-es-init",
"image": "busybox:1.36",
"isMain": false,
"restart": "no",
"user": "root",
"command": ["chown", "-R", "1000:0", "/usr/share/elasticsearch/data"],
"volumes": [
{
"hostPath": "${APP_DATA_DIR}/data/es",
"containerPath": "/usr/share/elasticsearch/data",
"readOnly": false
}
]
},
{
"name": "ragflow-es01",
"image": "elasticsearch:8.11.3",
"environment": [
{ "key": "node.name", "value": "es01" },
{ "key": "ELASTIC_PASSWORD", "value": "${RAGFLOW_ELASTIC_PASSWORD}" },
{ "key": "bootstrap.memory_lock", "value": "false" },
{ "key": "discovery.type", "value": "single-node" },
{ "key": "xpack.security.enabled", "value": "true" },
{ "key": "xpack.security.http.ssl.enabled", "value": "false" },
{ "key": "xpack.security.transport.ssl.enabled", "value": "false" },
{ "key": "cluster.routing.allocation.disk.watermark.low", "value": "5gb" },
{ "key": "cluster.routing.allocation.disk.watermark.high", "value": "3gb" },
{ "key": "cluster.routing.allocation.disk.watermark.flood_stage", "value": "2gb" },
{ "key": "ES_JAVA_OPTS", "value": "-Xms2g -Xmx4g" }
],
"volumes": [
{
"hostPath": "${APP_DATA_DIR}/data/es",
"containerPath": "/usr/share/elasticsearch/data",
"readOnly": false
}
],
"ulimits": {
"memlock": { "soft": -1, "hard": -1 }
},
"deploy": {
"resources": {
"limits": {
"memory": "8000M"
}
}
},
"dependsOn": {
"ragflow-es-init": { "condition": "service_completed_successfully" }
},
"healthCheck": {
"test": "curl http://127.0.0.1:9200 || exit 1",
"interval": "10s",
"timeout": "10s",
"retries": 120,
"startPeriod": "120s"
}
},
{
"name": "ragflow-mysql",
"image": "mysql:8.0.39",
"environment": [
{ "key": "MYSQL_ROOT_PASSWORD", "value": "${RAGFLOW_MYSQL_ROOT_PASSWORD}" },
{ "key": "MYSQL_DATABASE", "value": "rag_flow" }
],
"command": [
"mysqld",
"--max_connections=1000",
"--character-set-server=utf8mb4",
"--collation-server=utf8mb4_unicode_ci",
"--default-authentication-plugin=mysql_native_password",
"--tls_version=TLSv1.2,TLSv1.3",
"--binlog_expire_logs_seconds=604800"
],
"volumes": [
{
"hostPath": "${APP_DATA_DIR}/data/mysql",
"containerPath": "/var/lib/mysql",
"readOnly": false
}
],
"healthCheck": {
"test": "mysqladmin ping -uroot -p${RAGFLOW_MYSQL_ROOT_PASSWORD}",
"interval": "10s",
"timeout": "10s",
"retries": 60,
"startPeriod": "30s"
}
},
{
"name": "ragflow-minio",
"image": "pgsty/minio:RELEASE.2026-03-25T00-00-00Z",
"command": ["server", "--console-address", ":9001", "/data"],
"environment": [
{ "key": "MINIO_ROOT_USER", "value": "rag_flow" },
{ "key": "MINIO_ROOT_PASSWORD", "value": "${RAGFLOW_MINIO_ROOT_PASSWORD}" }
],
"volumes": [
{
"hostPath": "${APP_DATA_DIR}/data/minio",
"containerPath": "/data",
"readOnly": false
}
],
"healthCheck": {
"test": "curl -f http://127.0.0.1:9000/minio/health/live || exit 1",
"interval": "10s",
"timeout": "10s",
"retries": 60,
"startPeriod": "30s"
}
},
{
"name": "ragflow-redis",
"image": "valkey/valkey:8.0.2",
"command": [
"redis-server",
"--requirepass", "${RAGFLOW_REDIS_PASSWORD}",
"--maxmemory", "128mb",
"--maxmemory-policy", "allkeys-lru"
],
"volumes": [
{
"hostPath": "${APP_DATA_DIR}/data/redis",
"containerPath": "/data",
"readOnly": false
}
],
"healthCheck": {
"test": "redis-cli -a ${RAGFLOW_REDIS_PASSWORD} ping",
"interval": "10s",
"timeout": "10s",
"retries": 30,
"startPeriod": "15s"
}
},
{
"name": "ragflow-server",
"image": "infiniflow/ragflow:v0.26.4",
"isMain": true,
"internalPort": 80,
"entrypoint": [
"/bin/bash",
"-c",
"if [ \"$RAGFLOW_REGISTER_ENABLED\" = \"false\" ]; then export REGISTER_ENABLED=0; else export REGISTER_ENABLED=1; fi; exec ./entrypoint.sh \"$@\"",
"bash"
],
"command": ["--enable-adminserver", "--init-model-provider-tables"],
"environment": [
{ "key": "TZ", "value": "${TZ}" },
{ "key": "DOC_ENGINE", "value": "elasticsearch" },
{ "key": "ES_HOST", "value": "ragflow-es01" },
{ "key": "ELASTIC_PASSWORD", "value": "${RAGFLOW_ELASTIC_PASSWORD}" },
{ "key": "MYSQL_HOST", "value": "ragflow-mysql" },
{ "key": "MYSQL_PORT", "value": "3306" },
{ "key": "MYSQL_DBNAME", "value": "rag_flow" },
{ "key": "MYSQL_PASSWORD", "value": "${RAGFLOW_MYSQL_ROOT_PASSWORD}" },
{ "key": "MINIO_HOST", "value": "ragflow-minio" },
{ "key": "MINIO_USER", "value": "rag_flow" },
{ "key": "MINIO_PASSWORD", "value": "${RAGFLOW_MINIO_ROOT_PASSWORD}" },
{ "key": "REDIS_HOST", "value": "ragflow-redis" },
{ "key": "REDIS_PASSWORD", "value": "${RAGFLOW_REDIS_PASSWORD}" },
{ "key": "RAGFLOW_SECRET_KEY", "value": "${RAGFLOW_SECRET_KEY}" },
{ "key": "RAGFLOW_REGISTER_ENABLED", "value": "${RAGFLOW_REGISTER_ENABLED}" },
{ "key": "API_PROXY_SCHEME", "value": "python" },
{ "key": "USE_DOCLING", "value": "false" },
{ "key": "DOTNET_SYSTEM_GLOBALIZATION_INVARIANT", "value": "1" }
],
"volumes": [
{
"hostPath": "${APP_DATA_DIR}/data/logs",
"containerPath": "/ragflow/logs",
"readOnly": false
}
],
"dependsOn": {
"ragflow-es01": { "condition": "service_healthy" },
"ragflow-mysql": { "condition": "service_healthy" },
"ragflow-minio": { "condition": "service_healthy" },
"ragflow-redis": { "condition": "service_healthy" }
},
"healthCheck": {
"test": "curl -f http://127.0.0.1:80/ || exit 1",
"interval": "30s",
"timeout": "10s",
"retries": 10,
"startPeriod": "180s"
}
}
]
}
-80
View File
@@ -1,80 +0,0 @@
# RAGFlow
RAGFlow is an open-source Retrieval-Augmented Generation (RAG) engine built on
deep document understanding. It combines robust document layout parsing (PDF,
Word, spreadsheets, slides, images, and more) with a configurable retrieval
pipeline so you can ground LLM answers in your own knowledge base with
traceable citations.
## What's included
This app deploys the full self-hosted RAGFlow stack in one shot:
- **RAGFlow server** (`infiniflow/ragflow:v0.26.4`) — the web UI, HTTP API,
admin server, and task executor. This is the version-pinned image RAGFlow
ships as the lightweight variant since v0.22 (no bundled embedding models).
- **Elasticsearch 8.11.3** — the document/vector engine used for full-text and
hybrid search over parsed document chunks. A one-shot `ragflow-es-init`
helper container runs before it to fix ownership of the data directory
(Elasticsearch's image runs as uid 1000, but Docker auto-creates bind-mount
directories as root; without this step Elasticsearch crash-loops on first
install and the whole stack never comes up).
- **MySQL 8.0** — relational metadata store (users, datasets, chat sessions,
configuration). The `rag_flow` database is created automatically via
`MYSQL_DATABASE` on first boot.
- **MinIO** — S3-compatible object storage for uploaded documents and
generated artifacts.
- **Valkey (Redis-compatible) 8.0.2** — cache and task queue for the ingestion
pipeline.
## Models
No LLM or embedding model is bundled or hard-coded into this deployment.
After installing, open **Settings → Model Providers** in the RAGFlow UI and
add an **Ollama** provider pointing at your existing Ollama server on this
instance:
- Base URL: `http://ollama-nvidia:11434`
- Fallback (if the container-name route doesn't resolve): `http://172.18.0.1:11434`
## Admin bootstrap and public-exposure sequence (read before exposing)
This app installs with **self-registration ON** by default so you can create
the first (admin) account. Follow this sequence exactly:
1. Install and wait for `ragflow-server` to report healthy.
2. Visit the app on the LAN (`http://<tipi-local-domain-or-ip>`), register the
first account — this becomes the admin account — and confirm you can sign
in and reach the dashboard.
3. Open this app's settings in the Runtipi dashboard and turn **Enable
Self-Registration** OFF, then save. This re-applies the app config with
`RAGFLOW_REGISTER_ENABLED=false`, which is translated internally to
RAGFlow's `REGISTER_ENABLED=0`.
4. Only after step 3 is confirmed, assign a public domain
(`ragflow.alexzaw.dev`) and enable Traefik exposure from the dashboard.
Do not skip step 3 before going public — leaving self-registration on for an
internet-facing instance lets anyone create an account.
## Session/JWT signing secret
A random 32-byte hex value is generated at install time and passed as
`RAGFLOW_SECRET_KEY`, which RAGFlow uses directly (`common/settings.py:
init_secret_key()`) to sign session cookies and JWTs, as long as it's at
least 32 characters — ours is 64 hex characters. If this field were ever
left empty, RAGFlow's own fallback still applies: it auto-generates a secret
and persists it in Redis (`ragflow:system:secret_key`), so sessions survive
container restarts either way. The explicit field is stronger because it
doesn't depend on Redis data surviving a wipe/reset.
## Residual public-exposure risk
RAGFlow has no built-in 2FA and its own auth surface has not been
independently audited by this deployment. Before assigning the public
domain, put this app behind Cloudflare Access or an authentik forward-auth
gate at the network layer, in addition to the registration lockdown above.
## Links
- Source: https://github.com/infiniflow/ragflow
- Docs: https://ragflow.io/docs
Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

-52
View File
@@ -1,52 +0,0 @@
# sap-drawio — Build Files
Image source repo (Dockerfile lives there, NOT in this store folder):
`~/projects/sap-architecture-diagrams` (git remote `git.alexzaw.dev/alexz/sap-architecture-diagrams`).
## What the image is
A single multi-stage image, `git.alexzaw.dev/alexz/sap-drawio:latest`:
1. **Builder stage** (`node:20-bookworm-slim`) — builds the React (CRA + CRACO,
Yarn Classic 1.22.22) frontend with `REACT_APP_BACKEND_URL=""` so all API
calls are same-origin (`/api`), working from any host/domain.
2. **Runtime stage** (`python:3.12-slim`) — installs `backend/requirements.txt`,
copies `backend/` to `/app/backend` and the built UI to `/app/frontend/build`
(FastAPI/uvicorn serves both from one process on port 8011), bakes the
12 MB SQLite catalog seed to `/seed/app.db`, and runs as root (homelab —
avoids the non-root volume-permission trap).
## How to build
```bash
cd /etc/runtipi/repos/runtipi/apps/sap-drawio/build
./build.sh
```
Builds and pushes `git.alexzaw.dev/alexz/sap-drawio:latest`. Override
`IMAGE_NAME` / `IMAGE_TAG` / `SRC` env vars if needed. To build without
pushing, run the `docker build` line directly from `$SRC`.
## Seed-on-first-run behavior
`docker/entrypoint.sh` (in the source repo) copies `/seed/app.db`
`/data/app.db` only if `/data/app.db` is absent, then execs uvicorn. This
means:
- First install: the persistent volume (`${APP_DATA_DIR}/data` on the host)
gets the baked catalog (icons/services) as its starting point.
- Every subsequent `app update`/restart: the existing `/data/app.db` (with any
saved conversions, added logos, etc.) is left untouched — the image rebuild
never overwrites live data.
## LLM dependency
Calls an Ollama-compatible host at `OLLAMA_URL` (form field, defaults to
`http://192.168.0.32:11434`, this homeserver's local `ollama-nvidia` app).
Models (`VISION_MODEL`, `VALIDATOR_MODEL`) are also form fields so they can be
changed without a rebuild. `qwen3-vl:8b` / `gemma4:31b` must exist on the
Ollama host for the defaults to work.
## Architecture note
**amd64 only.** No multi-arch build — matches the host architecture; not
needed for this homelab deployment.
-80
View File
@@ -1,80 +0,0 @@
#!/bin/bash
set -euo pipefail
# Builds (and, unless SKIP_PUSH=1, pushes) the sap-drawio image from the app
# source repo. Both the floating "latest" tag and the version-pinned tag
# (kept in sync with config.json "version") are built and pushed.
#
# Before building, this script (re)generates a deterministic, catalog-only
# seed DB (backend/data/seed.db) from the developer's live app.db. This is
# REQUIRED — the live app.db is WAL-mode with uncommitted -wal data and a
# full conversion/history table, neither of which may ever ship in the image
# (see sap-drawio Phase-1 code review FIX B1 / M2). The live app.db is never
# opened directly by this script; it is only ever `cp`-copied at the OS
# level, so it cannot be mutated by an incidental SQLite WAL checkpoint.
#
# Env overrides:
# IMAGE_NAME default git.alexzaw.dev/alexz/sap-drawio
# IMAGE_TAG default latest
# VERSION_TAG default 1.0.0 (must match config.json "version")
# SRC default ~/projects/sap-architecture-diagrams
# SKIP_PUSH set to 1 to build both tags locally only (no push) — used
# for pre-push smoke-testing.
IMAGE_NAME="${IMAGE_NAME:-git.alexzaw.dev/alexz/sap-drawio}"
IMAGE_TAG="${IMAGE_TAG:-latest}"
VERSION_TAG="${VERSION_TAG:-1.0.0}"
SRC="${SRC:-$HOME/projects/sap-architecture-diagrams}"
SKIP_PUSH="${SKIP_PUSH:-0}"
cd "$SRC"
echo "== [1/3] Generating deterministic catalog-only seed DB =="
SEED_WORK="$(mktemp -d)"
trap 'rm -rf "$SEED_WORK"' EXIT
# Copy the live dev DB (main + WAL + SHM, if present) at the OS level ONLY.
# backend/data/app.db is NEVER opened directly by this script.
cp backend/data/app.db "$SEED_WORK/app.db"
[ -f backend/data/app.db-wal ] && cp backend/data/app.db-wal "$SEED_WORK/app.db-wal"
[ -f backend/data/app.db-shm ] && cp backend/data/app.db-shm "$SEED_WORK/app.db-shm"
# .backup against the COPY (never the original) merges any committed WAL
# frames into a single consistent snapshot file.
sqlite3 "$SEED_WORK/app.db" ".backup '$SEED_WORK/seed.db'"
# Clear ONLY the conversion/history table (db.py: insert_conversion() writes
# to `conversions`). Every catalog/icon/service/brand/override row is kept.
sqlite3 "$SEED_WORK/seed.db" "DELETE FROM conversions;"
sqlite3 "$SEED_WORK/seed.db" "VACUUM;"
echo "-- Catalog row counts (source vs seed) --"
for t in icon_assets sap_services label_overrides schema_meta; do
src_count=$(sqlite3 "$SEED_WORK/app.db" "SELECT COUNT(*) FROM $t;")
seed_count=$(sqlite3 "$SEED_WORK/seed.db" "SELECT COUNT(*) FROM $t;")
printf ' %-16s source=%-8s seed=%-8s\n' "$t" "$src_count" "$seed_count"
if [ "$src_count" != "$seed_count" ]; then
echo "ERROR: catalog table $t row count mismatch (source=$src_count seed=$seed_count)" >&2
exit 1
fi
done
conv_count=$(sqlite3 "$SEED_WORK/seed.db" "SELECT COUNT(*) FROM conversions;")
echo " conversions seed=$conv_count (must be 0)"
if [ "$conv_count" != "0" ]; then
echo "ERROR: seed.db still has $conv_count conversion/history rows" >&2
exit 1
fi
cp "$SEED_WORK/seed.db" backend/data/seed.db
echo "Seed written to backend/data/seed.db ($(du -h backend/data/seed.db | cut -f1))"
echo "== [2/3] Building image (tags: ${IMAGE_TAG}, ${VERSION_TAG}) =="
sudo docker build "$@" -t "${IMAGE_NAME}:${IMAGE_TAG}" -t "${IMAGE_NAME}:${VERSION_TAG}" .
if [ "$SKIP_PUSH" = "1" ]; then
echo "== [3/3] SKIP_PUSH=1 — not pushing. Built locally: ${IMAGE_NAME}:${IMAGE_TAG}, ${IMAGE_NAME}:${VERSION_TAG} =="
exit 0
fi
echo "== [3/3] Pushing image =="
sudo docker push "${IMAGE_NAME}:${IMAGE_TAG}"
sudo docker push "${IMAGE_NAME}:${VERSION_TAG}"
-54
View File
@@ -1,54 +0,0 @@
{
"name": "Sketch to SAP BTP",
"id": "sap-drawio",
"available": true,
"short_desc": "Turn a hand-drawn or Mermaid sketch into a polished SAP BTP draw.io diagram.",
"author": "alexz",
"port": 8011,
"categories": [
"development",
"utilities"
],
"tipi_version": 1,
"version": "1.0.0",
"source": "https://git.alexzaw.dev/alexz/sap-architecture-diagrams",
"website": "https://git.alexzaw.dev/alexz/sap-architecture-diagrams",
"exposable": true,
"dynamic_config": true,
"supported_architectures": [
"amd64"
],
"min_tipi_version": "4.0.0",
"form_fields": [
{
"type": "text",
"label": "Ollama URL",
"hint": "Ollama-compatible LLM host",
"required": true,
"env_variable": "OLLAMA_URL",
"default": "http://172.17.0.1:11434"
},
{
"type": "text",
"label": "Vision model",
"required": true,
"env_variable": "VISION_MODEL",
"default": "qwen3-vl:8b"
},
{
"type": "text",
"label": "Validator model",
"required": true,
"env_variable": "VALIDATOR_MODEL",
"default": "qwen3-vl:8b"
},
{
"type": "password",
"label": "Brandfetch API key (optional)",
"required": false,
"env_variable": "BRANDFETCH_API_KEY"
}
],
"created_at": 1784733792011,
"updated_at": 1784733792011
}
-76
View File
@@ -1,76 +0,0 @@
{
"$schema": "https://schemas.runtipi.io/dynamic-compose.json",
"schemaVersion": 2,
"services": [
{
"name": "sap-drawio",
"image": "git.alexzaw.dev/alexz/sap-drawio:latest",
"isMain": true,
"internalPort": "${APP_PORT}",
"environment": [
{
"key": "TZ",
"value": "${TZ}"
},
{
"key": "APP_PORT",
"value": "${APP_PORT}"
},
{
"key": "OLLAMA_URL",
"value": "${OLLAMA_URL}"
},
{
"key": "VISION_MODEL",
"value": "${VISION_MODEL}"
},
{
"key": "VALIDATOR_MODEL",
"value": "${VALIDATOR_MODEL}"
},
{
"key": "BRANDFETCH_API_KEY",
"value": "${BRANDFETCH_API_KEY}"
},
{
"key": "ANTHROPIC_API_KEY",
"value": "${ANTHROPIC_API_KEY}"
},
{
"key": "FLAGSHIP_MODEL",
"value": "${FLAGSHIP_MODEL}"
},
{
"key": "RENDERER",
"value": "${RENDERER}"
},
{
"key": "REFERENCE_LAYOUT",
"value": "${REFERENCE_LAYOUT}"
},
{
"key": "SQLITE_DB_PATH",
"value": "/data/app.db"
},
{
"key": "CORS_ORIGINS",
"value": "https://sap-drawio.alexzaw.dev"
}
],
"volumes": [
{
"hostPath": "${APP_DATA_DIR}/data",
"containerPath": "/data",
"readOnly": false
}
],
"healthCheck": {
"test": "python -c \"import os,urllib.request,sys; p=os.environ['APP_PORT']; sys.exit(0 if urllib.request.urlopen(f'http://127.0.0.1:{p}/api/health', timeout=5).status==200 else 1)\"",
"interval": "30s",
"timeout": "10s",
"retries": 3,
"startPeriod": "40s"
}
}
]
}
-32
View File
@@ -1,32 +0,0 @@
# Sketch to SAP BTP
Turn a rough architecture idea into a polished **SAP BTP architecture diagram**
in seconds. Feed it either a hand-drawn/whiteboard **sketch (image)** or a
**Mermaid diagram**, and it produces a clean draw.io XML diagram (plus an
inline SVG preview) styled in SAP's official BTP solution-diagram visual
language — complete with an LLM-generated architecture review calling out
gaps, risks, and suggestions.
## Two ways in
- **Image sketch** — upload a photo or screenshot of a whiteboard sketch; the
vision model reads the shapes, labels, and connections and maps them to the
matching SAP BTP services/icons.
- **Mermaid** — paste a Mermaid diagram and get the same SAP BTP-styled
draw.io output without needing to draw anything by hand.
Every conversion is saved to a local history so past diagrams and their
review notes can be revisited later, exported again, or refined further.
## Access note
This app has **no built-in authentication** of its own — access control is
handled at the edge (Authentik SSO via the homeserver's forward-auth gate).
Anyone who reaches the app through the gate can drive the full pipeline.
## Dependencies
Requires a reachable Ollama-compatible LLM endpoint (configured via the
"Ollama URL" field) with the configured vision and validator models
available. An optional Brandfetch API key enables looking up third-party
brand logos for non-SAP components in a diagram.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB