feat: auto-reload tibi-server on api/ changes + skill docs for project registration

This commit is contained in:
2026-07-02 20:39:38 +00:00
parent 2d52272b2e
commit 3e8f6ec312
5 changed files with 150 additions and 50 deletions
+2 -3
View File
@@ -31,7 +31,6 @@ Do not mark a phase done only because code exists. A phase is done only when its
- `api/hooks/config-client.js`
- `frontend/.htaccess` when the deployment path uses the shipped Apache rewrite/proxy file
- `package.json`
- optional operator-owned root `config.yml` for the tibi-server instance when the current stack expects server-level config outside the project config
**Implementation checks:**
@@ -55,8 +54,8 @@ Do not mark a phase done only because code exists. A phase is done only when its
- [ ] Decide explicitly which bootstrap path applies:
- local starter Docker stack from `docker-compose-local.yml` and `Makefile`
- shared or operator-managed tibi-server with explicit server-level config/project registration
- [ ] For the local starter Docker path, confirm the repo is mounted into `/data` and the project serves through the repo-local `api/config.yml`; do not invent a separate root `config.yml` or `/api/v1/project` step.
- [ ] For the shared/external tibi-server path, create the root `config.yml`, register the project, and reload it.
- [ ] For the local starter Docker path, confirm the repo is mounted into `/data` and the project serves through the repo-local `api/config.yml`; treat a new project as initially unregistered and, if the running stack returns `project not found` for the namespace, complete a one-time `/api/v1/project` registration.
- [ ] For the shared/external tibi-server path, confirm the raw project API access and register/reload the project there.
- [ ] Confirm local/dev assumptions for Docker, reverse proxy, and any required basic-auth files only when the current environment actually uses them.
- [ ] If audit logging or transaction-sensitive features are planned, confirm the MongoDB/replica-set prerequisite for the target environment instead of assuming the local Docker setup is enough.
+47 -38
View File
@@ -113,8 +113,11 @@ EOF
Important:
- `ADMIN_TOKEN` is used for collection-level writes through the header name declared by the collection permission key; in this starter that is typically `Token` via `token:${ADMIN_TOKEN}`
- `ADMIN_TOKEN` generated in `api/config.yml.env` is the project-local token used for collection-level writes through the header name declared by the collection permission key; in this starter that is typically `Token` via `token:${ADMIN_TOKEN}`
- the current deploy scripts also use the same secret as a bearer token on the project-local reload endpoint
- do not assume this generated project-local `ADMIN_TOKEN` automatically has raw system-level `project` or `project.read` permissions
- if `GET /api/v1/project` with `X-Admin-Token: $ADMIN_TOKEN` returns `permission denied: admin token lacks permission project.read`, that token is not valid for project registration
- create new projects by logging in with `admin` / `admin` in the default starter dev setup and using the returned JWT via `X-Auth-Token`
- `ADMIN_ASSET_VERSION` is required so Nova picks up the current admin bundle
- `PROJECT_NAME`, `TIBI_NAMESPACE`, `PRODUCTION_PATH`, and `STAGING_PATH` should be project-specific before the first deploy
- `package.json` should no longer advertise the starter repository or default package name once the project is bootstrapped
@@ -144,7 +147,13 @@ Important characteristics:
- the project is mounted into `tibiserver` as `/data`
- `DB_DIAL`, `DB_PREFIX`, `MAIL_HOST`, and security overrides are injected via container environment
- the project is served from the repo's own `api/config.yml`
- no extra root `config.yml` or `/api/v1/project` registration step is required for basic local startup
- the repo files alone do not guarantee that the project is already registered in the running tibi-server instance
Important exception:
- if the local stack returns `project not found` for `/_/<namespace>/...` routes, treat that as a real registration gap instead of assuming the proxy is broken
- new projects should be assumed to be unregistered until the current tibi-server instance proves otherwise
- if needed, perform the one-time project registration before debugging unrelated frontend or SSR layers
Use this path unless the operator environment clearly tells you otherwise.
@@ -154,46 +163,22 @@ Only use this path when the project is not started through the local starter com
In that case, confirm all of these with the operator first:
- where the server-level `config.yml` lives
- which admin token is valid for raw system-level APIs
- which base URL exposes `/api/v1/project`
- how the project path is mounted into the shared tibi-server instance
Do not invent Path B steps in the local starter Docker stack just because upstream tibi-server docs mention them.
But do not ignore an explicit `project not found` server response either; that is the discriminating signal that the project may still need registration in the current runtime.
## Step 6 — Optional server-level config and project registration for Path B
## Step 6 — Treat new projects as unregistered first
Shared or external tibi-server setups may require a server-level `config.yml` outside the project config. That file defines database connection, JWT secret, and admin tokens used for project CRUD and reload.
Do not assume that a new project is already known to the running tibi-server instance just because the files exist on disk.
Create a root-level `config.yml` such as:
Use this mental model first:
```yaml
db:
dial: mongodb://mongo
prefix: tibi
api:
port: 8080
jwtSecret: <random-secret>
adminTokens:
- token: "<ADMIN_TOKEN>"
label: "admin"
permissions:
- project
- project.reload
- user
- namespace.<PROJECT_NAME>
- server.shutdown
mail:
host: localhost:25
security:
allowAbsolutePaths: false
allowUpperPaths: true
```
Then copy it into the tibi-server container and restart that container if the current environment requires this manual step.
- files on disk define the project config
- project registration makes that config available to the running tibi-server instance
- until registration exists, namespace routes can fail with `project not found`
## Step 7 — Verify website, admin, and API reachability
@@ -212,15 +197,32 @@ curl -I "$CODING_TIBISERVER_URL/api/v1/version"
```
If `/api/...` returns HTML instead of JSON, the reverse-proxy/setup path is still wrong.
If `/api/...` fails with `project not found`, the project runtime is up but the namespace is not registered in the current tibi-server instance yet.
## Step 8 — Optional project registration for Path B
## Step 8 — Project registration when the current runtime requires it
Projects are not assumed to exist just because files are present on disk. Register and reload them explicitly when the current stack requires project registration.
This step is mandatory for Path B and should be the first check for new projects whenever the running instance responds with `project not found` for the project namespace.
Token source for this step:
- do not blindly reuse the generated `api/config.yml.env` `ADMIN_TOKEN`
- first verify whether that token can read `GET /api/v1/project`
- in the default local starter setup, log in via `POST /api/v1/login` with `admin` / `admin` and use the returned JWT via `X-Auth-Token`
Default local dev flow:
```sh
jwt=$(curl -s -X POST "$CODING_TIBIADMIN_URL/api/login" \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"admin"}' | jq -r '.token')
```
```sh
curl -s -X POST "$CODING_TIBISERVER_URL/api/v1/project" \
-H "Content-Type: application/json" \
-H "X-Admin-Token: $ADMIN_TOKEN" \
-H "X-Auth-Token: $jwt" \
-d '{
"name": "<PROJECT_NAME>",
"description": "...",
@@ -229,18 +231,25 @@ curl -s -X POST "$CODING_TIBISERVER_URL/api/v1/project" \
}'
```
Expected effect:
- the project appears in `GET /api/v1/project`
- the project namespace begins to resolve on `/_/<namespace>/...`
- project-local `/api/...` proxy calls can start returning JSON instead of `project not found`
Reload after creation or config changes:
```sh
curl -s -X POST "$CODING_TIBISERVER_URL/api/v1/_/<PROJECT_NAME>/_/admin/reload" \
-H "X-Admin-Token: $ADMIN_TOKEN"
-H "X-Auth-Token: $jwt"
```
### Token header distinction
- raw system-level API such as project CRUD or direct admin reload: `X-Admin-Token`
- project registration in the default starter dev setup: log in with `admin` / `admin` and use `X-Auth-Token`
- raw system-level API such as project CRUD or direct admin reload can also use `X-Admin-Token` when such a server-level admin token exists in the current runtime
- collection-level CRUD such as content/navigation writes: use the header name from the collection permission key, typically `Token` in this starter via `token:${ADMIN_TOKEN}`
- JWT-authenticated user requests: `X-Auth-Token`
- JWT-authenticated user requests: `X-Auth-Token`; for project/user endpoints this is checked as fallback when no admin token is provided
The current starter deploy scripts are a separate case: they call the reverse-proxied reload endpoint on `LIVE_URL` or `STAGING_URL` with `Authorization: Bearer ${ADMIN_TOKEN}`.
+1 -1
View File
@@ -1,2 +1,2 @@
ADMIN_TOKEN=5bdfjc78hdxn338cuhSJ
ADMIN_ASSET_VERSION=8cbf0db-dirty-1779049064994
ADMIN_ASSET_VERSION=2d52272-dirty-1782309351197
+25 -1
View File
@@ -237,6 +237,19 @@ if (process.argv[2] == "start") {
}
}
// Pre-read config for tibi-server API reload (used by esbuild-wrapper.js)
let _tibiNamespace, _adminToken, _tibiReloadUrl
try {
const dotEnv = fs.readFileSync(__dirname + "/.env", "utf8")
_tibiNamespace = dotEnv.match(/TIBI_NAMESPACE=(.*)/)?.[1]
const adminEnv = fs.readFileSync(__dirname + "/api/config.yml.env", "utf8")
_adminToken = adminEnv.match(/ADMIN_TOKEN=(.*)/)?.[1]
// Inside Docker compose, the tibiserver container is reachable by hostname
_tibiReloadUrl = `http://tibiserver:8080/api/v1/_/${_tibiNamespace}/_/admin/reload`
} catch (_) {
// Not critical — fallback: no reload capability
}
module.exports = {
sveltePlugin: sveltePlugin,
resolvePlugin: resolvePlugin,
@@ -244,8 +257,19 @@ module.exports = {
options: options,
distDir,
watch: {
path: [__dirname + "/" + frontendDir + "/src"],
path: [
__dirname + "/" + frontendDir + "/src",
__dirname + "/api",
],
// buildInfo.ts is written by writeBuildInfo() on every rebuild — ignoring
// it prevents a rebuild loop. buildInfo.js and config.yml.env (ADMIN_ASSET_VERSION)
// are also written, but those changes are debounced into a single reload.
ignored: [
/\/buildInfo\.ts$/,
],
},
tibiReloadUrl: _tibiReloadUrl,
adminToken: _adminToken,
serve: {
onRequest(args) {
console.log(args)
+75 -7
View File
@@ -1,5 +1,6 @@
const esbuild = require("esbuild")
const fs = require("fs")
const http = require("http")
const path = require("path")
const config = require(process.cwd() + (process.argv?.length > 3 ? "/" + process.argv[3] : "/esbuild.config.js"))
@@ -71,15 +72,82 @@ switch (process.argv?.length > 2 ? process.argv[2] : "build") {
bs.reload()
}
})
const watcher = watch(config.watch.path)
log("watching files...")
watcher.on("change", function (path) {
log(`${path} changed`, true)
build(true).then(() => {
if (bs) {
bs.reload()
// Trigger tibi-server project reload for api/ changes
const apiDir = path.resolve("api") + "/"
let reloadInFlight = false
let reloadQueued = false
function reloadTibi() {
if (!config.tibiReloadUrl || !config.adminToken) {
log("tibi-server reload: missing URL or token (running in mock mode?)")
return
}
if (reloadInFlight) {
reloadQueued = true
log("tibi-server reload: already in flight, queuing another...")
return
}
reloadInFlight = true
const parts = new URL(config.tibiReloadUrl)
const opts = {
hostname: parts.hostname,
port: parts.port,
path: parts.pathname,
method: "POST",
headers: { Authorization: "Bearer " + config.adminToken },
timeout: 5000,
}
let body = ""
const finishReload = () => {
reloadInFlight = false
if (reloadQueued) {
reloadQueued = false
log("tibi-server reload: executing queued reload...")
reloadTibi()
}
}
const req = http.request(opts, (res) => {
res.on("data", (chunk) => (body += chunk))
res.on("end", () => {
if (res.statusCode === 200) {
log("tibi-server reloaded ✓")
} else {
console.error(
"\x1b[33mtibi-server reload: HTTP " + res.statusCode + " — " + body.trim() + "\x1b[0m"
)
}
finishReload()
})
})
req.on("error", (e) => {
console.error("\x1b[33mtibi-server reload failed: " + e.message + "\x1b[0m")
finishReload()
})
req.end()
}
const { path: watchPaths, ignored: watchIgnored, ...watchRest } = config.watch
const watcher = watch(watchPaths, { ignored: watchIgnored, ...watchRest })
log("watching files...")
const DEBOUNCE_MS = 200
let frontendTimer, apiTimer
watcher.on("change", function (changedPath) {
log(`${changedPath} changed`, true)
if (changedPath.startsWith(apiDir)) {
clearTimeout(apiTimer)
apiTimer = setTimeout(() => {
log("api/ change detected → reloading tibi-server...")
reloadTibi()
}, DEBOUNCE_MS)
} else {
clearTimeout(frontendTimer)
frontendTimer = setTimeout(() => {
build(true).then(() => {
if (bs) {
bs.reload()
}
})
}, DEBOUNCE_MS)
}
})
break
default: