Page Menu
Home
DevCentral
Search
Configure Global Search
Log In
Files
F24898420
D3989.id10447.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
16 KB
Referenced Files
None
Subscribers
None
D3989.id10447.diff
View Options
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
Details
Attached
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)
Attached To
Mode
D3989: Add dark mode toggle button
Attached
Detach File
Event Timeline
Log In to Comment