fix(system): report real version so update banner clears; move update notice to Operations (v0.16.11-beta)
Update-availability fixes: - The backend read its version from npm_package_version, which is unset when the container runs `node dist/index.js` directly, so current was always "0.0.0" and every release looked newer — the "update available" banner never cleared. Read the bundled package.json instead (routes/system.ts), falling back to npm_package_version. - Move the update notification out of the top-of-app banner into the Operations dashboard (a dedicated notice under the network-status hero), shown only when genuinely behind and cleared once up to date. Security banner: - Replace the now-dead jwt_secret_default / encryption_key_default checks with the current secrets_not_persisted warning (auto-generated secrets that couldn't be persisted), matching the self-healing secrets model. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
9e962939ea
commit
8e564fc813
@@ -1,7 +1,7 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { AlertTriangle, ShieldAlert, ArrowUpCircle, X } from 'lucide-react';
|
||||
import { authApi, systemApi } from '../services/api';
|
||||
import { AlertTriangle, ShieldAlert } from 'lucide-react';
|
||||
import { authApi } from '../services/api';
|
||||
|
||||
interface SecurityStatus {
|
||||
warnings: string[];
|
||||
@@ -9,83 +9,34 @@ interface SecurityStatus {
|
||||
|
||||
export default function SecurityBanners() {
|
||||
const [status, setStatus] = useState<SecurityStatus | null>(null);
|
||||
const [updateInfo, setUpdateInfo] = useState<{ latest: string } | null>(null);
|
||||
const [updateDismissed, setUpdateDismissed] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
authApi.securityStatus()
|
||||
.then((r) => setStatus(r.data))
|
||||
.catch(() => {});
|
||||
|
||||
systemApi.versionCheck()
|
||||
.then((r) => {
|
||||
if (r.data.update_available && r.data.latest) {
|
||||
setUpdateInfo({ latest: r.data.latest });
|
||||
}
|
||||
})
|
||||
.catch(() => {});
|
||||
}, []);
|
||||
|
||||
const noWarnings = !status || status.warnings.length === 0;
|
||||
const noUpdate = !updateInfo || updateDismissed;
|
||||
if (noWarnings && noUpdate) return null;
|
||||
if (noWarnings) return null;
|
||||
|
||||
const hasSecretWarning = !!status && (
|
||||
status.warnings.includes('jwt_secret_default') ||
|
||||
status.warnings.includes('encryption_key_default')
|
||||
);
|
||||
const hasSecretsNotPersisted = !!status && status.warnings.includes('secrets_not_persisted');
|
||||
const hasAdminWarning = !!status && status.warnings.includes('admin_password_default');
|
||||
|
||||
const secretNames = [
|
||||
status?.warnings.includes('jwt_secret_default') && 'JWT_SECRET',
|
||||
status?.warnings.includes('encryption_key_default') && 'ENCRYPTION_KEY',
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' and ');
|
||||
|
||||
return (
|
||||
<div>
|
||||
{updateInfo && !updateDismissed && (
|
||||
<div className="flex items-center justify-between gap-4 bg-blue-50 dark:bg-blue-900/20 border-b border-blue-200 dark:border-blue-800 px-4 py-2 text-sm text-blue-800 dark:text-blue-200">
|
||||
<div className="flex items-center gap-2">
|
||||
<ArrowUpCircle className="w-4 h-4 shrink-0" />
|
||||
<span>
|
||||
<span className="font-semibold">Update available:</span>{' '}
|
||||
v{updateInfo.latest} is on GitHub.{' '}
|
||||
<a
|
||||
href="https://github.com/2GT-Media-Group-LLC/mikrotik-manager"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="underline hover:no-underline"
|
||||
>
|
||||
View release
|
||||
</a>{' '}
|
||||
and run <code className="font-mono text-xs">docker compose pull && docker compose up -d</code> to update.
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setUpdateDismissed(true)}
|
||||
className="shrink-0 text-blue-500 hover:text-blue-700 dark:hover:text-blue-300"
|
||||
aria-label="Dismiss"
|
||||
>
|
||||
<X className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
{hasSecretWarning && (
|
||||
{hasSecretsNotPersisted && (
|
||||
<div className="flex items-center gap-2 bg-yellow-50 dark:bg-yellow-900/20 border-b border-yellow-200 dark:border-yellow-800 px-4 py-2 text-sm text-yellow-800 dark:text-yellow-200">
|
||||
<AlertTriangle className="w-4 h-4 shrink-0" />
|
||||
<span>
|
||||
<span className="font-semibold">Security warning:</span>{' '}
|
||||
{secretNames}{' '}
|
||||
{status.warnings.filter((w) =>
|
||||
['jwt_secret_default', 'encryption_key_default'].includes(w)
|
||||
).length > 1
|
||||
? 'are'
|
||||
: 'is'}{' '}
|
||||
using default values. Set strong secrets in your{' '}
|
||||
<code className="font-mono text-xs">.env</code> file before
|
||||
exposing this to any network.
|
||||
Auto-generated secrets could not be saved, so they will change on
|
||||
every restart — logging users out and potentially orphaning stored
|
||||
device credentials. Mount a writable volume at{' '}
|
||||
<code className="font-mono text-xs">SECRETS_DIR</code> (default{' '}
|
||||
<code className="font-mono text-xs">/app/data</code>) or set{' '}
|
||||
<code className="font-mono text-xs">JWT_SECRET</code> and{' '}
|
||||
<code className="font-mono text-xs">ENCRYPTION_KEY</code> in your{' '}
|
||||
<code className="font-mono text-xs">.env</code>.
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user