diff --git a/frontend/src/stores/auth.js b/frontend/src/stores/auth.js
index 2f0a96dd..9d8faaf5 100644
--- a/frontend/src/stores/auth.js
+++ b/frontend/src/stores/auth.js
@@ -75,13 +75,25 @@ export const useAuthStore = defineStore('auth', {
}
},
+ // Re-read the signed-in user from the server, permissions included.
+ //
+ // The login response is the ONLY thing that used to populate this, and it
+ // is cached in localStorage - so granting somebody a permission did nothing
+ // visible until they happened to log out and back in. They were told they
+ // had rights, the button stayed hidden, and nothing explained why. Called
+ // on app boot so a permission change lands on the next page load.
+ //
+ // Only a 401 logs out. A transient failure - API restarting, network blip -
+ // must not throw somebody out of a session they still hold; the cached user
+ // is stale, not invalid.
async fetchUser() {
try {
const response = await authApi.me()
this.user = response.data.data
await this.enrichFromDirectory()
+ localStorage.setItem('user', JSON.stringify(this.user))
} catch (error) {
- this.logout()
+ if (error.response?.status === 401) this.logout()
}
},
diff --git a/frontend/src/views/AppLayout.vue b/frontend/src/views/AppLayout.vue
index ebf4a747..2d37253a 100644
--- a/frontend/src/views/AppLayout.vue
+++ b/frontend/src/views/AppLayout.vue
@@ -189,14 +189,33 @@ const defaultNav = [
{ name: 'Notifications', icon: 'bell', route: '/notifications', position: 5 },
{ name: 'Calendar', icon: 'calendar', route: '/calendar', position: 6 },
{ name: 'Map', icon: 'map', route: '/map', position: 4 },
- { name: 'Machines', icon: 'cog', route: '/machines', position: 10 },
- { name: 'PCs', icon: 'desktop', route: '/pcs', position: 15 },
- { name: 'Network', icon: 'network-wired', route: '/network', position: 18 },
- { name: 'Printers', icon: 'printer', route: '/printers', position: 20 },
- { name: 'USB Devices', icon: 'usb', route: '/usb', position: 45 },
+ { name: 'Machines', icon: 'cog', route: '/machines', position: 10, section: 'assets' },
+ { name: 'PCs', icon: 'desktop', route: '/pcs', position: 15, section: 'assets' },
+ { name: 'Network', icon: 'network-wired', route: '/network', position: 18, section: 'assets' },
+ { name: 'Printers', icon: 'printer', route: '/printers', position: 20, section: 'assets' },
+ { name: 'USB Devices', icon: 'usb', route: '/usb', position: 25, section: 'assets' },
{ name: 'Applications', icon: 'app-window', route: '/applications', position: 30, section: 'information' },
- { name: 'Knowledge Base', icon: 'book-open', route: '/knowledgebase', position: 35 },
- { name: 'Reports', icon: 'bar-chart-3', route: '/reports', position: 40 },
+ { name: 'Knowledge Base', icon: 'book-open', route: '/knowledgebase', position: 35, section: 'information' },
+ { name: 'Reports', icon: 'bar-chart-3', route: '/reports', position: 40, section: 'information' },
+]
+
+// The canonical sidebar sections, in the order they render. A nav item DECLARES
+// which one it belongs to; nothing is inferred.
+//
+// Sections used to be derived from the position number - 10-29 became Assets,
+// 30-49 became Information - so a plugin landed in whichever bucket its number
+// happened to fall in. That is how USB Devices, an asset, came to sit under
+// Information next to GE-Enforce and Tech Tools, and why two plugins picking
+// the same number silently reordered each other. A number is for ORDER; it was
+// never a statement about meaning.
+//
+// An item with no section (or an unknown one) stays in the unlabelled group at
+// the top, which is the safe default: it appears, it is just not filed.
+const NAV_SECTIONS = [
+ { key: null, label: null },
+ { key: 'assets', label: 'Assets' },
+ { key: 'information', label: 'Information' },
+ { key: 'fleet', label: 'Fleet' },
]
function buildNavItems(items) {
@@ -206,29 +225,26 @@ function buildNavItems(items) {
const visible = items.filter(item =>
!item.permission || authStore.hasPermission(item.permission))
- // Sort by position
- const sorted = [...visible].sort((a, b) => (a.position || 99) - (b.position || 99))
+ const known = new Set(NAV_SECTIONS.map(s => s.key).filter(Boolean))
+ const bucket = item => {
+ const declared = (item.section || '').toLowerCase()
+ return known.has(declared) ? declared : null
+ }
- // Assign section headers based on position ranges
const result = []
- let currentSection = null
-
- for (const item of sorted) {
- let section = null
- if (item.position >= 10 && item.position < 30 && currentSection !== 'Assets') {
- section = 'Assets'
- currentSection = 'Assets'
- } else if (item.section === 'information' || (item.position >= 30 && item.position < 50 && currentSection !== 'Information')) {
- if (currentSection !== 'Information') {
- section = 'Information'
- currentSection = 'Information'
- }
- }
-
- result.push({
- ...item,
- section,
- iconComponent: iconMap[item.icon] || null
+ for (const { key, label } of NAV_SECTIONS) {
+ const inSection = visible
+ .filter(item => bucket(item) === key)
+ // ?? not ||: position 0 is a real position, and `0 || 99` is 99, which
+ // sent Dashboard - the only item that uses 0 - to the end of its group.
+ .sort((a, b) => (a.position ?? 99) - (b.position ?? 99))
+ inSection.forEach((item, index) => {
+ result.push({
+ ...item,
+ // Only the first item in a section carries its heading.
+ section: index === 0 ? label : null,
+ iconComponent: iconMap[item.icon] || null,
+ })
})
}
@@ -236,6 +252,11 @@ function buildNavItems(items) {
}
onMounted(async () => {
+ // Refresh the signed-in user (and their permissions) on every app load. The
+ // login payload is cached in localStorage, so without this a permission
+ // granted mid-session stays invisible until the person logs out and back in.
+ if (authStore.isAuthenticated) authStore.fetchUser()
+
// Half-hourly: cheap, and it crosses midnight into the new week without a
// reload on a screen nobody touches.
fiscalWeekTimer = setInterval(() => { currentFiscalWeek.value = fiscalWeek() }, 30 * 60 * 1000)
diff --git a/plugins/computers/plugin.py b/plugins/computers/plugin.py
index 01bea5cd..babf4b9a 100644
--- a/plugins/computers/plugin.py
+++ b/plugins/computers/plugin.py
@@ -1653,6 +1653,7 @@ class ComputersPlugin(BasePlugin):
'icon': 'desktop',
'route': '/pcs',
'position': 15,
+ 'section': 'assets',
},
]
diff --git a/plugins/geenforce/plugin.py b/plugins/geenforce/plugin.py
index 1768805b..5000e248 100644
--- a/plugins/geenforce/plugin.py
+++ b/plugins/geenforce/plugin.py
@@ -125,6 +125,7 @@ class GeEnforcePlugin(BasePlugin):
'icon': 'shield',
'route': '/geenforce/manifests',
'position': 46,
+ 'section': 'fleet',
},
]
diff --git a/plugins/guides/frontend/views/GuideEditor.vue b/plugins/guides/frontend/views/GuideEditor.vue
index 5d5dd462..3afbbdab 100644
--- a/plugins/guides/frontend/views/GuideEditor.vue
+++ b/plugins/guides/frontend/views/GuideEditor.vue
@@ -68,12 +68,71 @@
+
+