feat(updates): add API endpoints for update management, backup, chang…#110
Conversation
…elog, system info, and version check - Implemented POST /api/updates/apply to trigger self-hosted updates via Docker Compose. - Created POST /api/updates/backup for PostgreSQL database backups before updates. - Added GET /api/updates/changelog to parse and return structured changelog entries from CHANGELOG.md. - Developed GET /api/updates/system to provide system information for diagnostics. - Introduced GET /api/updates/version to compare the running version against the latest GitHub release.
|
🚅 Deployed to the reqcore-pr-110 environment in applirank
|
|
Caution Review failedPull request was closed or merged during review 📝 WalkthroughWalkthroughAdds a self-hosting and updates subsystem: documentation (SELF-HOSTING.md), UI entry and dashboard for updates, five server APIs (version, system, changelog, backup, apply), Dockerfile/docker-compose adjustments, and cleanup of duplicate CHANGELOG.md entries. Changes
Sequence Diagram(s)sequenceDiagram
participant Client
participant Dashboard as Updates Dashboard
participant VersionAPI as /api/updates/version
participant BackupAPI as /api/updates/backup
participant ApplyAPI as /api/updates/apply
participant GitHub as GitHub API
participant DB as PostgreSQL
participant Docker as Docker Engine
Client->>Dashboard: Open Updates Page
activate Dashboard
Dashboard->>VersionAPI: GET /api/updates/version
activate VersionAPI
VersionAPI->>GitHub: Query latest release
GitHub-->>VersionAPI: Release info
VersionAPI-->>Dashboard: currentVersion, latestVersion, updateAvailable
deactivate VersionAPI
alt Update Available
Client->>Dashboard: Click "Install Update"
Dashboard->>BackupAPI: POST /api/updates/backup
activate BackupAPI
BackupAPI->>DB: pg_dump
DB-->>BackupAPI: SQL dump file
BackupAPI-->>Dashboard: { success, filename }
deactivate BackupAPI
Dashboard->>ApplyAPI: POST /api/updates/apply
activate ApplyAPI
ApplyAPI->>Docker: git pull origin main
Docker-->>ApplyAPI: git pull output
ApplyAPI->>Docker: docker compose up --build --detach
Docker-->>ApplyAPI: rebuild & restart output
ApplyAPI-->>Dashboard: { success, steps }
deactivate ApplyAPI
end
deactivate Dashboard
sequenceDiagram
participant Client
participant Dashboard as Updates Dashboard
participant SystemAPI as /api/updates/system
participant ChangelogAPI as /api/updates/changelog
participant DB as PostgreSQL
participant MinIO as MinIO/S3
participant FS as Filesystem
Client->>Dashboard: Load Updates Page
activate Dashboard
par Health Check & Changelog
Dashboard->>SystemAPI: GET /api/updates/system
activate SystemAPI
SystemAPI->>DB: SELECT 1
SystemAPI->>MinIO: HeadBucket
SystemAPI->>FS: Check /.dockerenv
SystemAPI-->>Dashboard: diagnostics object
deactivate SystemAPI
and
Dashboard->>ChangelogAPI: GET /api/updates/changelog
activate ChangelogAPI
ChangelogAPI->>FS: Read CHANGELOG.md & package.json
ChangelogAPI-->>Dashboard: entries[], currentVersion
deactivate ChangelogAPI
end
Dashboard->>Dashboard: Render health and changelog
deactivate Dashboard
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Poem
🚥 Pre-merge checks | ✅ 1 | ❌ 2❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (1 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
📝 Coding Plan
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 11
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@app/components/AppTopBar.vue`:
- Around line 199-209: The Updates icon link is hidden on mobile and lacks an
accessible name; update the NuxtLink (the Updates button using ArrowUpCircle and
isActiveRoute) to include an explicit aria-label (e.g., aria-label="Updates and
changelog") and make it visible on small screens (remove or adjust the "hidden
sm:flex" class) so it appears in the mobile toolbar, and also add "updates" (or
the same route $localePath('/dashboard/updates')) to the mobile menu data where
mainNav and "New Job" are rendered so the Updates page is reachable from mobile.
In `@app/pages/dashboard/updates.vue`:
- Around line 243-254: The UI currently shows "Up to date" when versionInfo is
null; change the header and the fallback template to only claim up-to-date when
versionInfo exists and versionInfo.updateAvailable is false. Update the h2
conditional and the last <template> so they check versionInfo (e.g., use
versionLoading ? "Checking…" : versionInfo ? (versionInfo.updateAvailable ?
"Update available" : "Up to date") : "Update status unavailable"), and change
the paragraph fallback to show a neutral message when versionInfo is missing
(e.g., "Update status unavailable" or similar) instead of "You're running the
latest version"; keep use of versionInfo.latestVersion and
versionInfo.currentVersion only when versionInfo is present.
- Around line 592-598: The template currently treats failed fetches and
genuinely empty changelogs the same by only checking entries.length; add a
reactive error flag (e.g., changelogError) alongside entries, set changelogError
= true inside the changelog fetch function (the method that populates entries —
e.g., fetchChangelog / loadEntries) when the network/API call fails and clear it
on successful fetch, then change the template logic so the empty-state block is
shown only when entries.length === 0 && !changelogError and add a new error
block (or modify the existing block) to display a distinct error message when
changelogError === true. Ensure the fetch function records the actual error (for
logging) and toggles the flag appropriately.
- Around line 467-472: The panel currently handles loading
(v-if="systemLoading") and success (v-else-if="systemInfo") but has no fallback,
so when loading finishes with no data the panel is blank; update the template in
updates.vue to add an explicit v-else block after the existing v-if/v-else-if
pair (i.e., after the elements referencing systemLoading and systemInfo) that
renders a clear failure state (e.g., a message like "System information
unavailable" and optional retry action) so users see a meaningful error UI when
systemInfo is falsy.
In `@SELF-HOSTING.md`:
- Around line 191-193: The fenced code blocks in SELF-HOSTING.md (example blocks
around the snippets at lines 191-193, 318-322, and 625-640) lack language
identifiers and are flagged by markdownlint; update each opening fence from ```
to a labeled fence such as ```text or ```plaintext so the linter recognizes them
(e.g., change the three unlabeled fenced blocks to use ```text).
In `@server/api/updates/apply.post.ts`:
- Around line 96-124: The rebuild currently runs execFileAsync('docker',
['compose', 'up', ...]) inside the container serving the request (the "Rebuild &
restart" step), which will tear down the caller and make the HTTP response
unreliable; change this to hand off the work to an external updater: either
spawn a detached background process (use child_process.spawn with detached: true
and unref()) to run a small updater script that performs the docker compose
rebuild, or write an update request to a file/queue that a sidecar/updater
service polls; update the code paths around the execFileAsync call and the
"Rebuild & restart" step so the handler returns a success status immediately
after scheduling the external update and does not block on or directly invoke
the in-container docker compose command.
- Around line 75-76: The code currently runs execFileAsync('git', ['pull',
'origin', 'main']) which always updates main; instead fetch and check out the
release tag that was validated by version.get. Replace the pull of origin/main
with steps that use the verified release tag (e.g., a releaseTag variable coming
from the request or version check): run git fetch --tags (or git fetch origin)
and then git checkout -f "refs/tags/<releaseTag>" (or git reset --hard
"<releaseTag>") via execFileAsync to ensure the repo is moved to the exact
release commit rather than the main branch; update the execFileAsync
invocation(s) in apply.post to reference that releaseTag.
- Around line 58-103: The updater currently assumes the container is a repo
checkout and can run git/docker commands; add a pre-check before running
execFileAsync('git'...) and the docker compose call to verify the runtime has a
repository and compose file: check that '/app/.git' exists and that either
'/app/docker-compose.yml' or '/app/docker-compose.yaml' (or 'docker-compose.yml'
variants) exist (use fs.stat or fs.access), and if those checks fail push a
clear failure UpdateResult (using the existing steps array and UpdateResult
shape) explaining the container is not a repo/compose layout and instructing
manual update; update the error message returned where execFileAsync('git'...)
or the docker compose block would run to short-circuit earlier when these files
are missing and avoid attempting git pull/docker compose in apply.post.ts
(referencing execFileAsync, steps, and UpdateResult).
In `@server/api/updates/backup.post.ts`:
- Around line 28-34: The current logic only calls mkdir(backupDir, { recursive:
true }) and assumes writability; instead, after ensuring the directory exists
(via mkdir or otherwise) probe actual write access before assigning effectiveDir
(for example using fs.access with fs.constants.W_OK or attempting to
create+remove a temp file) so that root-owned or read-only mounts are detected
and you fall back to '/tmp'. Apply the same writable-probe fix to the other
similar block (lines 48-55) that sets effectiveDir so both code paths verify
real write permission before invoking pg_dump or other writers.
- Around line 48-55: The handler in server/api/updates/backup.post.ts currently
assumes the pg_dump binary is present when calling execFileAsync('pg_dump',
...); modify the handler to first check for pg_dump availability (e.g., attempt
a lightweight execFileAsync('which' or 'pg_dump --version') or fs.access) and if
absent provide a safe fallback: either (A) return a clear 5xx error explaining
pg_dump is not installed, or (B) implement a Node-side dump using the 'pg'
client (connect via the same host/port/user/database and stream a logical dump
by enumerating tables and using COPY TO STDOUT or SELECTs to write schema+data
into backupPath). Update the code path around execFileAsync to use this
check/fallback so the endpoint does not assume pg_dump exists.
In `@server/api/updates/version.get.ts`:
- Around line 23-31: The fetch to GitHub in version.get.ts (the call that
assigns to response using owner, repo and currentVersion) lacks a timeout; wrap
the request with an AbortController: create controller = new AbortController(),
start a timer (e.g. 5000ms) that calls controller.abort(), pass
controller.signal to fetch, and clear the timer after fetch resolves; keep the
existing try/catch so the abort will fall into the current error handling and
return the default response.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 91f7c3bc-6d36-44c2-bbb4-be6645c6d58f
📒 Files selected for processing (10)
CHANGELOG.mdSELF-HOSTING.mdapp/components/AppTopBar.vueapp/pages/dashboard/updates.vuedocker-compose.ymlserver/api/updates/apply.post.tsserver/api/updates/backup.post.tsserver/api/updates/changelog.get.tsserver/api/updates/system.get.tsserver/api/updates/version.get.ts
💤 Files with no reviewable changes (1)
- CHANGELOG.md
| <!-- Updates button --> | ||
| <NuxtLink | ||
| :to="$localePath('/dashboard/updates')" | ||
| class="hidden sm:flex items-center justify-center size-8 rounded-lg transition-all duration-200 no-underline" | ||
| :class="isActiveRoute('/dashboard/updates', false) | ||
| ? 'text-brand-600 dark:text-brand-400 bg-brand-50 dark:bg-brand-950/40' | ||
| : 'text-surface-500 dark:text-surface-400 hover:text-surface-700 dark:hover:text-surface-200 hover:bg-surface-100 dark:hover:bg-surface-800'" | ||
| title="Updates & changelog" | ||
| > | ||
| <ArrowUpCircle class="size-4" /> | ||
| </NuxtLink> |
There was a problem hiding this comment.
Expose the Updates route on mobile and give this icon link an accessible name.
This control disappears below sm, and the mobile menus in this file only render mainNav plus “New Job”, so the new page has no visible mobile entry. It is also icon-only with just a title, which does not provide a reliable accessible name—please add a mobile nav item and an explicit aria-label.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@app/components/AppTopBar.vue` around lines 199 - 209, The Updates icon link
is hidden on mobile and lacks an accessible name; update the NuxtLink (the
Updates button using ArrowUpCircle and isActiveRoute) to include an explicit
aria-label (e.g., aria-label="Updates and changelog") and make it visible on
small screens (remove or adjust the "hidden sm:flex" class) so it appears in the
mobile toolbar, and also add "updates" (or the same route
$localePath('/dashboard/updates')) to the mobile menu data where mainNav and
"New Job" are rendered so the Updates page is reachable from mobile.
| v-else-if="entries.length === 0" | ||
| class="rounded-xl border border-surface-200 dark:border-surface-700 bg-white dark:bg-surface-900 p-12 text-center" | ||
| > | ||
| <RefreshCw class="size-8 text-surface-300 dark:text-surface-600 mx-auto mb-3" /> | ||
| <div class="text-sm font-medium text-surface-500 dark:text-surface-400">No changelog entries found</div> | ||
| <div class="text-xs text-surface-400 dark:text-surface-500 mt-1">The CHANGELOG.md file may be missing or empty.</div> | ||
| </div> |
There was a problem hiding this comment.
Differentiate changelog fetch errors from true empty data.
Line 592 treats failed loads and genuinely empty changelogs the same, which can hide operational/API issues.
Proposed fix
- <div
- v-else-if="entries.length === 0"
+ <div
+ v-else-if="status === 'error'"
+ class="rounded-xl border border-danger-200 dark:border-danger-900 bg-danger-50/40 dark:bg-danger-950/20 p-12 text-center"
+ >
+ <AlertTriangle class="size-8 text-danger-500 mx-auto mb-3" />
+ <div class="text-sm font-medium text-danger-600 dark:text-danger-400">Failed to load changelog</div>
+ <div class="text-xs text-danger-500/80 mt-1">Please retry in a moment.</div>
+ </div>
+ <div
+ v-else-if="entries.length === 0"
class="rounded-xl border border-surface-200 dark:border-surface-700 bg-white dark:bg-surface-900 p-12 text-center"
>🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@app/pages/dashboard/updates.vue` around lines 592 - 598, The template
currently treats failed fetches and genuinely empty changelogs the same by only
checking entries.length; add a reactive error flag (e.g., changelogError)
alongside entries, set changelogError = true inside the changelog fetch function
(the method that populates entries — e.g., fetchChangelog / loadEntries) when
the network/API call fails and clear it on successful fetch, then change the
template logic so the empty-state block is shown only when entries.length === 0
&& !changelogError and add a new error block (or modify the existing block) to
display a distinct error message when changelogError === true. Ensure the fetch
function records the actual error (for logging) and toggles the flag
appropriately.
| ``` | ||
| http://localhost:3000 | ||
| ``` |
There was a problem hiding this comment.
Add language identifiers to these fenced blocks.
markdownlint is already flagging these fences, so docs lint will keep failing until they are labeled. text/plaintext is enough for all three examples here.
Also applies to: 318-322, 625-640
🧰 Tools
🪛 markdownlint-cli2 (0.21.0)
[warning] 191-191: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@SELF-HOSTING.md` around lines 191 - 193, The fenced code blocks in
SELF-HOSTING.md (example blocks around the snippets at lines 191-193, 318-322,
and 625-640) lack language identifiers and are flagged by markdownlint; update
each opening fence from ``` to a labeled fence such as ```text or ```plaintext
so the linter recognizes them (e.g., change the three unlabeled fenced blocks to
use ```text).
| const { stdout } = await execFileAsync('git', ['pull', 'origin', 'main'], { | ||
| cwd: '/app', |
There was a problem hiding this comment.
Update the checked release, not the main branch head.
server/api/updates/version.get.ts compares against GitHub's latest release, but this step always pulls origin/main. Those can diverge, so the UI can advertise vX.Y.Z and then deploy different or unreleased code.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@server/api/updates/apply.post.ts` around lines 75 - 76, The code currently
runs execFileAsync('git', ['pull', 'origin', 'main']) which always updates main;
instead fetch and check out the release tag that was validated by version.get.
Replace the pull of origin/main with steps that use the verified release tag
(e.g., a releaseTag variable coming from the request or version check): run git
fetch --tags (or git fetch origin) and then git checkout -f
"refs/tags/<releaseTag>" (or git reset --hard "<releaseTag>") via execFileAsync
to ensure the repo is moved to the exact release commit rather than the main
branch; update the execFileAsync invocation(s) in apply.post to reference that
releaseTag.
| // Step 2: Rebuild and restart via Docker Compose | ||
| try { | ||
| const { stdout } = await execFileAsync( | ||
| 'docker', ['compose', 'up', '--build', '--detach', '--no-deps', 'app'], | ||
| { | ||
| cwd: '/app', | ||
| timeout: 600_000, // 10 minutes for build | ||
| }, | ||
| ) | ||
| steps.push({ | ||
| step: 'Rebuild & restart', | ||
| status: 'success', | ||
| detail: stdout.trim(), | ||
| }) | ||
| } | ||
| catch (err: unknown) { | ||
| const message = err instanceof Error ? err.message : 'Unknown error' | ||
| steps.push({ step: 'Rebuild & restart', status: 'failed', detail: message }) | ||
| return { | ||
| success: false, | ||
| message: 'Failed to rebuild. Your current version is still running safely. Try running the update manually.', | ||
| previousVersion, | ||
| steps, | ||
| } satisfies UpdateResult | ||
| } | ||
|
|
||
| return { | ||
| success: true, | ||
| message: 'Update started successfully. The application will restart momentarily. Refresh this page in about 30 seconds.', |
There was a problem hiding this comment.
Recreating app from inside app makes the response unreliable.
If this step succeeds, Docker tears down the very container serving the request, so the browser is likely to see a dropped connection instead of the success payload. This needs an external updater process/sidecar or an async handoff before the current container is replaced.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@server/api/updates/apply.post.ts` around lines 96 - 124, The rebuild
currently runs execFileAsync('docker', ['compose', 'up', ...]) inside the
container serving the request (the "Rebuild & restart" step), which will tear
down the caller and make the HTTP response unreliable; change this to hand off
the work to an external updater: either spawn a detached background process (use
child_process.spawn with detached: true and unref()) to run a small updater
script that performs the docker compose rebuild, or write an update request to a
file/queue that a sidecar/updater service polls; update the code paths around
the execFileAsync call and the "Rebuild & restart" step so the handler returns a
success status immediately after scheduling the external update and does not
block on or directly invoke the in-container docker compose command.
| await execFileAsync( | ||
| 'pg_dump', | ||
| ['-h', host, '-p', port, '-U', user, '-d', database, '--no-owner', '--no-acl', '-f', backupPath], | ||
| { | ||
| timeout: 300_000, | ||
| env: { ...process.env, PGPASSWORD: dbUrl.password }, | ||
| }, | ||
| ) |
There was a problem hiding this comment.
pg_dump is not available in the shipped app image.
This handler shells out to pg_dump, but the provided runtime image only copies Node artifacts and does not install PostgreSQL client tools. In the current container this endpoint will fail before any backup is created.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@server/api/updates/backup.post.ts` around lines 48 - 55, The handler in
server/api/updates/backup.post.ts currently assumes the pg_dump binary is
present when calling execFileAsync('pg_dump', ...); modify the handler to first
check for pg_dump availability (e.g., attempt a lightweight
execFileAsync('which' or 'pg_dump --version') or fs.access) and if absent
provide a safe fallback: either (A) return a clear 5xx error explaining pg_dump
is not installed, or (B) implement a Node-side dump using the 'pg' client
(connect via the same host/port/user/database and stream a logical dump by
enumerating tables and using COPY TO STDOUT or SELECTs to write schema+data into
backupPath). Update the code path around execFileAsync to use this
check/fallback so the endpoint does not assume pg_dump exists.
… with error handling
…elog, system info, and version check
Summary
Type of change
Validation
DCO
Signed-off-by) viagit commit -sSummary by CodeRabbit
New Features
Documentation
Chores