diff --git a/src/app/servers/components/ServerConfigModal.tsx b/src/app/servers/components/ServerConfigModal.tsx
index 9505f4c..dacf96a 100644
--- a/src/app/servers/components/ServerConfigModal.tsx
+++ b/src/app/servers/components/ServerConfigModal.tsx
@@ -1,19 +1,13 @@
"use client";
-import { type ReactNode, useCallback, useEffect, useState } from "react";
+import { useCallback, useEffect, useState } from "react";
import { createPortal } from "react-dom";
import {
AlertTriangle,
Eye,
EyeOff,
- Globe,
- HardDrive,
Loader2,
- Power,
RefreshCw,
- Settings2,
- Users,
- Wrench,
X,
} from "lucide-react";
import {
@@ -87,6 +81,14 @@ export default function ServerConfigModal({
const deleteConfirmed = resetConfirmation.trim() === "DELETE";
const hasSelectedDockerPruneOption =
Object.values(dockerPruneOptions).some(Boolean);
+ const tabs: Array<{ id: ServerConfigTab; label: string }> = [
+ { id: "overview", label: "Overview" },
+ { id: "users", label: "Users" },
+ { id: "services", label: "Services" },
+ { id: "web-server", label: "Web Server" },
+ { id: "mounts", label: "Disk Mounts" },
+ { id: "actions", label: "Actions" },
+ ];
const publishNotice = useCallback((nextNotice: ServerConfigNotice) => {
setNotice(nextNotice);
@@ -602,26 +604,26 @@ export default function ServerConfigModal({
}
};
- const tabButton = (id: ServerConfigTab, label: string, icon: ReactNode) => (
+ const tabButton = (id: ServerConfigTab, label: string) => (
setActiveTab(id)}
+ className="btn btn-ghost"
style={{
- padding: "9px 14px",
- borderRadius: 10,
- border: `1px solid ${activeTab === id ? "rgba(59,130,246,0.35)" : "var(--border)"}`,
- background:
- activeTab === id ? "rgba(59,130,246,0.12)" : "var(--bg-input)",
- color: activeTab === id ? "#3b82f6" : "var(--text-secondary)",
- display: "inline-flex",
- alignItems: "center",
- gap: 8,
- cursor: "pointer",
+ height: 28,
+ minHeight: 28,
+ padding: "5px 12px",
+ borderRadius: 4,
+ boxSizing: "border-box",
+ borderColor: activeTab === id ? "rgba(59,130,246,0.5)" : "transparent",
+ background: activeTab === id ? "rgba(59,130,246,0.16)" : "transparent",
+ color:
+ activeTab === id ? "var(--accent-blue)" : "var(--text-secondary)",
+ flex: "0 0 auto",
fontSize: 12,
- fontWeight: 600,
}}
>
- {icon}
{label}
);
@@ -637,523 +639,569 @@ export default function ServerConfigModal({
>
-
- {pendingConfirm && typeof document !== "undefined"
- ? createPortal(
-
-
-
-
- {pendingConfirm.title}
-
-
- {pendingConfirm.description}
-
-
- {pendingConfirm.kind === "server" &&
- pendingConfirm.action === "prune-docker" ? (
-
-
- Select unused Docker artifacts to clean up
-
- {[
- {
- key: "images",
- label: "Unused images",
- description: "Equivalent to docker image prune -a.",
- },
- {
- key: "containers",
- label: "Stopped containers",
- description: "Equivalent to docker container prune.",
- },
- {
- key: "networks",
- label: "Unused networks",
- description: "Equivalent to docker network prune.",
- },
- {
- key: "volumes",
- label: "Unused volumes",
- description: "Equivalent to docker volume prune.",
- },
- {
- key: "buildCache",
- label: "Build cache",
- description: "Equivalent to docker builder prune.",
- },
- ].map((option) => {
- const checked =
- dockerPruneOptions[
- option.key as keyof typeof dockerPruneOptions
- ];
-
- return (
-
- {
- const nextOptions = {
- ...dockerPruneOptions,
- [option.key]: event.target.checked,
- };
- setDockerPruneOptions(nextOptions);
- setPendingConfirm((current) =>
- current &&
- current.kind === "server" &&
- current.action === "prune-docker"
- ? {
- ...current,
- description:
- getDockerPruneSummary(nextOptions),
- pruneOptions: nextOptions,
- }
- : current,
- );
- }}
- style={{ marginTop: 2 }}
- />
-
-
- {option.label}
-
-
- {option.description}
-
-
-
- );
- })}
-
- ) : null}
-
- Confirm this action only if you expect a brief service
- interruption or cleanup change on the host.
-
-
-
setPendingConfirm(null)}
- disabled={activeActionKey !== null}
- >
- Cancel
-
-
{
- const currentConfirm = pendingConfirm;
- if (!currentConfirm) {
- return;
- }
-
- if (currentConfirm.kind === "server") {
- if (
- currentConfirm.action === "prune-docker" &&
- !hasSelectedDockerPruneOption
- ) {
- return;
- }
-
- setPendingConfirm(null);
- void handleServerAction(
- currentConfirm.action,
- currentConfirm.pruneOptions,
- );
- return;
- }
-
- setPendingConfirm(null);
-
- if (currentConfirm.kind === "docker") {
- void handleDockerAction(currentConfirm.action);
- return;
- }
-
- if (currentConfirm.kind === "service") {
- void handleServiceRestart(currentConfirm.serviceName);
- return;
- }
-
- void handleWebStackAction(
- currentConfirm.component,
- currentConfirm.action,
- );
- }}
- disabled={
- activeActionKey !== null ||
- (pendingConfirm.kind === "server" &&
- pendingConfirm.action === "prune-docker" &&
- !hasSelectedDockerPruneOption)
- }
- style={{
- background:
- pendingConfirm.tone === "danger"
- ? "rgba(239,68,68,0.12)"
- : "rgba(245,158,11,0.12)",
- color:
- pendingConfirm.tone === "danger" ? "#ef4444" : "#f59e0b",
- border:
- pendingConfirm.tone === "danger"
- ? "1px solid rgba(239,68,68,0.22)"
- : "1px solid rgba(245,158,11,0.22)",
- opacity:
- pendingConfirm.kind === "server" &&
- pendingConfirm.action === "prune-docker" &&
- !hasSelectedDockerPruneOption
- ? 0.6
- : 1,
- }}
- >
- {activeActionKey !== null ? (
-
- ) : (
-
- )}
- {pendingConfirm.confirmLabel}
-
-
-
-
,
- document.body,
- )
- : null}
-
-
-
-
- Server Config
-
-
-
-
- {server.name} • {server.ip}:{server.sshPort}
-
-
- Snapshot refresh runs only when this modal opens or when you click
- Refresh.
-
-
-
- void loadSnapshot()}
- disabled={loading}
- >
- Refresh
-
-
-
-
-
- {tabButton("overview", "Overview",
)}
- {tabButton("users", "Users",
)}
- {tabButton("services", "Services",
)}
- {tabButton("web-server", "Web Server",
)}
- {tabButton("mounts", "Disk Mounts",
)}
- {tabButton("actions", "Actions",
)}
-
-
- {notice && notice.tab === activeTab ? (
-
-
-
-
{notice.title}
-
- {notice.summary}
-
-
-
{
- setNotice(null);
- setNoticeExpanded(false);
- }}
- aria-label="Dismiss notice"
- style={{
- display: "inline-flex",
- alignItems: "center",
- justifyContent: "center",
- width: 28,
- height: 28,
- borderRadius: 8,
- border: "1px solid rgba(148,163,184,0.18)",
- background: "rgba(15,23,42,0.16)",
- color: "currentColor",
- cursor: "pointer",
- flexShrink: 0,
- }}
- >
-
-
-
- {notice.details?.length || notice.detailText ? (
-
-
setNoticeExpanded((current) => !current)}
- style={{ width: "fit-content" }}
+ {pendingConfirm && typeof document !== "undefined"
+ ? createPortal(
+
- {noticeExpanded ?
:
}
- {noticeExpanded ? "Hide detail" : "More detail"}
-
- {noticeExpanded ? (
- {notice.details?.length ? (
-
+ {pendingConfirm.description}
+
+
+ {pendingConfirm.kind === "server" &&
+ pendingConfirm.action === "prune-docker" ? (
+
+
+ Select unused Docker artifacts to clean up
+
+ {[
+ {
+ key: "images",
+ label: "Unused images",
+ description: "Equivalent to docker image prune -a.",
+ },
+ {
+ key: "containers",
+ label: "Stopped containers",
+ description:
+ "Equivalent to docker container prune.",
+ },
+ {
+ key: "networks",
+ label: "Unused networks",
+ description: "Equivalent to docker network prune.",
+ },
+ {
+ key: "volumes",
+ label: "Unused volumes",
+ description: "Equivalent to docker volume prune.",
+ },
+ {
+ key: "buildCache",
+ label: "Build cache",
+ description: "Equivalent to docker builder prune.",
+ },
+ ].map((option) => {
+ const checked =
+ dockerPruneOptions[
+ option.key as keyof typeof dockerPruneOptions
+ ];
+
+ return (
+
+ {
+ const nextOptions = {
+ ...dockerPruneOptions,
+ [option.key]: event.target.checked,
+ };
+ setDockerPruneOptions(nextOptions);
+ setPendingConfirm((current) =>
+ current &&
+ current.kind === "server" &&
+ current.action === "prune-docker"
+ ? {
+ ...current,
+ description:
+ getDockerPruneSummary(nextOptions),
+ pruneOptions: nextOptions,
+ }
+ : current,
+ );
+ }}
+ style={{ marginTop: 2 }}
+ />
+
+
+ {option.label}
+
+
+ {option.description}
+
+
+
+ );
+ })}
+
) : null}
- {notice.detailText ? (
-
+ Confirm this action only if you expect a brief service
+ interruption or cleanup change on the host.
+
+
+
setPendingConfirm(null)}
+ disabled={activeActionKey !== null}
+ >
+ Cancel
+
+
{
+ const currentConfirm = pendingConfirm;
+ if (!currentConfirm) {
+ return;
+ }
+
+ if (currentConfirm.kind === "server") {
+ if (
+ currentConfirm.action === "prune-docker" &&
+ !hasSelectedDockerPruneOption
+ ) {
+ return;
+ }
+
+ setPendingConfirm(null);
+ void handleServerAction(
+ currentConfirm.action,
+ currentConfirm.pruneOptions,
+ );
+ return;
+ }
+
+ setPendingConfirm(null);
+
+ if (currentConfirm.kind === "docker") {
+ void handleDockerAction(currentConfirm.action);
+ return;
+ }
+
+ if (currentConfirm.kind === "service") {
+ void handleServiceRestart(
+ currentConfirm.serviceName,
+ );
+ return;
+ }
+
+ void handleWebStackAction(
+ currentConfirm.component,
+ currentConfirm.action,
+ );
+ }}
+ disabled={
+ activeActionKey !== null ||
+ (pendingConfirm.kind === "server" &&
+ pendingConfirm.action === "prune-docker" &&
+ !hasSelectedDockerPruneOption)
+ }
style={{
- margin: 0,
- whiteSpace: "pre-wrap",
- wordBreak: "break-word",
- color: "var(--text-primary)",
- fontSize: 12,
- fontFamily: "var(--font-geist-mono, monospace)",
+ background:
+ pendingConfirm.tone === "danger"
+ ? "rgba(239,68,68,0.12)"
+ : "rgba(245,158,11,0.12)",
+ color:
+ pendingConfirm.tone === "danger"
+ ? "#ef4444"
+ : "#f59e0b",
+ border:
+ pendingConfirm.tone === "danger"
+ ? "1px solid rgba(239,68,68,0.22)"
+ : "1px solid rgba(245,158,11,0.22)",
+ opacity:
+ pendingConfirm.kind === "server" &&
+ pendingConfirm.action === "prune-docker" &&
+ !hasSelectedDockerPruneOption
+ ? 0.6
+ : 1,
}}
>
- {notice.detailText}
-
+ {activeActionKey !== null ? (
+
+ ) : (
+
+ )}
+ {pendingConfirm.confirmLabel}
+
+
+
+
,
+ document.body,
+ )
+ : null}
+
+
+
+
+
+ Server Config
+
+
+
+
+ {server.name} • {server.ip}:{server.sshPort}
+
+
+ Snapshot refresh runs only when this modal opens or when you
+ click Refresh.
+
+
+
+ void loadSnapshot()}
+ disabled={loading}
+ >
+ Refresh
+
+
+
+
+
+ {tabs.map((tab) => tabButton(tab.id, tab.label))}
+
+
+
+ {notice && notice.tab === activeTab ? (
+
+
+
+
{notice.title}
+
+ {notice.summary}
+
+
+
{
+ setNotice(null);
+ setNoticeExpanded(false);
+ }}
+ aria-label="Dismiss notice"
+ style={{
+ display: "inline-flex",
+ alignItems: "center",
+ justifyContent: "center",
+ width: 28,
+ height: 28,
+ borderRadius: 8,
+ border: "1px solid rgba(148,163,184,0.18)",
+ background: "rgba(15,23,42,0.16)",
+ color: "currentColor",
+ cursor: "pointer",
+ flexShrink: 0,
+ }}
+ >
+
+
+
+ {notice.details?.length || notice.detailText ? (
+
+
setNoticeExpanded((current) => !current)}
+ style={{ width: "fit-content" }}
+ >
+ {noticeExpanded ? (
+
+ ) : (
+
+ )}
+ {noticeExpanded ? "Hide detail" : "More detail"}
+
+ {noticeExpanded ? (
+
+ {notice.details?.length ? (
+
+ {notice.details.map((detail) => (
+ {detail}
+ ))}
+
+ ) : null}
+ {notice.detailText ? (
+
+ {notice.detailText}
+
+ ) : null}
+
) : null}
) : null}
) : null}
-
- ) : null}
- {error ? (
-
- ) : null}
+ {error ? (
+
+ ) : null}
- {snapshotLoadError ? (
-
-
-
- Live configuration snapshot unavailable
-
-
- The server did not return a full config snapshot, so this modal
- is showing fallback information. Recovery actions remain
- available, especially in the Actions tab.
-
-
-
-
- ) : null}
+
+
+ Live configuration snapshot unavailable
+
+
+ The server did not return a full config snapshot, so this
+ modal is showing fallback information. Recovery actions
+ remain available, especially in the Actions tab.
+
+
+
+
+ ) : null}
- {loading ? (
-
-
-
- Loading configuration snapshot...
-
+ {loading ? (
+
+
+
+ Loading configuration snapshot...
+
+
+ ) : (
+ renderActiveTab()
+ )}
- ) : (
- renderActiveTab()
- )}