diff --git a/frontend-modern/src/App.tsx b/frontend-modern/src/App.tsx
index 64badb006..eb8ab1b54 100644
--- a/frontend-modern/src/App.tsx
+++ b/frontend-modern/src/App.tsx
@@ -47,7 +47,10 @@ export const useDarkMode = () => {
function App() {
const owner = getOwner();
- const acquireWsStore = () => (owner ? runWithOwner(owner, () => getGlobalWebSocketStore()) : getGlobalWebSocketStore());
+ const acquireWsStore = (): EnhancedStore => {
+ const store = owner ? runWithOwner(owner, () => getGlobalWebSocketStore()) : getGlobalWebSocketStore();
+ return store || getGlobalWebSocketStore();
+ };
// Simple auth state
const [isLoading, setIsLoading] = createSignal(true);
diff --git a/frontend-modern/src/components/Settings/OIDCPanel.tsx b/frontend-modern/src/components/Settings/OIDCPanel.tsx
index 613f9b492..a3f6a2275 100644
--- a/frontend-modern/src/components/Settings/OIDCPanel.tsx
+++ b/frontend-modern/src/components/Settings/OIDCPanel.tsx
@@ -202,6 +202,16 @@ export const OIDCPanel: Component = (props) => {
+
+
+ Exports and imports {securityStatus()?.exportProtected && !securityStatus()?.unprotectedExportAllowed ? 'require an API token and a passphrase' : 'follow the current server policy'}. Generating a token lets you:
+
+
+ - Authenticate scripts with the
X-API-Token header.
+ - Unlock encrypted export/import flows in Settings → Security → Backup & restore.
+ - Keep UI logins separate from automation secrets.
+
+
+
+ Unprotected exports are currently allowed. Set ALLOW_UNPROTECTED_EXPORT=false or configure an API token to harden backups.
+
+
+
diff --git a/frontend-modern/src/pages/Alerts.tsx b/frontend-modern/src/pages/Alerts.tsx
index 0947b193c..4dc0c7b75 100644
--- a/frontend-modern/src/pages/Alerts.tsx
+++ b/frontend-modern/src/pages/Alerts.tsx
@@ -88,15 +88,16 @@ interface GroupingConfig {
byGuest?: boolean;
}
+type EscalationNotifyTarget = 'email' | 'webhook' | 'all';
+
+interface EscalationLevel {
+ after: number;
+ notify: EscalationNotifyTarget;
+}
+
interface EscalationConfig {
enabled: boolean;
- timeToEscalate?: number;
- levels: Array<{
- level?: number;
- destinations?: string[];
- after?: number;
- notify?: string;
- }>;
+ levels: EscalationLevel[];
}
const getLocalTimezone = () => Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC';
@@ -411,9 +412,14 @@ export function Alerts() {
}
if (config.schedule.escalation) {
+ const rawLevels = config.schedule.escalation.levels || [];
+ const levels = rawLevels.map((level) => ({
+ after: typeof level.after === 'number' ? level.after : 15,
+ notify: (level.notify as EscalationNotifyTarget) || 'all'
+ }));
setScheduleEscalation({
- enabled: config.schedule.escalation.enabled || false,
- levels: config.schedule.escalation.levels || []
+ enabled: Boolean(config.schedule.escalation.enabled),
+ levels
});
}
}
@@ -1647,7 +1653,8 @@ function ScheduleTab(props: ScheduleTabProps) {
value={level.after}
onChange={(e) => {
const newLevels = [...escalation().levels];
- newLevels[index()] = { ...level, after: parseInt(e.currentTarget.value) };
+ const parsed = Number.parseInt(e.currentTarget.value, 10);
+ newLevels[index()] = { ...level, after: Number.isNaN(parsed) ? level.after : parsed };
setEscalation({ ...escalation(), levels: newLevels });
props.setHasUnsavedChanges(true);
}}
@@ -1661,7 +1668,7 @@ function ScheduleTab(props: ScheduleTabProps) {
value={level.notify}
onChange={(e) => {
const newLevels = [...escalation().levels];
- newLevels[index()] = { ...level, notify: e.currentTarget.value };
+ newLevels[index()] = { ...level, notify: e.currentTarget.value as EscalationNotifyTarget };
setEscalation({ ...escalation(), levels: newLevels });
props.setHasUnsavedChanges(true);
}}
@@ -1698,7 +1705,7 @@ function ScheduleTab(props: ScheduleTabProps) {
const newAfter = typeof lastLevel?.after === 'number' ? lastLevel.after + 30 : 15;
setEscalation({
...escalation(),
- levels: [...escalation().levels, { after: newAfter, notify: 'all' }]
+ levels: [...escalation().levels, { after: newAfter, notify: 'all' as EscalationNotifyTarget }]
});
props.setHasUnsavedChanges(true);
}}
diff --git a/frontend-modern/src/types/config.ts b/frontend-modern/src/types/config.ts
index b46b373bd..2e816ea3d 100644
--- a/frontend-modern/src/types/config.ts
+++ b/frontend-modern/src/types/config.ts
@@ -97,6 +97,18 @@ export interface SecurityStatus {
exportProtected: boolean;
hasAuditLogging: boolean;
configuredButPendingRestart: boolean;
+ unprotectedExportAllowed?: boolean;
+ hasHTTPS?: boolean;
+ oidcEnabled?: boolean;
+ publicAccess?: boolean;
+ isPrivateNetwork?: boolean;
+ clientIP?: string;
+ hasProxyAuth?: boolean;
+ proxyAuthUsername?: string;
+ proxyAuthIsAdmin?: boolean;
+ proxyAuthLogoutURL?: string;
+ authUsername?: string;
+ authLastModified?: string;
}
/**
@@ -137,4 +149,4 @@ export const DEFAULT_CONFIG: {
backendPort: 7655,
frontendPort: 7655,
}
-};
\ No newline at end of file
+};
diff --git a/internal/api/router.go b/internal/api/router.go
index ee2c29138..337dc435b 100644
--- a/internal/api/router.go
+++ b/internal/api/router.go
@@ -235,10 +235,10 @@ func (r *Router) setupRoutes() {
envPath = "/etc/pulse/.env"
}
- // If no auth is currently active but .env exists, security is pending restart
- if !hasAuthentication && r.config.AuthUser == "" && r.config.AuthPass == "" {
- if _, err := os.Stat(envPath); err == nil {
- // .env exists but auth not loaded - pending restart
+ authLastModified := ""
+ if stat, err := os.Stat(envPath); err == nil {
+ authLastModified = stat.ModTime().UTC().Format(time.RFC3339)
+ if !hasAuthentication && r.config.AuthUser == "" && r.config.AuthPass == "" {
configuredButPendingRestart = true
}
}
@@ -306,6 +306,8 @@ func (r *Router) setupRoutes() {
"proxyAuthLogoutURL": r.config.ProxyAuthLogoutURL,
"proxyAuthUsername": proxyAuthUsername,
"proxyAuthIsAdmin": proxyAuthIsAdmin,
+ "authUsername": r.config.AuthUser,
+ "authLastModified": authLastModified,
}
if oidcCfg != nil {