Release v1.34.1
- **Einstellungen in Bereiche gegliedert:** Mein Konto, Benachrichtigungen, Anmeldung & Zugriff, Betrieb, Sicherung & Export und System, mit Navigation links (am Handy als wischbare Leiste). Die Betriebseinstellungen stehen jetzt beim jeweiligen Thema. Bestehende Links (z. B. aus dem Update-Hinweis) öffnen den passenden Bereich. - **Git-Checkouts:** `opnexus update` lässt in einem Git-Checkout (Release-Modus aus dem Quellcode-Verzeichnis) die versionierte `docker-compose.yml` unangetastet; sie kommt dort mit `git pull`.
This commit is contained in:
1 parent
b1a96c7563
commit
a56dc0c8b9
12 files changed
+227
-39
No files matched your search
+9
-1
@@ -29,7 +29,15 @@ in der App auf der Login-Seite, im Footer jeder Seite, auf `/about` und in
|
||||
den Einstellungen.
|
||||
|
||||
Nächste Codenamen (der Reihe nach, wenn die jeweils nächste MINOR-Version
|
||||
ansteht): wird beim nächsten MINOR-Release festgelegt (Saker ist mit 1.33.0 vergeben).
|
||||
ansteht): wird beim nächsten MINOR-Release festgelegt (Griffon ist mit 1.34.0 vergeben).
|
||||
|
||||
## [1.34.1] "Griffon" — 2026-10-24
|
||||
|
||||
- **Einstellungen in Bereiche gegliedert:** Mein Konto, Benachrichtigungen, Anmeldung & Zugriff, Betrieb, Sicherung & Export und
|
||||
System, mit Navigation links (am Handy als wischbare Leiste). Die Betriebseinstellungen stehen jetzt beim jeweiligen Thema. Bestehende
|
||||
Links (z. B. aus dem Update-Hinweis) öffnen den passenden Bereich.
|
||||
- **Git-Checkouts:** `opnexus update` lässt in einem Git-Checkout (Release-Modus aus dem Quellcode-Verzeichnis) die versionierte
|
||||
`docker-compose.yml` unangetastet; sie kommt dort mit `git pull`.
|
||||
|
||||
## [1.34.0] "Griffon" — 2026-10-24
|
||||
|
||||
|
||||
+2
-2
@@ -7,7 +7,7 @@ Zentrale Verwaltung mehrerer [OPNsense](https://opnsense.org)-Firewalls: Regeln,
|
||||
|
||||

|
||||
|
||||
> **Stand:** `1.34.0` ist die aktuelle stabile Version. Entwickelt und getestet wurde gegen echte OPNsense-Instanzen; eine frische Installation aus dem Release-Bundle sowie Updates und Rollbacks sind durchgängig geprüft. Ecken und Kanten bitte melden.
|
||||
> **Stand:** `1.34.1` ist die aktuelle stabile Version. Entwickelt und getestet wurde gegen echte OPNsense-Instanzen; eine frische Installation aus dem Release-Bundle sowie Updates und Rollbacks sind durchgängig geprüft. Ecken und Kanten bitte melden.
|
||||
|
||||
## Funktionen
|
||||
|
||||
@@ -68,7 +68,7 @@ Für den Zugriff von anderen Rechnern und den Produktivbetrieb mit TLS (mitgelie
|
||||
|
||||
```bash
|
||||
OPNEXUS_IMAGE_REPO=git.opnexus.dev/opnexus
|
||||
OPNEXUS_VERSION=1.34.0
|
||||
OPNEXUS_VERSION=1.34.1
|
||||
```
|
||||
|
||||
```bash
|
||||
|
||||
@@ -7,7 +7,7 @@ Central management for multiple [OPNsense](https://opnsense.org) firewalls: rule
|
||||
|
||||

|
||||
|
||||
> **Status:** `1.34.0` is the current stable release. It has been developed and tested against real OPNsense instances; a clean install from the release bundle, updates and rollbacks have been verified end to end. Please report rough edges.
|
||||
> **Status:** `1.34.1` is the current stable release. It has been developed and tested against real OPNsense instances; a clean install from the release bundle, updates and rollbacks have been verified end to end. Please report rough edges.
|
||||
|
||||
## What it does
|
||||
|
||||
@@ -70,7 +70,7 @@ For access from other machines and for production use with TLS (bundled Caddy pr
|
||||
|
||||
```bash
|
||||
OPNEXUS_IMAGE_REPO=git.opnexus.dev/opnexus
|
||||
OPNEXUS_VERSION=1.34.0
|
||||
OPNEXUS_VERSION=1.34.1
|
||||
```
|
||||
|
||||
```bash
|
||||
|
||||
@@ -6,5 +6,5 @@ convention — but both are bumped together whenever a release ships).
|
||||
Versioning scheme: see CHANGELOG.md ("Versionierung").
|
||||
"""
|
||||
|
||||
APP_VERSION = "1.34.0"
|
||||
APP_VERSION = "1.34.1"
|
||||
APP_CODENAME = "Griffon"
|
||||
@@ -18,6 +18,7 @@ import { SystemSettingsSection } from "@/components/SystemSettingsSection";
|
||||
import { AuditExportSection } from "@/components/AuditExportSection";
|
||||
import { APP_CODENAME, APP_MAINTAINER, APP_VERSION } from "@/lib/version";
|
||||
import { translate } from "@/lib/i18n";
|
||||
import { Bell, HardDrive, Info, KeyRound, SlidersHorizontal, User } from "lucide-react";
|
||||
|
||||
function SetupAssistantSection() {
|
||||
const { t } = useLanguage();
|
||||
@@ -482,14 +483,150 @@ function LogIngestSection() {
|
||||
);
|
||||
}
|
||||
|
||||
type CategoryKey = "account" | "notifications" | "access" | "operation" | "data" | "system";
|
||||
|
||||
/** Areas of the settings page. `anchors`: section ids (and the area key itself) that open this area, so old deep links
|
||||
* like /settings#opnexus-update or #cleanup keep working. */
|
||||
const CATEGORIES: { key: CategoryKey; icon: typeof User; anchors: string[]; adminOnly?: boolean }[] = [
|
||||
{ key: "account", icon: User, anchors: ["account", "appearance", "language", "password", "two-factor"] },
|
||||
{ key: "notifications", icon: Bell, anchors: ["notifications", "channels", "system-integrations"] },
|
||||
{ key: "access", icon: KeyRound, anchors: ["access", "system-security", "sso", "duties", "gui-access"] },
|
||||
{ key: "operation", icon: SlidersHorizontal, anchors: ["operation", "system-operation", "system-monitoring", "logs", "cleanup"] },
|
||||
{ key: "data", icon: HardDrive, anchors: ["data", "backups", "audit-export"], adminOnly: true },
|
||||
{ key: "system", icon: Info, anchors: ["system", "opnexus-update", "setup", "about"] },
|
||||
];
|
||||
|
||||
function categoryFromHash(hash: string): { category: CategoryKey; anchor: string | null } | null {
|
||||
const id = hash.replace(/^#/, "");
|
||||
if (!id) return null;
|
||||
const found = CATEGORIES.find((c) => c.anchors.includes(id));
|
||||
return found ? { category: found.key, anchor: id === found.key ? null : id } : null;
|
||||
}
|
||||
|
||||
export default function SettingsPage(){
|
||||
const {theme,setTheme}=useTheme();const [settings,setSettings]=useState<AppSettings|null>(null);
|
||||
const {language,setLanguage,t,errorMessage}=useLanguage();
|
||||
const notAdmin=useNotAdmin();
|
||||
const [error,setError]=useState('');const [notice,setNotice]=useState('');const [saving,setSaving]=useState(false);
|
||||
const [current,setCurrent]=useState('');const [password,setPassword]=useState('');const [repeat,setRepeat]=useState('');
|
||||
const [active,setActive]=useState<CategoryKey>("account");
|
||||
const categories=CATEGORIES.filter((c)=>!(c.adminOnly && notAdmin));
|
||||
useEffect(()=>{api.settings().then(setSettings).catch(e=>setError(errorMessage(e)));},[errorMessage]);
|
||||
|
||||
// Follow the URL hash: an area key or the id of a section inside one (deep links from banner, wizard, cleanup tab).
|
||||
useEffect(()=>{
|
||||
let tries=0; let timer: ReturnType<typeof setTimeout>|undefined;
|
||||
function apply(){
|
||||
const target=categoryFromHash(window.location.hash);
|
||||
if(!target) return;
|
||||
setActive(target.category);
|
||||
if(!target.anchor) return;
|
||||
tries=0;
|
||||
const scroll=()=>{ // sections load their data first and appear only then
|
||||
const el=document.getElementById(target.anchor as string);
|
||||
if(el){el.scrollIntoView({block:"start"});return;}
|
||||
if(++tries<25) timer=setTimeout(scroll,200);
|
||||
};
|
||||
timer=setTimeout(scroll,0);
|
||||
}
|
||||
apply();
|
||||
window.addEventListener("hashchange",apply);
|
||||
return ()=>{window.removeEventListener("hashchange",apply); if(timer) clearTimeout(timer);};
|
||||
},[]);
|
||||
|
||||
function select(key: CategoryKey){
|
||||
setActive(key); setError(''); setNotice('');
|
||||
window.history.replaceState(null,"",`#${key}`);
|
||||
window.scrollTo({top:0});
|
||||
}
|
||||
|
||||
async function saveTheme(value:Theme){setSaving(true);setError('');setNotice('');const old=theme;setTheme(value);try{setSettings(await api.saveSettings({theme:value,language}));setNotice(t('settings.appearanceSaved'));}catch(e){setTheme(old);setError(errorMessage(e));}finally{setSaving(false);}}
|
||||
async function saveLanguage(value:"de"|"en"){setSaving(true);setError('');setNotice('');const old=language;setLanguage(value);try{setSettings(await api.saveSettings({theme,language:value}));setNotice(translate(value,'settings.language.saved'));}catch(e){setLanguage(old);setError(errorMessage(e));}finally{setSaving(false);}}
|
||||
async function changePassword(e:React.FormEvent){e.preventDefault();setError('');setNotice('');if(password!==repeat){setError(t('settings.passwordMismatch'));return;}setSaving(true);try{await api.changePassword(current,password);setCurrent('');setPassword('');setRepeat('');setNotice(t('settings.passwordChanged'));}catch(e){setError(errorMessage(e,'settings.passwordFailed'));}finally{setSaving(false);}}
|
||||
return <div className="mx-auto max-w-3xl space-y-6"><h1 className="text-2xl font-semibold">{t("settings.title")}</h1>{error && <p role="alert" className="rounded-lg bg-red-50 p-3 text-sm text-red-700">{error}</p>}{notice && <p role="status" className="text-sm text-emerald-700">{notice}</p>}<section className="space-y-4 rounded-xl border border-slate-200 bg-white p-5"><h2 className="flex items-center gap-2 text-lg font-medium">{t('settings.appearance')}<InfoTip text={t('settings.appearanceHint')} /></h2><div role="radiogroup" aria-label={t('settings.colorScheme')} className="flex flex-wrap gap-3">{(['light','dark','system'] as const).map((v)=><label key={v} className={`flex cursor-pointer items-center gap-2 rounded-lg border px-4 py-3 text-sm ${theme===v?'border-sky-500 bg-sky-50 text-sky-700':'border-slate-200'}`}><input type="radio" name="theme" checked={theme===v} disabled={saving || !settings} onChange={()=>saveTheme(v)}/>{t(`settings.theme.${v}`)}</label>)}</div></section><section className="space-y-4 rounded-xl border border-slate-200 bg-white p-5"><h2 className="text-lg font-medium">{t('settings.language.label')}</h2><div role="radiogroup" aria-label={t('settings.language.label')} className="flex flex-wrap gap-3">{(['de','en'] as const).map(value=><label key={value} className={`flex cursor-pointer items-center gap-2 rounded-lg border px-4 py-3 text-sm ${language===value?'border-sky-500 bg-sky-50 text-sky-700':'border-slate-200'}`}><input type="radio" name="language" checked={language===value} disabled={saving || !settings} onChange={()=>saveLanguage(value)}/>{t(`settings.language.${value}`)}</label>)}</div></section><section className="space-y-4 rounded-xl border border-slate-200 bg-white p-5"><h2 className="text-lg font-medium">{t('settings.account')} {settings?.username ? `· ${settings.username}`:''}</h2><form onSubmit={changePassword} className="space-y-4"><label className="block text-sm"><span className="mb-1 block font-medium text-slate-700">{t('settings.currentPassword')}</span><Input aria-label={t('settings.currentPassword')} type="password" autoComplete="current-password" required value={current} onChange={e=>setCurrent(e.target.value)}/></label><label className="block text-sm"><span className="mb-1 block font-medium text-slate-700">{t('settings.newPassword')}</span><Input aria-label={t('settings.newPassword')} type="password" autoComplete="new-password" minLength={10} maxLength={1024} required value={password} onChange={e=>setPassword(e.target.value)}/></label><label className="block text-sm"><span className="mb-1 block font-medium text-slate-700">{t('settings.repeatPassword')}</span><Input aria-label={t('settings.repeatPassword')} type="password" autoComplete="new-password" minLength={10} required value={repeat} onChange={e=>setRepeat(e.target.value)}/></label><Button type="submit" disabled={saving}>{t('settings.changePassword')}</Button></form></section><SystemSettingsSection/><SetupAssistantSection/><TwoFactorSection/><NotificationsSection/><LogIngestSection/><OidcSection/><CleanupSettings/><BackupSettingsSection/><AuditExportSection/><GuiAccessSection/><OpnexusUpdateSection/><section className="space-y-2 rounded-xl border border-slate-200 bg-white p-5"><h2 className="flex items-center gap-2 text-lg font-medium">{t('settings.about')}<InfoTip text={t('settings.changelogNote',{file:'CHANGELOG.md'})} /></h2><dl className="grid grid-cols-2 gap-3 text-sm"><dt>{t('settings.version')}</dt><dd>{APP_VERSION} {language==='en'?'“':'„'}{APP_CODENAME}{language==='en'?'”':'“'}</dd><dt>{t('settings.development')}</dt><dd>{APP_MAINTAINER}</dd></dl></section></div>;
|
||||
|
||||
const account = (
|
||||
<>
|
||||
<section id="appearance" className="scroll-mt-4 space-y-4 rounded-xl border border-slate-200 bg-white p-5">
|
||||
<h2 className="flex items-center gap-2 text-lg font-medium">{t('settings.appearance')}<InfoTip text={t('settings.appearanceHint')} /></h2>
|
||||
<div role="radiogroup" aria-label={t('settings.colorScheme')} className="flex flex-wrap gap-3">
|
||||
{(['light','dark','system'] as const).map((v)=>(
|
||||
<label key={v} className={`flex cursor-pointer items-center gap-2 rounded-lg border px-4 py-3 text-sm ${theme===v?'border-sky-500 bg-sky-50 text-sky-700':'border-slate-200'}`}>
|
||||
<input type="radio" name="theme" checked={theme===v} disabled={saving || !settings} onChange={()=>saveTheme(v)}/>{t(`settings.theme.${v}`)}
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
<section id="language" className="scroll-mt-4 space-y-4 rounded-xl border border-slate-200 bg-white p-5">
|
||||
<h2 className="text-lg font-medium">{t('settings.language.label')}</h2>
|
||||
<div role="radiogroup" aria-label={t('settings.language.label')} className="flex flex-wrap gap-3">
|
||||
{(['de','en'] as const).map(value=>(
|
||||
<label key={value} className={`flex cursor-pointer items-center gap-2 rounded-lg border px-4 py-3 text-sm ${language===value?'border-sky-500 bg-sky-50 text-sky-700':'border-slate-200'}`}>
|
||||
<input type="radio" name="language" checked={language===value} disabled={saving || !settings} onChange={()=>saveLanguage(value)}/>{t(`settings.language.${value}`)}
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
<section id="password" className="scroll-mt-4 space-y-4 rounded-xl border border-slate-200 bg-white p-5">
|
||||
<h2 className="text-lg font-medium">{t('settings.account')} {settings?.username ? `· ${settings.username}`:''}</h2>
|
||||
<form onSubmit={changePassword} className="space-y-4">
|
||||
<label className="block text-sm"><span className="mb-1 block font-medium text-slate-700">{t('settings.currentPassword')}</span><Input aria-label={t('settings.currentPassword')} type="password" autoComplete="current-password" required value={current} onChange={e=>setCurrent(e.target.value)}/></label>
|
||||
<label className="block text-sm"><span className="mb-1 block font-medium text-slate-700">{t('settings.newPassword')}</span><Input aria-label={t('settings.newPassword')} type="password" autoComplete="new-password" minLength={10} maxLength={1024} required value={password} onChange={e=>setPassword(e.target.value)}/></label>
|
||||
<label className="block text-sm"><span className="mb-1 block font-medium text-slate-700">{t('settings.repeatPassword')}</span><Input aria-label={t('settings.repeatPassword')} type="password" autoComplete="new-password" minLength={10} required value={repeat} onChange={e=>setRepeat(e.target.value)}/></label>
|
||||
<Button type="submit" disabled={saving}>{t('settings.changePassword')}</Button>
|
||||
</form>
|
||||
</section>
|
||||
<div id="two-factor" className="scroll-mt-4"><TwoFactorSection/></div>
|
||||
</>
|
||||
);
|
||||
|
||||
const about = (
|
||||
<section id="about" className="scroll-mt-4 space-y-2 rounded-xl border border-slate-200 bg-white p-5">
|
||||
<h2 className="flex items-center gap-2 text-lg font-medium">{t('settings.about')}<InfoTip text={t('settings.changelogNote',{file:'CHANGELOG.md'})} /></h2>
|
||||
<dl className="grid grid-cols-2 gap-3 text-sm"><dt>{t('settings.version')}</dt><dd>{APP_VERSION} {language==='en'?'“':'„'}{APP_CODENAME}{language==='en'?'”':'“'}</dd><dt>{t('settings.development')}</dt><dd>{APP_MAINTAINER}</dd></dl>
|
||||
</section>
|
||||
);
|
||||
|
||||
const content: Record<CategoryKey, React.ReactNode> = {
|
||||
account,
|
||||
notifications: <><NotificationsSection/><SystemSettingsSection groups={["integrations"]}/></>,
|
||||
access: <><SystemSettingsSection groups={["security"]}/><OidcSection/><GuiAccessSection/></>,
|
||||
operation: <><SystemSettingsSection groups={["operation","monitoring"]}/><div id="logs" className="scroll-mt-4"><LogIngestSection/></div><CleanupSettings/></>,
|
||||
data: <><BackupSettingsSection/><AuditExportSection/></>,
|
||||
system: <><OpnexusUpdateSection/><div id="setup" className="scroll-mt-4"><SetupAssistantSection/></div>{about}</>,
|
||||
};
|
||||
const shown = categories.some((c)=>c.key===active) ? active : "account";
|
||||
|
||||
return (
|
||||
<div className="mx-auto max-w-5xl">
|
||||
<h1 className="mb-4 text-2xl font-semibold">{t("settings.title")}</h1>
|
||||
<div className="lg:grid lg:grid-cols-[13rem_minmax(0,1fr)] lg:gap-6">
|
||||
<nav aria-label={t("settings.nav.label")} className="-mx-4 mb-4 overflow-x-auto px-4 lg:mx-0 lg:mb-0 lg:overflow-visible lg:px-0">
|
||||
<ul className="flex gap-2 lg:sticky lg:top-4 lg:flex-col lg:gap-1">
|
||||
{categories.map(({key,icon:Icon})=>(
|
||||
<li key={key} className="shrink-0">
|
||||
<button
|
||||
type="button"
|
||||
onClick={()=>select(key)}
|
||||
aria-current={shown===key ? "page" : undefined}
|
||||
className={`flex w-full items-center gap-2 whitespace-nowrap rounded-lg px-3 py-2 text-left text-sm font-medium transition-colors ${shown===key ? "bg-brand-50 text-brand-600" : "text-slate-600 hover:bg-slate-100 hover:text-slate-900"}`}
|
||||
>
|
||||
<Icon size={16} aria-hidden="true" className="shrink-0" />
|
||||
<span>{t(`settings.nav.${key}`)}</span>
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</nav>
|
||||
<div className="min-w-0 space-y-6">
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-slate-900">{t(`settings.nav.${shown}`)}</h2>
|
||||
<p className="text-sm text-slate-500">{t(`settings.nav.${shown}Hint`)}</p>
|
||||
</div>
|
||||
{error && <p role="alert" className="rounded-lg bg-red-50 p-3 text-sm text-red-700">{error}</p>}
|
||||
{notice && <p role="status" className="text-sm text-emerald-700">{notice}</p>}
|
||||
{content[shown]}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -23,8 +23,9 @@ function draftOf(items: SystemSetting[]): Draft {
|
||||
return draft;
|
||||
}
|
||||
|
||||
/** Settings › Betrieb: everything that used to need the server's .env and is safe to change while OPNexus runs. */
|
||||
export function SystemSettingsSection() {
|
||||
/** Settings that used to need the server's .env and are safe to change while OPNexus runs (Settings › several areas).
|
||||
* `groups` picks which groups this instance shows, each as its own card. */
|
||||
export function SystemSettingsSection({ groups = GROUPS }: { groups?: readonly (typeof GROUPS)[number][] }) {
|
||||
const { t, errorMessage } = useLanguage();
|
||||
const toast = useToast();
|
||||
const readOnly = useNotAdmin();
|
||||
@@ -186,37 +187,36 @@ export function SystemSettingsSection() {
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="space-y-4 rounded-xl border border-slate-200 bg-white p-5">
|
||||
<h2 className="flex items-center gap-2 text-lg font-medium">
|
||||
{t("settings.operational")}
|
||||
<InfoTip text={t("settings.system.intro")} />
|
||||
</h2>
|
||||
{!items && !error && <p className="text-sm text-slate-500">{t("common.loading")}</p>}
|
||||
<>
|
||||
{!items && !error && (
|
||||
<section className="rounded-xl border border-slate-200 bg-white p-5 text-sm text-slate-500">{t("common.loading")}</section>
|
||||
)}
|
||||
{items &&
|
||||
GROUPS.filter((g) => byGroup[g]?.length).map((group) => {
|
||||
groups.filter((g) => byGroup[g]?.length).map((group) => {
|
||||
const dirty = (byGroup[group] ?? []).some(changed);
|
||||
return (
|
||||
<form
|
||||
key={group}
|
||||
className="space-y-3 rounded-lg border border-slate-100 p-4"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
saveGroup(group);
|
||||
}}
|
||||
>
|
||||
<p className="flex items-center gap-2 text-sm font-medium">
|
||||
{t(`settings.system.group.${group}`)}
|
||||
<InfoTip text={t(`settings.system.groupHint.${group}`)} />
|
||||
</p>
|
||||
<div className="grid gap-4 sm:grid-cols-2">{(byGroup[group] ?? []).map(field)}</div>
|
||||
{!readOnly && (
|
||||
<div className="flex justify-end border-t border-slate-100 pt-3">
|
||||
<Button type="submit" disabled={!dirty || saving !== null}>
|
||||
{t("common.save")}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</form>
|
||||
<section key={group} id={`system-${group}`} className="scroll-mt-4 rounded-xl border border-slate-200 bg-white p-5">
|
||||
<form
|
||||
className="space-y-4"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
saveGroup(group);
|
||||
}}
|
||||
>
|
||||
<h2 className="flex items-center gap-2 text-lg font-medium">
|
||||
{t(`settings.system.group.${group}`)}
|
||||
<InfoTip text={`${t(`settings.system.groupHint.${group}`)} ${t("settings.system.intro")}`} />
|
||||
</h2>
|
||||
<div className="grid gap-4 sm:grid-cols-2">{(byGroup[group] ?? []).map(field)}</div>
|
||||
{!readOnly && (
|
||||
<div className="flex justify-end border-t border-slate-100 pt-4">
|
||||
<Button type="submit" disabled={!dirty || saving !== null}>
|
||||
{t("common.save")}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</form>
|
||||
</section>
|
||||
);
|
||||
})}
|
||||
{error && (
|
||||
@@ -224,6 +224,6 @@ export function SystemSettingsSection() {
|
||||
{error}
|
||||
</p>
|
||||
)}
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -340,6 +340,21 @@
|
||||
"hint": "Wie lange OPNexus auf den Mailserver wartet."
|
||||
}
|
||||
}
|
||||
},
|
||||
"nav": {
|
||||
"label": "Bereiche der Einstellungen",
|
||||
"account": "Mein Konto",
|
||||
"accountHint": "Darstellung, Sprache, Passwort und Zwei-Faktor-Anmeldung für dein eigenes Konto.",
|
||||
"notifications": "Benachrichtigungen",
|
||||
"notificationsHint": "Welche Ereignisse melden, über welche Kanäle, ab welchen Schwellen. Dazu der Heartbeat für einen Überwacher von außen und die Mail-CA.",
|
||||
"access": "Anmeldung & Zugriff",
|
||||
"accessHint": "Sitzungsdauer und Schutz vor erratenen Passwörtern, Anmeldung über SSO, Aufgabentrennung und der Fernzugriff auf die Firewall-Oberfläche.",
|
||||
"operation": "Betrieb",
|
||||
"operationHint": "Sicherheitsnetz beim Ausrollen, Messintervalle und Aufbewahrung, Log-Erfassung und Regel-Bereinigung.",
|
||||
"data": "Sicherung & Export",
|
||||
"dataHint": "Geplante Sicherungen der Firewall-Konfigurationen (auch offsite) und die Weitergabe des Audit-Logs an Syslog oder Webhook.",
|
||||
"system": "System",
|
||||
"systemHint": "OPNexus aktualisieren, den Einrichtungsassistenten erneut öffnen und Versionsinformationen."
|
||||
}
|
||||
},
|
||||
"nav": {
|
||||
|
||||
@@ -340,6 +340,21 @@
|
||||
"hint": "How long OPNexus waits for the mail server."
|
||||
}
|
||||
}
|
||||
},
|
||||
"nav": {
|
||||
"label": "Settings areas",
|
||||
"account": "My account",
|
||||
"accountHint": "Appearance, language, password and two-factor sign-in for your own account.",
|
||||
"notifications": "Notifications",
|
||||
"notificationsHint": "Which events to report, through which channels, from which thresholds. Plus the heartbeat for an external watcher and the mail CA.",
|
||||
"access": "Sign-in & access",
|
||||
"accessHint": "Session lifetime and protection against guessed passwords, SSO sign-in, separation of duties and remote access to the firewall web interface.",
|
||||
"operation": "Operation",
|
||||
"operationHint": "The safety net when rolling out, sampling intervals and retention, log collection and rule cleanup.",
|
||||
"data": "Backup & export",
|
||||
"dataHint": "Scheduled backups of the firewall configurations (offsite too) and forwarding the audit log to syslog or a webhook.",
|
||||
"system": "System",
|
||||
"systemHint": "Update OPNexus, reopen the setup assistant and version information."
|
||||
}
|
||||
},
|
||||
"nav": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Manually bumped whenever a notable change ships — mirrors the dated
|
||||
// entries already kept in the project README/CHANGELOG.md, no build
|
||||
// tooling involved. Versioning scheme: see CHANGELOG.md ("Versionierung").
|
||||
export const APP_VERSION = "1.34.0";
|
||||
export const APP_VERSION = "1.34.1";
|
||||
export const APP_CODENAME = "Griffon";
|
||||
export const APP_MAINTAINER = "Julian Boldt";
|
||||
Binary file not shown.
@@ -495,6 +495,11 @@ COMPOSE_PREV_SHA=""
|
||||
refresh_compose() {
|
||||
local target="$1" repo ref digest tmp keep=0 cur_sha rec_sha label
|
||||
[ "${OPNEXUS_NO_COMPOSE_REFRESH:-}" != 1 ] || return 0
|
||||
# Ein Git-Checkout im Release-Modus bekommt die Compose-Datei per `git pull`; sie hier zu ersetzen, änderte eine
|
||||
# versionierte Datei.
|
||||
if git rev-parse --is-inside-work-tree >/dev/null 2>&1 && git ls-files --error-unmatch docker-compose.yml >/dev/null 2>&1; then
|
||||
say "Compose-Datei nicht abgeglichen (Git-Checkout: sie kommt mit 'git pull')."; return 0
|
||||
fi
|
||||
repo=$(env_get OPNEXUS_IMAGE_REPO)
|
||||
[ -n "$repo" ] || return 0
|
||||
if ! command -v cosign >/dev/null 2>&1 || [ ! -f cosign.pub ]; then
|
||||
|
||||
@@ -139,6 +139,14 @@ class ComposeRefresh(unittest.TestCase):
|
||||
self.assertEqual(self.compose(), OLD)
|
||||
self.assertEqual(self.recorded(), _sha(OLD))
|
||||
|
||||
def test_a_git_checkout_keeps_its_versioned_compose_file(self):
|
||||
subprocess.run(["git", "init", "-q", str(self.tmp)], check=True)
|
||||
subprocess.run(["git", "-C", str(self.tmp), "add", "docker-compose.yml"], check=True)
|
||||
r = self.refresh()
|
||||
self.assertEqual(self.compose(), OLD)
|
||||
self.assertIn("Git-Checkout", r.stdout)
|
||||
self.assertFalse(self.log.exists()) # not even pulled
|
||||
|
||||
def test_rollback_without_a_swapped_file_does_nothing(self):
|
||||
r = self.run_body('prev_compose="" restore_compose; echo DONE')
|
||||
self.assertIn("DONE", r.stdout)
|
||||
|
||||
Reference in new issue
Block a user