Page MenuHomeDevCentral

D3989.id10447.diff
No OneTemporary

D3989.id10447.diff

diff --git a/frontend/src/components/AppNavbar.vue b/frontend/src/components/AppNavbar.vue
--- a/frontend/src/components/AppNavbar.vue
+++ b/frontend/src/components/AppNavbar.vue
@@ -3,13 +3,16 @@
import { RouterLink } from 'vue-router'
import { configApi } from '@/plugins/api'
import { useAuth } from '@/composables/useAuth'
+import { useDarkMode } from '@/composables/useDarkMode'
const config = ref(null)
const { isAuthenticated, logout } = useAuth()
+const { isDark, toggleable, toggle, setTheme } = useDarkMode()
onMounted(async () => {
try {
config.value = await configApi.getAll()
+ setTheme(config.value.theme || 'default')
} catch {
config.value = { navbar: { title: 'ServPulse', buttons_left: [] } }
}
@@ -27,10 +30,10 @@
<div class="flex items-center justify-between h-14">
<div class="flex items-center gap-4">
<a
- v-for="btn in config?.navbar?.buttons_left"
- :key="btn.name"
- :href="btn.link"
- class="text-xs font-medium text-gray-400 hover:text-white transition-colors"
+ v-for="btn in config?.navbar?.buttons_left"
+ :key="btn.name"
+ :href="btn.link"
+ class="text-xs font-medium text-gray-400 hover:text-white transition-colors"
>
{{ btn.name }}
</a>
@@ -41,24 +44,31 @@
</RouterLink>
<div class="flex items-center gap-4">
+ <button
+ v-if="toggleable"
+ @click="toggle"
+ class="text-xs font-medium text-gray-400 hover:text-white transition-colors"
+ >
+ {{ isDark ? 'Light' : 'Dark' }}
+ </button>
<RouterLink
- v-if="isAuthenticated"
- to="/admin"
- class="text-xs font-medium text-gray-400 hover:text-white transition-colors"
+ v-if="isAuthenticated"
+ to="/admin"
+ class="text-xs font-medium text-gray-400 hover:text-white transition-colors"
>
Dashboard
</RouterLink>
<button
- v-if="isAuthenticated"
- @click="handleLogout"
- class="text-xs font-medium text-gray-400 hover:text-white transition-colors"
+ v-if="isAuthenticated"
+ @click="handleLogout"
+ class="text-xs font-medium text-gray-400 hover:text-white transition-colors"
>
Logout
</button>
<RouterLink
- v-else
- to="/admin/login"
- class="text-xs font-medium text-gray-400 hover:text-white transition-colors"
+ v-else
+ to="/admin/login"
+ class="text-xs font-medium text-gray-400 hover:text-white transition-colors"
>
Admin
</RouterLink>
diff --git a/frontend/src/composables/useDarkMode.js b/frontend/src/composables/useDarkMode.js
new file mode 100644
--- /dev/null
+++ b/frontend/src/composables/useDarkMode.js
@@ -0,0 +1,46 @@
+import { ref, computed } from 'vue'
+
+export const THEMES = [
+ { id: 'default', label: 'Default', toggleable: true },
+ { id: 'light', label: 'Light', toggleable: false },
+ { id: 'dark', label: 'Dark', toggleable: false },
+]
+
+const LS_MODE_KEY = 'servpulse_theme_mode'
+
+const theme = ref('default')
+const mode = ref(
+ localStorage.getItem(LS_MODE_KEY) ||
+ (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light')
+)
+
+const currentThemeDef = computed(() => THEMES.find((t) => t.id === theme.value) ?? THEMES[0])
+const toggleable = computed(() => currentThemeDef.value.toggleable)
+const isDark = computed(() => {
+ if (theme.value === 'dark') return true
+ if (theme.value === 'light') return false
+ return mode.value === 'dark'
+})
+
+function applyToDOM() {
+ document.documentElement.setAttribute('data-theme', isDark.value ? 'dark' : 'light')
+}
+
+applyToDOM()
+
+export function useDarkMode() {
+ const setTheme = (themeId) => {
+ if (!THEMES.find((t) => t.id === themeId)) return
+ theme.value = themeId
+ applyToDOM()
+ }
+
+ const toggle = () => {
+ if (!toggleable.value) return
+ mode.value = isDark.value ? 'light' : 'dark'
+ localStorage.setItem(LS_MODE_KEY, mode.value)
+ applyToDOM()
+ }
+
+ return { theme, isDark, toggleable, toggle, setTheme, THEMES }
+}
diff --git a/frontend/src/views/AdminDashboard.vue b/frontend/src/views/AdminDashboard.vue
--- a/frontend/src/views/AdminDashboard.vue
+++ b/frontend/src/views/AdminDashboard.vue
@@ -3,10 +3,13 @@
import { useServices } from '@/composables/useServices'
import { useIncidents } from '@/composables/useIncidents'
import { useMaintenances } from '@/composables/useMaintenances'
+import { useDarkMode, THEMES } from '@/composables/useDarkMode'
import { servicesApi, incidentsApi, maintenancesApi, configApi } from '@/plugins/api'
import StatusBadge from '@/components/StatusBadge.vue'
import { formatDate } from '@/utils/status'
+const { setTheme } = useDarkMode()
+
const activeTab = ref('services')
const tabs = [
{ key: 'services', label: 'Services' },
@@ -152,6 +155,7 @@
line1: { text: '', link1_label: '', link1_url: '', link2_label: '', link2_url: '' },
line2: { text: '', link_label: '', link_url: '' },
},
+ theme: 'default',
})
const MAX_FOOTER_LENGTH = 100
const settingsSaved = ref(false)
@@ -167,6 +171,7 @@
if (!data.footer) data.footer = { ...defaultFooter }
if (!data.footer.line1) data.footer.line1 = { ...defaultFooter.line1 }
if (!data.footer.line2) data.footer.line2 = { ...defaultFooter.line2 }
+ if (!data.theme) data.theme = 'default'
settingsForm.value = data
} catch (err) {
alert('Error loading settings: ' + err.message)
@@ -182,6 +187,7 @@
}
try {
await configApi.update(settingsForm.value)
+ setTheme(settingsForm.value.theme)
settingsSaved.value = true
setTimeout(() => { settingsSaved.value = false }, 3000)
} catch (err) {
@@ -223,11 +229,11 @@
<!-- Tabs -->
<div class="flex gap-1 mb-6 border-b border-gray-200 dark:border-gray-700">
<button
- v-for="tab in tabs"
- :key="tab.key"
- @click="activeTab = tab.key"
- class="px-4 py-2 text-sm font-medium transition-colors border-b-2 -mb-px"
- :class="activeTab === tab.key
+ v-for="tab in tabs"
+ :key="tab.key"
+ @click="activeTab = tab.key"
+ class="px-4 py-2 text-sm font-medium transition-colors border-b-2 -mb-px"
+ :class="activeTab === tab.key
? 'border-brand-500 text-brand-600 dark:text-brand-400'
: 'border-transparent text-gray-500 hover:text-gray-700 dark:hover:text-gray-300'"
>
@@ -289,23 +295,23 @@
<div class="card overflow-hidden">
<table class="w-full text-sm">
<thead class="bg-gray-50 dark:bg-gray-800/50">
- <tr>
- <th class="text-left px-4 py-2.5 font-medium text-gray-500 dark:text-gray-400">Name</th>
- <th class="text-left px-4 py-2.5 font-medium text-gray-500 dark:text-gray-400">Group</th>
- <th class="text-left px-4 py-2.5 font-medium text-gray-500 dark:text-gray-400">Status</th>
- <th class="text-right px-4 py-2.5 font-medium text-gray-500 dark:text-gray-400">Actions</th>
- </tr>
+ <tr>
+ <th class="text-left px-4 py-2.5 font-medium text-gray-500 dark:text-gray-400">Name</th>
+ <th class="text-left px-4 py-2.5 font-medium text-gray-500 dark:text-gray-400">Group</th>
+ <th class="text-left px-4 py-2.5 font-medium text-gray-500 dark:text-gray-400">Status</th>
+ <th class="text-right px-4 py-2.5 font-medium text-gray-500 dark:text-gray-400">Actions</th>
+ </tr>
</thead>
<tbody class="divide-y divide-gray-100 dark:divide-gray-800">
- <tr v-for="service in services" :key="service.id" class="hover:bg-gray-50 dark:hover:bg-gray-800/30">
- <td class="px-4 py-2.5 text-gray-900 dark:text-gray-100">{{ service.name }}</td>
- <td class="px-4 py-2.5 text-gray-500 dark:text-gray-400">{{ service.group || '—' }}</td>
- <td class="px-4 py-2.5"><StatusBadge :status="service.status" /></td>
- <td class="px-4 py-2.5 text-right space-x-2">
- <button @click="openServiceForm(service)" class="text-brand-600 hover:text-brand-700 text-xs font-medium">Edit</button>
- <button @click="deleteService(service.id)" class="text-red-600 hover:text-red-700 text-xs font-medium">Delete</button>
- </td>
- </tr>
+ <tr v-for="service in services" :key="service.id" class="hover:bg-gray-50 dark:hover:bg-gray-800/30">
+ <td class="px-4 py-2.5 text-gray-900 dark:text-gray-100">{{ service.name }}</td>
+ <td class="px-4 py-2.5 text-gray-500 dark:text-gray-400">{{ service.group || '—' }}</td>
+ <td class="px-4 py-2.5"><StatusBadge :status="service.status" /></td>
+ <td class="px-4 py-2.5 text-right space-x-2">
+ <button @click="openServiceForm(service)" class="text-brand-600 hover:text-brand-700 text-xs font-medium">Edit</button>
+ <button @click="deleteService(service.id)" class="text-red-600 hover:text-red-700 text-xs font-medium">Delete</button>
+ </td>
+ </tr>
</tbody>
</table>
</div>
@@ -353,32 +359,32 @@
<div class="card overflow-hidden">
<table class="w-full text-sm">
<thead class="bg-gray-50 dark:bg-gray-800/50">
- <tr>
- <th class="text-left px-4 py-2.5 font-medium text-gray-500 dark:text-gray-400">Title</th>
- <th class="text-left px-4 py-2.5 font-medium text-gray-500 dark:text-gray-400">Status</th>
- <th class="text-left px-4 py-2.5 font-medium text-gray-500 dark:text-gray-400">Impact</th>
- <th class="text-left px-4 py-2.5 font-medium text-gray-500 dark:text-gray-400">Started</th>
- <th class="text-right px-4 py-2.5 font-medium text-gray-500 dark:text-gray-400">Actions</th>
- </tr>
+ <tr>
+ <th class="text-left px-4 py-2.5 font-medium text-gray-500 dark:text-gray-400">Title</th>
+ <th class="text-left px-4 py-2.5 font-medium text-gray-500 dark:text-gray-400">Status</th>
+ <th class="text-left px-4 py-2.5 font-medium text-gray-500 dark:text-gray-400">Impact</th>
+ <th class="text-left px-4 py-2.5 font-medium text-gray-500 dark:text-gray-400">Started</th>
+ <th class="text-right px-4 py-2.5 font-medium text-gray-500 dark:text-gray-400">Actions</th>
+ </tr>
</thead>
<tbody class="divide-y divide-gray-100 dark:divide-gray-800">
- <tr v-for="incident in incidents" :key="incident.id" class="hover:bg-gray-50 dark:hover:bg-gray-800/30">
- <td class="px-4 py-2.5 text-gray-900 dark:text-gray-100">{{ incident.title }}</td>
- <td class="px-4 py-2.5">
+ <tr v-for="incident in incidents" :key="incident.id" class="hover:bg-gray-50 dark:hover:bg-gray-800/30">
+ <td class="px-4 py-2.5 text-gray-900 dark:text-gray-100">{{ incident.title }}</td>
+ <td class="px-4 py-2.5">
<span class="text-xs font-medium" :class="{
'text-red-500': incident.status === 'investigating',
'text-orange-500': incident.status === 'identified',
'text-yellow-500': incident.status === 'monitoring',
'text-green-500': incident.status === 'resolved',
}">{{ incident.status }}</span>
- </td>
- <td class="px-4 py-2.5 text-gray-500 dark:text-gray-400">{{ incident.impact }}</td>
- <td class="px-4 py-2.5 text-gray-500 dark:text-gray-400 text-xs">{{ formatDate(incident.start_date) }}</td>
- <td class="px-4 py-2.5 text-right space-x-2">
- <button @click="openIncidentForm(incident)" class="text-brand-600 hover:text-brand-700 text-xs font-medium">Edit</button>
- <button v-if="incident.status !== 'resolved'" @click="resolveIncident(incident.id)" class="text-green-600 hover:text-green-700 text-xs font-medium">Resolve</button>
- </td>
- </tr>
+ </td>
+ <td class="px-4 py-2.5 text-gray-500 dark:text-gray-400">{{ incident.impact }}</td>
+ <td class="px-4 py-2.5 text-gray-500 dark:text-gray-400 text-xs">{{ formatDate(incident.start_date) }}</td>
+ <td class="px-4 py-2.5 text-right space-x-2">
+ <button @click="openIncidentForm(incident)" class="text-brand-600 hover:text-brand-700 text-xs font-medium">Edit</button>
+ <button v-if="incident.status !== 'resolved'" @click="resolveIncident(incident.id)" class="text-green-600 hover:text-green-700 text-xs font-medium">Resolve</button>
+ </td>
+ </tr>
</tbody>
</table>
</div>
@@ -428,23 +434,23 @@
<div class="card overflow-hidden">
<table class="w-full text-sm">
<thead class="bg-gray-50 dark:bg-gray-800/50">
- <tr>
- <th class="text-left px-4 py-2.5 font-medium text-gray-500 dark:text-gray-400">Title</th>
- <th class="text-left px-4 py-2.5 font-medium text-gray-500 dark:text-gray-400">Status</th>
- <th class="text-left px-4 py-2.5 font-medium text-gray-500 dark:text-gray-400">Scheduled</th>
- <th class="text-right px-4 py-2.5 font-medium text-gray-500 dark:text-gray-400">Actions</th>
- </tr>
+ <tr>
+ <th class="text-left px-4 py-2.5 font-medium text-gray-500 dark:text-gray-400">Title</th>
+ <th class="text-left px-4 py-2.5 font-medium text-gray-500 dark:text-gray-400">Status</th>
+ <th class="text-left px-4 py-2.5 font-medium text-gray-500 dark:text-gray-400">Scheduled</th>
+ <th class="text-right px-4 py-2.5 font-medium text-gray-500 dark:text-gray-400">Actions</th>
+ </tr>
</thead>
<tbody class="divide-y divide-gray-100 dark:divide-gray-800">
- <tr v-for="m in maintenances" :key="m.id" class="hover:bg-gray-50 dark:hover:bg-gray-800/30">
- <td class="px-4 py-2.5 text-gray-900 dark:text-gray-100">{{ m.title }}</td>
- <td class="px-4 py-2.5 text-xs font-medium text-gray-500">{{ m.status }}</td>
- <td class="px-4 py-2.5 text-gray-500 dark:text-gray-400 text-xs">{{ formatDate(m.scheduled_start) }}</td>
- <td class="px-4 py-2.5 text-right space-x-2">
- <button @click="openMaintenanceForm(m)" class="text-brand-600 hover:text-brand-700 text-xs font-medium">Edit</button>
- <button @click="deleteMaintenance(m.id)" class="text-red-600 hover:text-red-700 text-xs font-medium">Delete</button>
- </td>
- </tr>
+ <tr v-for="m in maintenances" :key="m.id" class="hover:bg-gray-50 dark:hover:bg-gray-800/30">
+ <td class="px-4 py-2.5 text-gray-900 dark:text-gray-100">{{ m.title }}</td>
+ <td class="px-4 py-2.5 text-xs font-medium text-gray-500">{{ m.status }}</td>
+ <td class="px-4 py-2.5 text-gray-500 dark:text-gray-400 text-xs">{{ formatDate(m.scheduled_start) }}</td>
+ <td class="px-4 py-2.5 text-right space-x-2">
+ <button @click="openMaintenanceForm(m)" class="text-brand-600 hover:text-brand-700 text-xs font-medium">Edit</button>
+ <button @click="deleteMaintenance(m.id)" class="text-red-600 hover:text-red-700 text-xs font-medium">Delete</button>
+ </td>
+ </tr>
</tbody>
</table>
</div>
@@ -534,6 +540,19 @@
</div>
</div>
+ <!-- appearance -->
+ <div class="card p-5">
+ <h3 class="text-sm font-semibold mb-3">Appearance</h3>
+ <div>
+ <label class="block text-xs font-medium text-gray-600 dark:text-gray-400 mb-1">Theme</label>
+ <select v-model="settingsForm.theme" class="input-field max-w-sm">
+ <option v-for="t in THEMES" :key="t.id" :value="t.id">
+ {{ t.label }}{{ !t.toggleable ? ' (fixed)' : '' }}
+ </option>
+ </select>
+ </div>
+ </div>
+
<!-- Save -->
<div class="flex items-center gap-3">
<button type="submit" class="btn-primary">Save Settings</button>
diff --git a/frontend/tailwind.config.js b/frontend/tailwind.config.js
--- a/frontend/tailwind.config.js
+++ b/frontend/tailwind.config.js
@@ -5,7 +5,7 @@
'./index.html',
'./src/**/*.{vue,js,ts,jsx,tsx}',
]
-export const darkMode = 'class'
+export const darkMode = ['class', '[data-theme="dark"]']
export const theme = {
extend: {
colors: {

File Metadata

Mime Type
text/plain
Expires
Tue, Mar 17, 19:24 (15 h, 38 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
3540659
Default Alt Text
D3989.id10447.diff (16 KB)

Event Timeline