feat(interfaces): add restart required handling and RNS reload functionality across interface components
This commit is contained in:
@@ -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";
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user