✨ feat: auto-reload tibi-server on api/ changes + skill docs for project registration
This commit is contained in:
@@ -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.
|
||||
|
||||
|
||||
@@ -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
@@ -1,2 +1,2 @@
|
||||
ADMIN_TOKEN=5bdfjc78hdxn338cuhSJ
|
||||
ADMIN_ASSET_VERSION=8cbf0db-dirty-1779049064994
|
||||
ADMIN_ASSET_VERSION=2d52272-dirty-1782309351197
|
||||
|
||||
+25
-1
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user