Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
331 changes: 331 additions & 0 deletions package-lock.json

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"version": "0.0.0",
"scripts": {
"dev": "vite --host '0.0.0.0'",
"build": "node prebundle.js && vite build",
"build": "node prebundle.js && vite build",
"preview": "vite preview --port 5050",
"lint": "npx eslint \"./**\"",
"lintfix": "npx eslint \"./**\" --fix",
Expand All @@ -30,6 +30,7 @@
"pinia": "^2.0.13",
"prismjs": "^1.29.0",
"pug": "^3.0.2",
"uuid": "^13.0.0",
"v-network-graph": "^0.9.8",
"vite": "^2.8.4",
"vue": "^3.2.31",
Expand Down
47 changes: 39 additions & 8 deletions prebundle.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,48 @@
const fs = require('fs-extra');
const yaml = require('js-yaml');

console.log('Copying all plugin GUI source files to magma');
console.log('Copying plugin GUI source files to magma');

if (fs.existsSync('./src/plugins/')) fs.removeSync('./src/plugins/');
if (fs.existsSync('./src/plugins/')) {
fs.removeSync('./src/plugins/');
}

const plugins = fs.readdirSync('../')
plugins.forEach((plugin) => {
// Check to see if gui directory exists
const requestedPlugins = process.argv.slice(2);

let plugins = [];

if (requestedPlugins.length > 0) {
// runtime targeted build
plugins = requestedPlugins;
console.log("Building selected plugins:", plugins.join(', '));

} else {
// server --build mode
try {
const conf = yaml.load(
fs.readFileSync('../../conf/default.yml', 'utf8')
);

plugins = conf.plugins || [];
console.log("Building enabled plugins from config:", plugins.join(', '));

} catch (e) {
// fallback for dev mode
plugins = fs.readdirSync('../');
console.log("Building ALL plugins (fallback)");
}
}

plugins.forEach(plugin => {
if (!fs.existsSync(`../${plugin}/gui`)) return;

// Copy contents of gui directory to src directory
console.log(`Copying over "${plugin}" files...`)
fs.copySync(`../${plugin}/gui/`, `./src/plugins/${plugin}`, { overwrite: true, recursive: true })
console.log(`Copying "${plugin}" files...`);

fs.copySync(
`../${plugin}/gui/`,
`./src/plugins/${plugin}`,
{ overwrite: true, recursive: true }
);
});

console.log('Plugin GUI source files copied!');
164 changes: 151 additions & 13 deletions src/App.vue
Original file line number Diff line number Diff line change
@@ -1,34 +1,149 @@
<script setup>
import { onMounted } from "vue";
import { onMounted, watch, inject, ref, onUnmounted } from "vue";
import { useRoute } from "vue-router";

import { storeToRefs } from "pinia";
import { useCoreStore } from "@/stores/coreStore";
import { useCoreDisplayStore } from "@/stores/coreDisplayStore";
import { useAuthStore } from "@/stores/authStore";

import Navigation from "@/components/core/Navigation.vue";
import PageTabs from "@/components/core/PageTabs.vue";

const route = useRoute();
const authStore = useAuthStore();
const coreStore = useCoreStore();
const coreDisplayStore = useCoreDisplayStore();

const { restarting } = storeToRefs(coreDisplayStore);

const $api = inject("$api");

const restartMessage = ref("Caldera is restarting…");
const showSpinner = ref(true);

let buildPollTimer = null;
let healthPollTimer = null;

function forceReconnect() {
window.location.reload();
}

onMounted(() => {
coreStore.getUserSettings();
})
</script>
coreStore.getUserSettings();
});

watch(
() => route.name,
async (name) => {
if (!$api) return;
if (name === "login") return;
if (!authStore.isUserAuthenticated) return;

await coreStore.getAvailablePlugins($api);
await coreStore.getMainConfig($api);
await authStore.getGroup($api);
},
{ immediate: true }
);

watch(restarting, (value) => {
if (!value) {
clearInterval(buildPollTimer);
clearInterval(healthPollTimer);
return;
}

startBuildPolling();
});

function startBuildPolling() {
clearInterval(buildPollTimer);

buildPollTimer = setInterval(async () => {
try {
const res = await $api.get("/api/v2/plugins/build-status");
const state = res.data;

switch (state.status) {
case "installing":
restartMessage.value =
`Installing dependencies for ${state.plugin}…`;
break;

case "building":
restartMessage.value =
`Building plugin interface for ${state.plugin}…`;
break;

case "restarting":
restartMessage.value = "Restarting Caldera…";
clearInterval(buildPollTimer);
startHealthPolling();
break;

default:
restartMessage.value = "Preparing plugin…";
}
} catch {
// backend may be restarting
}
}, 1000);
}

function startHealthPolling() {
clearInterval(healthPollTimer);

healthPollTimer = setInterval(async () => {
try {
await $api.get("/api/v2/health", {
params: { t: Date.now() },
validateStatus: () => true
});

clearInterval(healthPollTimer);
window.location.reload();
} catch {
// still restarting
}
}, 2000);
}

onUnmounted(() => {
clearInterval(buildPollTimer);
clearInterval(healthPollTimer);
});
</script>
<template lang="pug">
//- GLOBAL restart overlay (ALWAYS FIRST)
.restart-overlay(v-if="restarting")
.restart-box
span.icon.is-large(v-if="showSpinner")
font-awesome-icon(icon="spinner" spin)

p.mt-3.mb-4.build-title Building Updated Plugin Interface…
p(style="white-space: pre-line") {{ restartMessage }}

button.button.is-primary.mt-4(
v-if="showReconnect"
@click="forceReconnect"
) Reconnect
Comment on lines +123 to +129
Copy link

Copilot AI Feb 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

showReconnect is referenced in the template but isn’t defined in <script setup>, so the overlay’s reconnect button can never appear (and Vue will warn about an undefined binding). Either define showReconnect (e.g., after a timeout / on repeated polling failures) or remove the conditional/button.

Copilot uses AI. Check for mistakes.

//- Login page
.is-fullwidth(v-if="route.name === 'login'")
router-view
router-view

//- All other pages
.is-flex.is-flex-direction-row(v-else style="min-height: 100vh;")
Suspense
Navigation
.is-flex.is-flex-direction-row(
v-else
style="min-height: 100vh;"
)
Navigation

main
PageTabs
main
PageTabs

.p-4#router
router-view
.p-4#router
router-view
</template>

<style scoped>
Expand Down Expand Up @@ -90,4 +205,27 @@ main {
margin: 0 20px 0 10px;
background-color: grey;
}
.restart-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(10,10,10,0.85);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
color: white;
}

.restart-box {
text-align: center;
font-size: 1.2rem;
}
.build-title {
font-size: 1.6rem;
font-weight: 600;
}

</style>
Loading
Loading