Skip to content

feat: Add API key edit, protected mode UI, file manager improvements, system views#63

Merged
nfebe merged 4 commits into
mainfrom
feat/2026-05
May 25, 2026
Merged

feat: Add API key edit, protected mode UI, file manager improvements, system views#63
nfebe merged 4 commits into
mainfrom
feat/2026-05

Conversation

@nfebe

@nfebe nfebe commented May 25, 2026

Copy link
Copy Markdown
Contributor

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

nfebe added 2 commits May 25, 2026 11:21
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
@sourceant

sourceant Bot commented May 25, 2026

Copy link
Copy Markdown

Code Review Summary

This 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

  • Extracted TabbedFormModal and DeploymentAccessField into shared components.
  • Narrowed the 401 interceptor to only logout on actual session failures.
  • Implemented a context-agnostic FileBrowser that works for both deployments and system-wide access via an injected API adapter.

💡 Minor Suggestions

  • Simplify permissions bitwise logic in FileBrowser.vue.
  • Switch from command-buffered terminal input to raw input for better interactive shell support.

@sourceant sourceant Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review complete. See the overview comment for a summary.

Comment thread src/services/api.ts
Comment on lines -39 to -40
localStorage.removeItem("auth_token");
window.location.href = "/login";

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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 => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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) => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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.
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented May 25, 2026

Copy link
Copy Markdown

Deploying flatrun-ui with  Cloudflare Pages  Cloudflare Pages

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

View logs

@sourceant sourceant Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review complete. See the overview comment for a summary.

Comment thread src/services/api.ts
localStorage.removeItem("auth_token");
window.location.href = "/login";
const failedURL: string = error.config?.url || "";
const isSessionEndpoint = /\/auth\/|\/users\/me(\b|\/)/.test(failedURL);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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.

@sourceant sourceant Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review complete. See the overview comment for a summary.

Comment thread src/services/api.ts
localStorage.removeItem("auth_token");
window.location.href = "/login";
const failedURL: string = error.config?.url || "";
const isSessionEndpoint = /\/auth\/|\/users\/me(\b|\/)/.test(failedURL);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
const isSessionEndpoint = /\/auth\/|\/users\/me(\b|\/)/.test(failedURL);
const isSessionEndpoint = failedURL.includes('/auth/') || failedURL.includes('/users/me');

@nfebe nfebe merged commit 18b6410 into main May 25, 2026
5 checks passed
@nfebe nfebe deleted the feat/2026-05 branch May 25, 2026 11:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant