feat: Add API key edit, protected mode UI, file manager improvements, system views#63
Conversation
The dev server proxied any path starting with the literal string "api" to the agent. The path "/api-keys" matched that prefix and was being sent to a backend that has no such endpoint, so refreshing the API keys page returned a 404 instead of letting the SPA router handle the route. The proxy now matches only paths starting with "/api/", so the front end's own routes that share the "api" prefix fall through to the SPA fallback as expected.
… system views
API keys can now be edited from the UI in a tabbed dialog that mirrors
the user dialog: profile, permissions, and a deployment access tab
that grants per-deployment read, write, or admin level. The tabbed
modal shell and the deployment access field are extracted into shared
components so the user dialog and the API key dialog use exactly the
same primitives. The blanket 401 interceptor that logged people out
on any per-resource auth error is narrowed to only redirect on session
endpoints, so a refused create or update surfaces as a toast instead
of a forced sign-out.
The deployment protected-mode panel and the global system-terminal
protection panel share a single explainer helper, so each blocked
command rule renders a human-readable description ("blocks any
command containing rm -rf") and the add form previews the rule as
it is typed. Tooltips on the blocked-action chips state what each
action covers.
The file browser becomes context-agnostic: it accepts an injected
API adapter and renders the same UI for deployment files and for a
new system-wide file manager. Files and folders can be created in
place, permissions edited through a per-bit chmod dialog, and row
actions are collapsed into an overflow menu to reduce accidental
clicks. Hidden and system folders are hidden by default with toggles
to reveal them, and the manager opens in the user's home directory
while still allowing navigation up to the configured root.
New routes mount the system-wide file manager and the system terminal
in the dashboard, both gated by the corresponding new permissions
threaded through the role defaults.
Closes #117
Closes #122
Closes #123
Code Review SummaryThis PR introduces several significant features including API key editing, a system-wide file manager, and 'Protected Mode' for deployments and system terminals. The codebase shows good progress in component reusability. 🚀 Key Improvements
💡 Minor Suggestions
|
| localStorage.removeItem("auth_token"); | ||
| window.location.href = "/login"; |
There was a problem hiding this comment.
The current regex check for session endpoints is slightly brittle. It's safer to check for common auth/user path prefixes more explicitly or use a list of protected routes.
| localStorage.removeItem("auth_token"); | |
| window.location.href = "/login"; | |
| if (error.response?.status === 401 && !window.location.pathname.includes("/setup")) { | |
| const failedURL = error.config?.url || ""; | |
| const sessionEndpoints = ["/auth/", "/users/me"]; | |
| const isSessionEndpoint = sessionEndpoints.some(endpoint => failedURL.includes(endpoint)); | |
| if (isSessionEndpoint) { | |
| localStorage.removeItem("auth_token"); | |
| window.location.href = "/login"; | |
| } | |
| } |
| showDeleteModal.value = true; | ||
| }; | ||
|
|
||
| const parsePermissionsMode = (permissions: string | undefined, isDir: boolean): number => { |
There was a problem hiding this comment.
The permission parsing logic can be simplified. Instead of checking every index of the symbolic string, a loop can iterate through the string and apply bit shifts.
| const parsePermissionsMode = (permissions: string | undefined, isDir: boolean): number => { | |
| const parsePermissionsMode = (permissions: string | undefined, isDir: boolean): number => { | |
| if (!permissions) return isDir ? 0o755 : 0o644; | |
| const symbolic = permissions.length === 10 ? permissions.slice(1) : permissions; | |
| if (symbolic.length === 9) { | |
| let n = 0; | |
| const chars = "rwxrwxrwx"; | |
| for (let i = 0; i < 9; i++) { | |
| if (symbolic[i] === chars[i]) n |= (1 << (8 - i)); | |
| } | |
| return n; | |
| } | |
| const numeric = parseInt(permissions, 8); | |
| return !Number.isNaN(numeric) ? numeric & 0o777 : (isDir ? 0o755 : 0o644); | |
| }; |
| terminal.open(terminalRef.value); | ||
| fitAddon.fit(); | ||
|
|
||
| terminal.onData((data) => { |
There was a problem hiding this comment.
The terminal input handling currently processes data character by character and sends command messages on Enter. For a more 'native' feel, consider sending all raw input directly to the backend PTY and letting the server handle the buffering/echoing, which avoids double-echoing or mismatched states in complex CLI tools.
| terminal.onData((data) => { | |
| terminal.onData((data) => { | |
| if (!connected.value || !socket || socket.readyState !== WebSocket.OPEN) return; | |
| socket.send(JSON.stringify({ type: "input", data })); | |
| }); |
ESLint flagged the ANSI-stripping regex in the container terminal as no-control-regex; the disable comment is added inline so the intent stays scoped to that one expression. Two dead helpers in the file browser (a row-menu closer and a mount tooltip generator) were left over after the overflow menu refactor; removed them so unused-vars passes cleanly. Settings now ships a System Terminal protection tab and the deployment detail view now ships a Settings (protected mode) tab, so the tab-count assertions are bumped from six to seven and from nine to ten, with coverage for each new tab label.
Deploying flatrun-ui with
|
| Latest commit: |
ff48be5
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://344e59f9.flatrun-ui.pages.dev |
| Branch Preview URL: | https://feat-2026-05.flatrun-ui.pages.dev |
| localStorage.removeItem("auth_token"); | ||
| window.location.href = "/login"; | ||
| const failedURL: string = error.config?.url || ""; | ||
| const isSessionEndpoint = /\/auth\/|\/users\/me(\b|\/)/.test(failedURL); |
There was a problem hiding this comment.
The regex \/auth\/|\/users\/me(\b|\/) covers common session endpoints, but it might still catch false positives depending on deployment names. Since session management is critical, explicitly matching the expected root path of the auth service is safer.
| const isSessionEndpoint = /\/auth\/|\/users\/me(\b|\/)/.test(failedURL); | |
| const isSessionEndpoint = failedURL.endsWith('/auth/login') || failedURL.includes('/auth/logout') || failedURL.includes('/users/me'); |
| if (symbolic[8] === "x") n |= 0o001; | ||
| return n; | ||
| } | ||
| const numeric = parseInt(permissions, 8); |
There was a problem hiding this comment.
When parsing octal permissions, specifying the radix is good, but applying a strict mask to ensure it stays within standard Unix permission bits (0o777) prevents issues with setuid/setgid bits if not intended.
| const numeric = parseInt(permissions, 8); | |
| const numeric = parseInt(permissions, 8) & 0o777; |
…s tabs The deep-equal assertions in the tab-definitions specs were not updated when the new tabs were introduced, so they still listed nine and six tabs respectively while the views render ten and seven. Adding the missing entries (Terminal in settings, Settings in the deployment detail view) brings the specs back in sync.
| localStorage.removeItem("auth_token"); | ||
| window.location.href = "/login"; | ||
| const failedURL: string = error.config?.url || ""; | ||
| const isSessionEndpoint = /\/auth\/|\/users\/me(\b|\/)/.test(failedURL); |
There was a problem hiding this comment.
The regex for session endpoints is a bit broad. It might accidentally match resource names that contain 'auth' in the path. Using startsWith or explicit path segments is safer for session management logic.
| const isSessionEndpoint = /\/auth\/|\/users\/me(\b|\/)/.test(failedURL); | |
| const isSessionEndpoint = failedURL.includes('/auth/') || failedURL.includes('/users/me'); |
API keys can now be edited from the UI in a tabbed dialog that mirrors the user dialog: profile, permissions, and a deployment access tab that grants per-deployment read, write, or admin level. The tabbed modal shell and the deployment access field are extracted into shared components so the user dialog and the API key dialog use exactly the same primitives. The blanket 401 interceptor that logged people out on any per-resource auth error is narrowed to only redirect on session endpoints, so a refused create or update surfaces as a toast instead of a forced sign-out.
The deployment protected-mode panel and the global system-terminal protection panel share a single explainer helper, so each blocked command rule renders a human-readable description ("blocks any command containing rm -rf") and the add form previews the rule asit is typed. Tooltips on the blocked-action chips state what each action covers.
The file browser becomes context-agnostic: it accepts an injected API adapter and renders the same UI for deployment files and for a new system-wide file manager. Files and folders can be created in place, permissions edited through a per-bit chmod dialog, and row actions are collapsed into an overflow menu to reduce accidental
clicks. Hidden and system folders are hidden by default with toggles to reveal them, and the manager opens in the user's home directory while still allowing navigation up to the configured root.
New routes mount the system-wide file manager and the system terminal in the dashboard, both gated by the corresponding new permissions threaded through the role defaults.
Closes #117
Closes #122
Closes #123