feat: bug fixes, layout improvements, and new features

Bug fixes:
- Remove non-existent `role` column from admin users SQL query (A1)
- Fix Nextcloud Talk chat API path v4 → v1 for messages/send/read (A2)
- Fix ServiceModeTab sync: useState → useEffect to reflect DB state (A3)
- Guard BookStack book_slug with book_id fallback to avoid broken URLs (A4)

Layout & UI:
- Chat panel: sticky full-height positioning, main content scrolls independently (B1)
- Vehicle booking datetime inputs: explicit text color for dark mode (B2)
- AnnouncementBanner moved into grid with full-width span (B3)

Features:
- Per-user widget visibility preferences stored in users.preferences JSONB (C1)
- Link collections: grouped external links in admin UI and dashboard widget (C2)
- Admin ping history: migration 026, checked_at timestamps, expandable history rows (C4)
- Service mode end date picker with scheduled deactivation display (C5)
- Vikunja startup config logging and configured:false warnings (C7)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Matthias Hochmeister
2026-03-12 14:57:54 +01:00
parent 81174c2498
commit a5cd78f01f
29 changed files with 593 additions and 105 deletions

View File

@@ -1,4 +1,4 @@
import { useState } from 'react';
import { useState, useEffect } from 'react';
import {
Box, Card, CardContent, Typography, Switch, FormControlLabel,
TextField, Button, Alert, CircularProgress,
@@ -20,17 +20,19 @@ export default function ServiceModeTab() {
const currentValue = setting?.value ?? { active: false, message: '' };
const [active, setActive] = useState<boolean>(currentValue.active ?? false);
const [message, setMessage] = useState<string>(currentValue.message ?? '');
const [endsAt, setEndsAt] = useState<string>('');
// Sync state when data loads
useState(() => {
useEffect(() => {
if (setting?.value) {
setActive(setting.value.active ?? false);
setMessage(setting.value.message ?? '');
setEndsAt(setting.value.ends_at ?? '');
}
});
}, [setting]);
const mutation = useMutation({
mutationFn: (value: { active: boolean; message: string }) =>
mutationFn: (value: { active: boolean; message: string; ends_at?: string | null }) =>
settingsApi.update('service_mode', value),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['service-mode'] });
@@ -41,7 +43,7 @@ export default function ServiceModeTab() {
});
const handleSave = () => {
mutation.mutate({ active, message });
mutation.mutate({ active, message, ends_at: endsAt || null });
};
if (isLoading) {
@@ -63,6 +65,12 @@ export default function ServiceModeTab() {
</Alert>
)}
{active && endsAt && (
<Alert severity="info" sx={{ mb: 2 }}>
Wartungsmodus endet automatisch am {new Date(endsAt).toLocaleString('de-DE')}.
</Alert>
)}
<FormControlLabel
control={
<Switch
@@ -87,6 +95,17 @@ export default function ServiceModeTab() {
helperText="Diese Nachricht sehen Benutzer auf der Wartungsseite."
/>
<TextField
fullWidth
label="Automatisch deaktivieren am"
type="datetime-local"
value={endsAt}
onChange={(e) => setEndsAt(e.target.value)}
InputLabelProps={{ shrink: true }}
helperText="Optional: Wartungsmodus wird automatisch zu diesem Zeitpunkt deaktiviert."
sx={{ mb: 3, '& input': { color: 'text.primary' } }}
/>
<Button
variant="contained"
color={active ? 'error' : 'primary'}