feat: sync DB prefs, update history tab, configurable dashboard shortcuts

- auth store fetchMe(): sync theme/sidebar_position/lang from DB to localStorage+stores on login/refresh
- profilePage setters: PATCH /api/auth/preferences on every preference change
- updatesPage: add history tab (GET /api/updates/history) with job list, click to view output
- dashboardPage: load shortcuts from settings API, fall back to defaults if none configured
- settingsPage: new Raccourcis tab to add/remove/configure dashboard shortcuts (saved as JSON)
- settings.go: expose dashboard_shortcuts in publicSettings for GET/PUT access
- pages.css: add .history-table, .shortcut-row styles

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
enzo 2026-03-22 00:35:24 +01:00
parent 780e5ec81d
commit 21e1e0ed1e
6 changed files with 230 additions and 10 deletions

View file

@ -118,6 +118,19 @@ document.addEventListener('alpine:init', () => {
const res = await apiFetch('/api/auth/me')
if (res.ok) {
this.user = await res.json()
// Sync préférences DB → localStorage + stores
const u = this.user
if (u.theme && u.theme !== Alpine.store('ui').theme) {
localStorage.setItem('pxp_theme', u.theme)
Alpine.store('ui').theme = u.theme
Alpine.store('ui').applyTheme()
}
if (u.sidebar_position && u.sidebar_position !== Alpine.store('ui').sidebarPosition) {
Alpine.store('ui').setSidebarPosition(u.sidebar_position)
}
if (u.lang && u.lang !== Alpine.store('i18n').lang) {
Alpine.store('i18n').setLang(u.lang)
}
} else {
// Token expiré, invalide, ou toute autre erreur → tenter un refresh
await this.tryRefresh()
@ -400,6 +413,7 @@ document.addEventListener('alpine:init', () => {
dragSrcIdx: null,
_dragOriginal: null,
hoveredWidgetId: null,
shortcuts: [],
widgets: (function() {
const defaults = [
@ -485,12 +499,34 @@ document.addEventListener('alpine:init', () => {
async init() {
await this.fetchResources()
await this.connectWS()
await this.loadShortcuts()
},
destroy() {
if (this._unsubWS) { this._unsubWS(); this._unsubWS = null }
},
async loadShortcuts() {
try {
const res = await apiFetch('/api/settings')
if (res.ok) {
const data = await res.json()
if (data.dashboard_shortcuts) {
const parsed = JSON.parse(data.dashboard_shortcuts)
if (Array.isArray(parsed) && parsed.length > 0) this.shortcuts = parsed
}
}
} catch(e) { /* use defaults */ }
},
get displayShortcuts() {
return this.shortcuts.length ? this.shortcuts : [
{ href: '/proxmox.html', icon: 'lnid-server-1', label: 'Proxmox' },
{ href: '/terminal.html', icon: 'lnid-terminal', label: 'Terminal' },
{ href: '/updates.html', icon: 'lnid-arrow-upward', label: 'Updates' },
]
},
async fetchResources() {
try {
const res = await apiFetch('/api/proxmox/resources')
@ -543,16 +579,19 @@ document.addEventListener('alpine:init', () => {
Alpine.store('ui').theme = t
Alpine.store('ui').applyTheme()
localStorage.setItem('pxp_theme', t)
apiFetch('/api/auth/preferences', { method: 'PATCH', body: JSON.stringify({ theme: t }) }).catch(() => {})
},
setSidebarPosition(pos) {
this.sidebarPosition = pos
Alpine.store('ui').setSidebarPosition(pos)
apiFetch('/api/auth/preferences', { method: 'PATCH', body: JSON.stringify({ sidebar_position: pos }) }).catch(() => {})
},
async setLang(lang) {
this.lang = lang
await Alpine.store('i18n').setLang(lang)
apiFetch('/api/auth/preferences', { method: 'PATCH', body: JSON.stringify({ lang }) }).catch(() => {})
},
async loadSessions() {
@ -664,6 +703,9 @@ document.addEventListener('alpine:init', () => {
currentJob: null,
output: '',
jobStatus: '',
activeTab: 'targets',
history: [],
historyLoading: false,
async init() {
await this.loadTargets()
@ -791,6 +833,21 @@ document.addEventListener('alpine:init', () => {
})
},
async loadHistory() {
this.historyLoading = true
try {
const res = await apiFetch('/api/updates/history')
if (res.ok) this.history = await res.json() || []
} catch(e) { /* ignore */ }
this.historyLoading = false
},
formatDate(iso) {
if (!iso) return '—'
const d = new Date(iso)
return d.toLocaleString('fr-FR', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit' })
},
get totalPackages() {
return this.targets.reduce((sum, t) => sum + (t.packages?.length || 0), 0)
},
@ -805,6 +862,7 @@ document.addEventListener('alpine:init', () => {
saving: false,
saved: false,
error: '',
shortcuts: [],
settings: {
instance_name: '',
public_url: '',
@ -827,6 +885,9 @@ document.addEventListener('alpine:init', () => {
if (res.ok) {
const data = await res.json()
Object.assign(this.settings, data)
if (data.dashboard_shortcuts) {
try { this.shortcuts = JSON.parse(data.dashboard_shortcuts) } catch(e) { this.shortcuts = [] }
}
}
} finally {
this.loading = false
@ -862,6 +923,36 @@ document.addEventListener('alpine:init', () => {
}
},
addShortcut() {
this.shortcuts.push({ label: '', href: '', icon: 'lnid-link-1' })
},
removeShortcut(idx) {
this.shortcuts.splice(idx, 1)
},
async saveShortcuts() {
this.saving = true
this.saved = false
this.error = ''
try {
const res = await apiFetch('/api/settings/dashboard_shortcuts', {
method: 'PUT',
body: JSON.stringify({ value: JSON.stringify(this.shortcuts) }),
})
if (!res.ok) {
const d = await res.json().catch(() => ({}))
throw new Error(d.error || 'Erreur sauvegarde raccourcis')
}
this.saved = true
setTimeout(() => { this.saved = false }, 3000)
} catch(e) {
this.error = e.message
} finally {
this.saving = false
}
},
t(key) { return Alpine.store('i18n').t(key) },
}))