diff --git a/src/components/models/navigation/items/NavigationMenuItemsSuperUser.vue b/src/components/models/navigation/items/NavigationMenuItemsSuperUser.vue index 2b3375b2..8e98e2af 100644 --- a/src/components/models/navigation/items/NavigationMenuItemsSuperUser.vue +++ b/src/components/models/navigation/items/NavigationMenuItemsSuperUser.vue @@ -323,6 +323,7 @@ const items = computed(() => [ { label: t('superuser.nav.department_relays'), to: '/superuser/department/relays'}, { label: t('superuser.nav.scanners'), to: '/superuser/scanners' }, { label: t('superuser.nav.selfserve'), to: '/superuser/selfserve' }, + { label: 'Self-serve sessions', to: '/superuser/selfserve/sessions' }, ]}, // Configurations { label: t('superuser.nav.configuration'), type: 'category', children: [ diff --git a/src/components/session/token/superUser/modules/edgegateway/Config.vue b/src/components/session/token/superUser/modules/edgegateway/Config.vue index 8963a380..3573b361 100644 --- a/src/components/session/token/superUser/modules/edgegateway/Config.vue +++ b/src/components/session/token/superUser/modules/edgegateway/Config.vue @@ -55,6 +55,22 @@ export const Config = { get: async () => Config.get("default_update_window"), set: async (value) => Config.set("default_update_window", value), }, + broker_url: { + get: async () => Config.get("broker_url"), + set: async (value) => Config.set("broker_url", value), + }, + public_broker_url: { + get: async () => Config.get("public_broker_url"), + set: async (value) => Config.set("public_broker_url", value), + }, + broker_auth_mode: { + get: async () => Config.get("broker_auth_mode"), + set: async (value) => Config.set("broker_auth_mode", value), + }, + broker_shared_secret: { + get: async () => Config.get("broker_shared_secret"), + set: async (value) => Config.set("broker_shared_secret", value), + }, }, }; diff --git a/src/features/edgeGateways/EdgeGatewayLogsPage.vue b/src/features/edgeGateways/EdgeGatewayLogsPage.vue index ecc81648..04e896e5 100644 --- a/src/features/edgeGateways/EdgeGatewayLogsPage.vue +++ b/src/features/edgeGateways/EdgeGatewayLogsPage.vue @@ -10,6 +10,10 @@ const props = defineProps({ type: Array, default: () => [], }, + relayLogs: { + type: Array, + default: () => [], + }, streamStatus: { type: String, default: "idle", @@ -23,6 +27,73 @@ const props = defineProps({ const levelFilter = ref("ALL"); const typeFilter = ref("ALL"); +const entryContext = (entry = {}) => { + const context = entry?.context || entry?.entry?.context || {}; + return context && typeof context === "object" && !Array.isArray(context) ? context : {}; +}; + +const relayEntries = computed(() => { + if (props.relayLogs.length) { + return props.relayLogs; + } + + return props.timeline + .filter((entry) => String(entry?.type || "").toLowerCase() === "relay") + .map((entry) => entry?.entry || entry) + .filter(Boolean); +}); + +const relayModuleLabel = (entry = {}) => entryContext(entry).module_responsible || entryContext(entry).module || "edge_gateway"; + +const relayHandlerLabel = (entry = {}) => { + const context = entryContext(entry); + const handler = String(context.handler || context.execution_path || "local").toLowerCase(); + const channel = context.delivery_channel || context.execution?.channel || ""; + return `${handler}${channel ? ` / ${channel}` : ""}`; +}; + +const relayReasonLabel = (entry = {}) => entryContext(entry).reason || "Relay dispatch"; + +const relayActorLabel = (entry = {}) => { + const context = entryContext(entry); + const actor = context.actor && typeof context.actor === "object" ? context.actor : {}; + const associated = context.associated && typeof context.associated === "object" ? context.associated : {}; + const admin = associated.admin_user_id || actor.admin_user_id; + const customer = associated.customer_number || actor.customer_number; + const customerUser = associated.customer_user_id || actor.customer_user_id; + const subuser = associated.subuser_id || actor.subuser_id; + const parts = []; + + if (admin) parts.push(`Admin #${admin}`); + if (customer) parts.push(`Customer ${customer}`); + else if (customerUser) parts.push(`Customer user #${customerUser}`); + if (subuser) parts.push(`Subuser #${subuser}`); + + return parts.join(" / ") || "No actor"; +}; + +const relaySignalLabel = (entry = {}) => { + const context = entryContext(entry); + const signal = context.signal && typeof context.signal === "object" ? context.signal : {}; + const action = String(context.action || signal.command_type || "RELAY").toUpperCase(); + const relayId = context.relay_id || signal.relay_id || signal.request?.relayId || signal.request?.id || "unknown"; + const targetOn = Object.prototype.hasOwnProperty.call(context, "target_on") ? context.target_on : signal.request?.on; + const target = typeof targetOn === "boolean" ? (targetOn ? "ON" : "OFF") : ""; + return [action, relayId, target].filter(Boolean).join(" "); +}; + +const relayResponseLabel = (entry = {}) => { + const response = entryContext(entry).response || {}; + const parts = []; + if (Object.prototype.hasOwnProperty.call(response, "online")) { + parts.push(`online ${response.online ? "yes" : "no"}`); + } + if (Object.prototype.hasOwnProperty.call(response, "on")) { + parts.push(`state ${response.on ? "ON" : "OFF"}`); + } + return parts.join(" / ") || "No response payload"; +}; + const filteredTimeline = computed(() => props.timeline.filter((entry) => { if (levelFilter.value !== "ALL" && String(entry.level || "").toUpperCase() !== levelFilter.value) { @@ -51,6 +122,7 @@ const filteredTimeline = computed(() => + @@ -83,6 +155,40 @@ const filteredTimeline = computed(() => +
+

Relay dispatch

+

Signals and responses

+ +
+

Terminal sessions

Recent shell activity

@@ -158,4 +264,33 @@ const filteredTimeline = computed(() => margin: 0.35rem 0; color: #334155; } + +.edge-gateway-logs__relay-details { + margin: 0.75rem 0; + display: grid; + gap: 0.45rem; +} + +.edge-gateway-logs__relay-details div { + display: grid; + grid-template-columns: minmax(5rem, 0.35fr) 1fr; + gap: 0.75rem; +} + +.edge-gateway-logs__relay-details dt, +.edge-gateway-logs__relay-details dd { + margin: 0; + min-width: 0; + overflow-wrap: anywhere; +} + +.edge-gateway-logs__relay-details dt { + color: #64748b; + font-size: 0.78rem; + text-transform: uppercase; +} + +.edge-gateway-logs__relay-details dd { + color: #1f2937; +} diff --git a/src/features/edgeGateways/EdgeGatewayManager.vue b/src/features/edgeGateways/EdgeGatewayManager.vue index 68067a57..b9a402fb 100644 --- a/src/features/edgeGateways/EdgeGatewayManager.vue +++ b/src/features/edgeGateways/EdgeGatewayManager.vue @@ -573,7 +573,7 @@ const buildLocalGatewayTimeline = (gateway) => { entry, })); const logEntries = (Array.isArray(gateway.log_entries) ? gateway.log_entries : []).map((entry) => ({ - type: "log", + type: String(entry?.stream || "").toLowerCase() === "relay" ? "relay" : "log", level: entry?.level || "INFO", message: entry?.message || "", created_at: entry?.created_at || null, @@ -695,6 +695,18 @@ const logTimeline = computed(() => { return buildLocalGatewayTimeline(selectedGatewayView.value); }); const logShellSessions = computed(() => (Array.isArray(logsSnapshot.value?.shell_sessions) ? logsSnapshot.value.shell_sessions : [])); +const logRelayLogs = computed(() => { + if (Array.isArray(logsSnapshot.value?.relay_logs)) { + return logsSnapshot.value.relay_logs; + } + + const fallbackEntries = Array.isArray(logsSnapshot.value?.log_entries) + ? logsSnapshot.value.log_entries + : selectedGatewayView.value?.log_entries; + return (Array.isArray(fallbackEntries) ? fallbackEntries : []).filter( + (entry) => String(entry?.stream || "").toLowerCase() === "relay" + ); +}); const statisticsPageState = computed(() => { if (statisticsSnapshot.value && typeof statisticsSnapshot.value === "object" && Object.keys(statisticsSnapshot.value).length) { return statisticsSnapshot.value; @@ -2528,6 +2540,7 @@ onUnmounted(() => { export const rotateNumberPlateScannerKey = async (scannerId) => authenticatedRequest(`/numberplatescanners/${encodeURIComponent(String(scannerId))}/rotate-key`, "POST", {}); -export const getEdgeGatewayModuleConfig = async () => - authenticatedRequest(EDGE_GATEWAY_CONFIG_BASE, "GET", {}).then((response) => { +export const getEdgeGatewayModuleConfig = async (variable = null) => { + const endpoint = variable + ? `${EDGE_GATEWAY_CONFIG_BASE}?variable=${encodeURIComponent(String(variable))}` + : EDGE_GATEWAY_CONFIG_BASE; + + return authenticatedRequest(endpoint, "GET", {}).then((response) => { const entries = normalizeEntries(unwrapEdgeGatewayResponse(response, [])); response.data.data = entries; response.data.config = toConfigPayload(entries); return response; }); +}; export const setEdgeGatewayModuleConfig = async (payload) => authenticatedRequest(EDGE_GATEWAY_CONFIG_BASE, "POST", payload); diff --git a/src/views/dashboards/superUserDashboard/DepartmentSelfServe.vue b/src/views/dashboards/superUserDashboard/DepartmentSelfServe.vue index b26da3bc..531b40c5 100644 --- a/src/views/dashboards/superUserDashboard/DepartmentSelfServe.vue +++ b/src/views/dashboards/superUserDashboard/DepartmentSelfServe.vue @@ -4,6 +4,7 @@ import RestrictedPageWrapper from "@/components/page/wrappers/RestrictedPageWrap import { SessionUser } from "@/components/session/token/SessionUser.vue"; import DepartmentSelfServePagination from "@/components/displays/pagination/models/SuperUserDashboard/DepartmentSelfServePagination.vue"; import { useRouter } from "vue-router"; +import SelfServeSuperUserTabs from "@/views/dashboards/superUserDashboard/selfserve/SelfServeSuperUserTabs.vue"; const router = useRouter(); @@ -15,6 +16,7 @@ const openFleetLanding = async () => {