feat(interfaces): add restart required handling and RNS reload functionality across interface components

This commit is contained in:
2026-01-02 19:40:12 -06:00
parent d52849e832
commit 3620643b92
4 changed files with 152 additions and 19 deletions

View File

@@ -1479,6 +1479,9 @@ export default {
// go to interfaces page
this.$router.push({
name: "interfaces",
query: {
restart_required: this.newInterfaceName,
},
});
} catch (e) {
const message = e.response?.data?.message ?? "failed to add interface";

View File

@@ -1,6 +1,37 @@
<template>
<div class="interface-card">
<div class="flex gap-4 items-start">
<div
class="interface-card transition-all duration-300"
:class="{
'opacity-60 grayscale-[0.5]': !isInterfaceEnabled(iface) || iface._restart_required || !isReticulumRunning,
}"
>
<div class="flex gap-4 items-start relative">
<!-- Offline Overlay -->
<div
v-if="!isReticulumRunning"
class="absolute inset-0 z-10 flex items-center justify-center bg-white/40 dark:bg-zinc-900/40 backdrop-blur-[1px] rounded-3xl"
>
<div
class="bg-red-500 text-white px-4 py-2 rounded-full shadow-lg flex items-center gap-2 text-sm font-bold"
>
<MaterialDesignIcon icon-name="lan-disconnect" class="w-4 h-4" />
<span>Reticulum Offline</span>
</div>
</div>
<!-- Restart Required Overlay -->
<div
v-if="isReticulumRunning && iface._restart_required"
class="absolute inset-0 z-10 flex items-center justify-center bg-white/40 dark:bg-zinc-900/40 backdrop-blur-[1px] rounded-3xl"
>
<div
class="bg-amber-500 text-white px-4 py-2 rounded-full shadow-lg flex items-center gap-2 text-sm font-bold animate-pulse"
>
<MaterialDesignIcon icon-name="restart" class="w-4 h-4" />
<span>{{ $t("interfaces.restart_required") }}</span>
</div>
</div>
<div class="interface-card__icon">
<MaterialDesignIcon :icon-name="iconName" class="w-6 h-6" />
</div>
@@ -148,6 +179,10 @@ export default {
type: Object,
required: true,
},
isReticulumRunning: {
type: Boolean,
default: true,
},
},
emits: ["enable", "disable", "edit", "export", "delete"],
data() {

View File

@@ -49,6 +49,19 @@
<MaterialDesignIcon icon-name="export" class="w-4 h-4" />
{{ $t("interfaces.export_all") }}
</button>
<button
type="button"
class="secondary-chip text-sm bg-amber-500/10 hover:bg-amber-500/20 text-amber-600 dark:text-amber-400 border-amber-500/30"
:disabled="reloadingRns"
@click="reloadRns"
>
<MaterialDesignIcon
:icon-name="reloadingRns ? 'refresh' : 'restart'"
class="w-4 h-4"
:class="{ 'animate-spin': reloadingRns }"
/>
{{ reloadingRns ? $t("app.reloading_rns") : $t("app.reload_rns") }}
</button>
</div>
</div>
<div class="flex flex-wrap gap-3 items-center">
@@ -106,6 +119,7 @@
v-for="iface of filteredInterfaces"
:key="iface._name"
:iface="iface"
:is-reticulum-running="isReticulumRunning"
@enable="enableInterface(iface._name)"
@disable="disableInterface(iface._name)"
@edit="editInterface(iface._name)"
@@ -127,6 +141,7 @@ import Utils from "../../js/Utils";
import ImportInterfacesModal from "./ImportInterfacesModal.vue";
import DownloadUtils from "../../js/DownloadUtils";
import MaterialDesignIcon from "../MaterialDesignIcon.vue";
import ToastUtils from "../../js/ToastUtils";
export default {
name: "InterfacesPage",
@@ -144,6 +159,9 @@ export default {
statusFilter: "all",
typeFilter: "all",
hasPendingInterfaceChanges: false,
reloadingRns: false,
modifiedInterfaceNames: new Set(),
isReticulumRunning: true,
};
},
computed: {
@@ -158,6 +176,7 @@ export default {
for (const [interfaceName, iface] of Object.entries(this.interfaces)) {
iface._name = interfaceName;
iface._stats = this.interfaceStats[interfaceName];
iface._restart_required = this.modifiedInterfaceNames.has(interfaceName);
results.push(iface);
}
return results;
@@ -216,6 +235,11 @@ export default {
this.loadInterfaces();
this.updateInterfaceStats();
// check if we have a restart required from adding an interface
if (this.$route.query.restart_required) {
this.trackInterfaceChange(this.$route.query.restart_required);
}
// update info every few seconds
this.reloadInterval = setInterval(() => {
this.updateInterfaceStats();
@@ -225,8 +249,11 @@ export default {
relaunch() {
ElectronUtils.relaunch();
},
trackInterfaceChange() {
trackInterfaceChange(interfaceName = null) {
this.hasPendingInterfaceChanges = true;
if (interfaceName) {
this.modifiedInterfaceNames.add(interfaceName);
}
},
isInterfaceEnabled: function (iface) {
return Utils.isInterfaceEnabled(iface);
@@ -235,6 +262,10 @@ export default {
try {
const response = await window.axios.get(`/api/v1/reticulum/interfaces`);
this.interfaces = response.data.interfaces;
// also check app info for running state
const appInfoResponse = await window.axios.get(`/api/v1/app/info`);
this.isReticulumRunning = appInfoResponse.data.app_info.is_reticulum_running;
} catch {
// do nothing if failed to load interfaces
}
@@ -259,7 +290,7 @@ export default {
await window.axios.post(`/api/v1/reticulum/interfaces/enable`, {
name: interfaceName,
});
this.trackInterfaceChange();
this.trackInterfaceChange(interfaceName);
} catch (e) {
DialogUtils.alert("failed to enable interface");
console.log(e);
@@ -274,7 +305,7 @@ export default {
await window.axios.post(`/api/v1/reticulum/interfaces/disable`, {
name: interfaceName,
});
this.trackInterfaceChange();
this.trackInterfaceChange(interfaceName);
} catch (e) {
DialogUtils.alert("failed to disable interface");
console.log(e);
@@ -304,7 +335,7 @@ export default {
await window.axios.post(`/api/v1/reticulum/interfaces/delete`, {
name: interfaceName,
});
this.trackInterfaceChange();
this.trackInterfaceChange(interfaceName);
} catch (e) {
DialogUtils.alert("failed to delete interface");
console.log(e);
@@ -357,6 +388,23 @@ export default {
filterChipClass(isActive) {
return isActive ? "primary-chip text-xs" : "secondary-chip text-xs";
},
async reloadRns() {
if (this.reloadingRns) return;
try {
this.reloadingRns = true;
const response = await window.axios.post("/api/v1/reticulum/reload");
ToastUtils.success(response.data.message);
this.hasPendingInterfaceChanges = false;
this.modifiedInterfaceNames.clear();
await this.loadInterfaces();
} catch (e) {
ToastUtils.error(e.response?.data?.error || "Failed to reload Reticulum!");
console.error(e);
} finally {
this.reloadingRns = false;
}
},
},
};
</script>

View File

@@ -105,9 +105,9 @@
</div>
<!-- settings grid -->
<div class="grid gap-4 lg:grid-cols-2">
<div class="columns-1 lg:columns-2 gap-4 space-y-4">
<!-- Page Archiver -->
<section class="glass-card">
<section class="glass-card break-inside-avoid">
<header class="glass-card__header">
<div>
<div class="glass-card__eyebrow">Browsing</div>
@@ -174,7 +174,7 @@
</section>
<!-- Smart Crawler -->
<section class="glass-card">
<section class="glass-card break-inside-avoid">
<header class="glass-card__header">
<div>
<div class="glass-card__eyebrow">Discovery</div>
@@ -249,7 +249,7 @@
</section>
<!-- Appearance -->
<section class="glass-card">
<section class="glass-card break-inside-avoid">
<header class="glass-card__header">
<div>
<div class="glass-card__eyebrow">Personalise</div>
@@ -277,7 +277,7 @@
</section>
<!-- Language -->
<section class="glass-card">
<section class="glass-card break-inside-avoid">
<header class="glass-card__header">
<div>
<div class="glass-card__eyebrow">i18n</div>
@@ -295,7 +295,7 @@
</section>
<!-- Transport -->
<section class="glass-card">
<section class="glass-card break-inside-avoid">
<header class="glass-card__header">
<div>
<div class="glass-card__eyebrow">Reticulum</div>
@@ -322,7 +322,7 @@
</section>
<!-- Interfaces -->
<section class="glass-card">
<section class="glass-card break-inside-avoid">
<header class="glass-card__header">
<div>
<div class="glass-card__eyebrow">Adapters</div>
@@ -348,7 +348,7 @@
</section>
<!-- Blocked -->
<section class="glass-card">
<section class="glass-card break-inside-avoid">
<header class="glass-card__header">
<div>
<div class="glass-card__eyebrow">Privacy</div>
@@ -366,7 +366,7 @@
</section>
<!-- Authentication -->
<section class="glass-card">
<section class="glass-card break-inside-avoid">
<header class="glass-card__header">
<div>
<div class="glass-card__eyebrow">Security</div>
@@ -398,7 +398,7 @@
</section>
<!-- Messages -->
<section class="glass-card">
<section class="glass-card break-inside-avoid">
<header class="glass-card__header">
<div>
<div class="glass-card__eyebrow">{{ $t("app.reliability") }}</div>
@@ -467,7 +467,7 @@
</section>
<!-- Propagation nodes -->
<section class="glass-card lg:col-span-2">
<section class="glass-card break-inside-avoid">
<header class="glass-card__header">
<div>
<div class="glass-card__eyebrow">LXMF</div>
@@ -566,8 +566,40 @@
</div>
</section>
<!-- Keyboard Shortcuts -->
<section class="glass-card lg:col-span-2">
<!-- System / RNS Reload -->
<section class="glass-card break-inside-avoid">
<header class="glass-card__header">
<div>
<div class="glass-card__eyebrow">{{ $t("app.system") }}</div>
<h2>{{ $t("app.reticulum_stack") }}</h2>
<p>{{ $t("app.reticulum_stack_description") }}</p>
</div>
</header>
<div class="glass-card__body space-y-4">
<div class="flex flex-col gap-3">
<button
class="btn btn--secondary w-full justify-center gap-2 py-3"
:disabled="reloadingRns"
@click="reloadRns"
>
<MaterialDesignIcon
:icon-name="reloadingRns ? 'refresh' : 'restart'"
class="w-5 h-5"
:class="{ 'animate-spin': reloadingRns }"
/>
<span>{{ reloadingRns ? $t("app.reloading_rns") : $t("app.reload_rns") }}</span>
</button>
<p class="text-xs text-gray-500 dark:text-gray-400">
{{ $t("app.reload_rns_description") }}
</p>
</div>
</div>
</section>
</div>
<!-- Keyboard Shortcuts (Full width at bottom) -->
<div class="mt-4">
<section class="glass-card">
<div class="glass-card__header">
<div class="flex items-center gap-3">
<div
@@ -642,6 +674,7 @@ export default {
},
saveTimeouts: {},
shortcuts: [],
reloadingRns: false,
};
},
beforeUnmount() {
@@ -941,6 +974,20 @@ export default {
}
}
},
async reloadRns() {
if (this.reloadingRns) return;
try {
this.reloadingRns = true;
const response = await window.axios.post("/api/v1/reticulum/reload");
ToastUtils.success(response.data.message);
} catch (e) {
ToastUtils.error(e.response?.data?.error || "Failed to reload Reticulum!");
console.error(e);
} finally {
this.reloadingRns = false;
}
},
formatSecondsAgo: function (seconds) {
return Utils.formatSecondsAgo(seconds);
},