Page MenuHomeDevCentral

D3989.id10446.diff
No OneTemporary

D3989.id10446.diff

diff --git a/frontend/app.json b/frontend/app.json
new file mode 100644
--- /dev/null
+++ b/frontend/app.json
@@ -0,0 +1,33 @@
+{
+ "theme": "default",
+ "navbar": {
+ "title": "ServPulse",
+ "logo_url": "",
+ "buttons_left": [
+ {
+ "name": "DevCentral",
+ "icon": "fa fa-play",
+ "link": "https://devcentral.nasqueron.org"
+ },
+ {
+ "name": "Agora",
+ "icon": "fa fa-book",
+ "link": "https://agora.nasqueron.org"
+ }
+ ]
+ },
+ "footer": {
+ "line1": {
+ "text": "Powered by %link1% — made with ❤️ by %link2%",
+ "link1_label": "ServPulse",
+ "link1_url": "https://devcentral.nasqueron.org/source/servpulse/",
+ "link2_label": "Nasqueron",
+ "link2_url": "https://nasqueron.org"
+ },
+ "line2": {
+ "text": "Find this useful? %link%",
+ "link_label": "Contribute to Nasqueron",
+ "link_url": "https://devcentral.nasqueron.org/source/servpulse/"
+ }
+ }
+}
diff --git a/frontend/src/assets/logo-white.svg b/frontend/src/assets/logo-white.svg
new file mode 100644
--- /dev/null
+++ b/frontend/src/assets/logo-white.svg
@@ -0,0 +1,50 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200" width="100%" height="100%">
+ <defs>
+ <clipPath id="left-mask-mono">
+ <rect x="0" y="0" width="100" height="200" />
+ </clipPath>
+
+ <clipPath id="right-mask-mono">
+ <rect x="100" y="0" width="100" height="200" />
+ </clipPath>
+
+ <polygon id="tooth-mono" points="88,0 112,0 115,22 85,22" />
+
+ <mask id="mono-cutout">
+ <rect x="0" y="0" width="200" height="200" fill="white" />
+
+ <g clip-path="url(#left-mask-mono)">
+ <circle cx="100" cy="100" r="60" fill="black" />
+ <circle cx="100" cy="100" r="28" fill="black" />
+ </g>
+
+ <g clip-path="url(#right-mask-mono)">
+ <rect x="100" y="69" width="100" height="4" fill="black" />
+ <rect x="100" y="127" width="100" height="4" fill="black" />
+ <circle cx="160" cy="42" r="5" fill="black" />
+ <circle cx="160" cy="100" r="5" fill="black" />
+ <circle cx="160" cy="158" r="5" fill="black" />
+ <path d="M 10 100 L 52 100 L 78 40 L 102 165 L 122 65 L 145 100 L 190 100" fill="none" stroke="black" stroke-width="5" stroke-linecap="round" stroke-linejoin="round" />
+ </g>
+ </mask>
+ </defs>
+
+ <g fill="#ffffff" mask="url(#mono-cutout)">
+ <g clip-path="url(#left-mask-mono)">
+ <use href="#tooth-mono" transform="rotate(-30 100 100)" />
+ <use href="#tooth-mono" transform="rotate(-60 100 100)" />
+ <use href="#tooth-mono" transform="rotate(-90 100 100)" />
+ <use href="#tooth-mono" transform="rotate(-120 100 100)" />
+ <use href="#tooth-mono" transform="rotate(-150 100 100)" />
+ <circle cx="100" cy="100" r="85" />
+ <circle cx="100" cy="100" r="46" />
+ </g>
+ <g clip-path="url(#right-mask-mono)">
+ <circle cx="100" cy="100" r="85" />
+ </g>
+ </g>
+
+ <g clip-path="url(#left-mask-mono)">
+ <path d="M 10 100 L 52 100 L 78 40 L 102 165 L 122 65 L 145 100 L 190 100" fill="none" stroke="#ffffff" stroke-width="5" stroke-linecap="round" stroke-linejoin="round" />
+ </g>
+</svg>
diff --git a/frontend/src/assets/logo.svg b/frontend/src/assets/logo.svg
--- a/frontend/src/assets/logo.svg
+++ b/frontend/src/assets/logo.svg
@@ -1 +1,65 @@
-<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 261.76 226.69"><path d="M161.096.001l-30.225 52.351L100.647.001H-.005l130.877 226.688L261.749.001z" fill="#41b883"/><path d="M161.096.001l-30.225 52.351L100.647.001H52.346l78.526 136.01L209.398.001z" fill="#34495e"/></svg>
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200" width="100%" height="100%">
+ <defs>
+ <linearGradient id="blueGrad" x1="0" y1="0" x2="100" y2="200" gradientUnits="userSpaceOnUse">
+ <stop offset="0%" stop-color="#4da7db" />
+ <stop offset="100%" stop-color="#24519c" />
+ </linearGradient>
+
+ <linearGradient id="purpleGrad" x1="100" y1="0" x2="200" y2="200" gradientUnits="userSpaceOnUse">
+ <stop offset="0%" stop-color="#8a41c9" />
+ <stop offset="100%" stop-color="#3d1e70" />
+ </linearGradient>
+
+ <linearGradient id="neonPulse" x1="0" y1="0" x2="200" y2="0" gradientUnits="userSpaceOnUse">
+ <stop offset="10%" stop-color="#76c8ff" />
+ <stop offset="50%" stop-color="#d082ff" />
+ <stop offset="90%" stop-color="#76c8ff" />
+ </linearGradient>
+
+ <filter id="glow" x="-20%" y="-20%" width="140%" height="140%">
+ <feGaussianBlur stdDeviation="3.5" result="blur" />
+ <feMerge>
+ <feMergeNode in="blur" />
+ <feMergeNode in="SourceGraphic" />
+ </feMerge>
+ </filter>
+
+ <clipPath id="left-mask">
+ <rect x="0" y="0" width="98" height="200" />
+ </clipPath>
+
+ <clipPath id="right-mask">
+ <rect x="102" y="0" width="98" height="200" />
+ </clipPath>
+
+ <polygon id="tooth" points="90,0 110,0 114,20 86,20" fill="url(#blueGrad)" />
+ </defs>
+
+ <g clip-path="url(#left-mask)">
+ <use href="#tooth" transform="rotate(-30 100 100)" />
+ <use href="#tooth" transform="rotate(-60 100 100)" />
+ <use href="#tooth" transform="rotate(-90 100 100)" />
+ <use href="#tooth" transform="rotate(-120 100 100)" />
+ <use href="#tooth" transform="rotate(-150 100 100)" />
+
+ <circle cx="100" cy="100" r="85" fill="url(#blueGrad)" />
+ <circle cx="100" cy="100" r="63" fill="#ffffff" />
+ <circle cx="100" cy="100" r="48" fill="url(#blueGrad)" />
+ <circle cx="100" cy="100" r="30" fill="#ffffff" />
+ </g>
+
+ <g clip-path="url(#right-mask)">
+ <circle cx="100" cy="100" r="85" fill="url(#purpleGrad)" />
+
+ <rect x="100" y="70" width="100" height="2" fill="#a4edff" opacity="0.45" />
+ <rect x="100" y="128" width="100" height="2" fill="#a4edff" opacity="0.45" />
+
+ <circle cx="160" cy="42" r="3.5" fill="#e8f8ff" filter="url(#glow)" />
+ <circle cx="160" cy="100" r="3.5" fill="#e8f8ff" filter="url(#glow)" />
+ <circle cx="160" cy="158" r="3.5" fill="#e8f8ff" filter="url(#glow)" />
+ </g>
+
+ <path d="M 12 100 L 52 100 L 78 40 L 102 165 L 122 65 L 145 100 L 188 100" fill="none" stroke="url(#neonPulse)" stroke-width="6" stroke-linecap="round" stroke-linejoin="round" opacity="0.5" filter="url(#glow)" />
+
+ <path d="M 12 100 L 52 100 L 78 40 L 102 165 L 122 65 L 145 100 L 188 100" fill="none" stroke="#ffffff" stroke-width="3.5" stroke-linecap="round" stroke-linejoin="round" />
+</svg>
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,17 @@
import { RouterLink } from 'vue-router'
import { configApi } from '@/plugins/api'
import { useAuth } from '@/composables/useAuth'
+import { useDarkMode } from '@/composables/useDarkMode'
+import defaultLogo from '@/assets/logo.svg'
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,38 +31,46 @@
<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>
</div>
- <RouterLink to="/" class="text-sm font-bold tracking-widest uppercase hover:text-gray-300 transition-colors">
- {{ config?.navbar?.title || 'ServPulse' }}
+ <RouterLink to="/" class="flex items-center gap-2 hover:text-gray-300 transition-colors">
+ <img :src="config?.navbar?.logo_url || defaultLogo" alt="logo" class="h-6 w-6" />
+ <span class="text-sm font-bold tracking-widest uppercase">{{ config?.navbar?.title || 'ServPulse' }}</span>
</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' },
@@ -147,11 +150,12 @@
// Settings
const settingsForm = ref({
- navbar: { title: 'ServPulse', buttons_left: [] },
+ navbar: { title: 'ServPulse', logo_url: '', buttons_left: [] },
footer: {
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,8 @@
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.navbar.logo_url) data.navbar.logo_url = ''
+ if (!data.theme) data.theme = 'default'
settingsForm.value = data
} catch (err) {
alert('Error loading settings: ' + err.message)
@@ -182,6 +188,7 @@
}
try {
await configApi.update(settingsForm.value)
+ setTheme(settingsForm.value.theme)
settingsSaved.value = true
setTimeout(() => { settingsSaved.value = false }, 3000)
} catch (err) {
@@ -223,11 +230,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 +296,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 +360,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 +435,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>
@@ -460,9 +467,15 @@
<!-- Site Title -->
<div class="card p-5">
<h3 class="text-sm font-semibold mb-3">Site Title</h3>
- <div>
- <label class="block text-xs font-medium text-gray-600 dark:text-gray-400 mb-1">Navbar Title</label>
- <input v-model="settingsForm.navbar.title" class="input-field max-w-sm" placeholder="ServPulse" />
+ <div class="space-y-3">
+ <div>
+ <label class="block text-xs font-medium text-gray-600 dark:text-gray-400 mb-1">Navbar Title</label>
+ <input v-model="settingsForm.navbar.title" class="input-field max-w-sm" placeholder="ServPulse" />
+ </div>
+ <div>
+ <label class="block text-xs font-medium text-gray-600 dark:text-gray-400 mb-1">Logo URL</label>
+ <input v-model="settingsForm.navbar.logo_url" type="url" class="input-field max-w-sm" placeholder="https://example.com/logo.png" />
+ </div>
</div>
</div>
@@ -534,6 +547,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 (13 h, 54 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
3540658
Default Alt Text
D3989.id10446.diff (25 KB)

Event Timeline