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

@@ -4,6 +4,7 @@ import {
Box,
Fade,
} from '@mui/material';
import { useQuery } from '@tanstack/react-query';
import { useAuth } from '../contexts/AuthContext';
import DashboardLayout from '../components/dashboard/DashboardLayout';
import SkeletonCard from '../components/shared/SkeletonCard';
@@ -22,6 +23,10 @@ import AdminStatusWidget from '../components/dashboard/AdminStatusWidget';
import AnnouncementBanner from '../components/dashboard/AnnouncementBanner';
import VehicleBookingQuickAddWidget from '../components/dashboard/VehicleBookingQuickAddWidget';
import EventQuickAddWidget from '../components/dashboard/EventQuickAddWidget';
import LinksWidget from '../components/dashboard/LinksWidget';
import { preferencesApi } from '../services/settings';
import { WidgetKey } from '../constants/widgets';
function Dashboard() {
const { user } = useAuth();
const isAdmin = user?.groups?.includes('dashboard_admin') ?? false;
@@ -33,6 +38,15 @@ function Dashboard() {
) ?? false;
const [dataLoading, setDataLoading] = useState(true);
const { data: preferences } = useQuery({
queryKey: ['user-preferences'],
queryFn: preferencesApi.get,
});
const widgetVisible = (key: WidgetKey) => {
return preferences?.widgets?.[key] !== false;
};
useEffect(() => {
const timer = setTimeout(() => {
setDataLoading(false);
@@ -44,7 +58,6 @@ function Dashboard() {
return (
<DashboardLayout>
<Container maxWidth={false} disableGutters>
<AnnouncementBanner />
<Box
sx={{
display: 'grid',
@@ -53,6 +66,9 @@ function Dashboard() {
alignItems: 'start',
}}
>
{/* Announcement Banner — spans full width, renders null when no banners */}
<AnnouncementBanner gridColumn="1 / -1" />
{/* User Profile Card — full width, contains welcome greeting */}
{user && (
<Box sx={{ gridColumn: '1 / -1' }}>
@@ -69,6 +85,7 @@ function Dashboard() {
)}
{/* Vehicle Status Card */}
{widgetVisible('vehicles') && (
<Box>
<Fade in={!dataLoading} timeout={600} style={{ transitionDelay: '300ms' }}>
<Box>
@@ -76,8 +93,10 @@ function Dashboard() {
</Box>
</Fade>
</Box>
)}
{/* Equipment Status Card */}
{widgetVisible('equipment') && (
<Box>
<Fade in={!dataLoading} timeout={600} style={{ transitionDelay: '350ms' }}>
<Box>
@@ -85,9 +104,10 @@ function Dashboard() {
</Box>
</Fade>
</Box>
)}
{/* Atemschutz Status Card */}
{canViewAtemschutz && (
{canViewAtemschutz && widgetVisible('atemschutz') && (
<Box>
<Fade in={!dataLoading} timeout={600} style={{ transitionDelay: '400ms' }}>
<Box>
@@ -98,6 +118,7 @@ function Dashboard() {
)}
{/* Upcoming Events Widget */}
{widgetVisible('events') && (
<Box>
<Fade in={!dataLoading} timeout={600} style={{ transitionDelay: '440ms' }}>
<Box>
@@ -105,8 +126,10 @@ function Dashboard() {
</Box>
</Fade>
</Box>
)}
{/* Nextcloud Talk Widget */}
{widgetVisible('nextcloudTalk') && (
<Box>
{dataLoading ? (
<SkeletonCard variant="basic" />
@@ -118,8 +141,10 @@ function Dashboard() {
</Fade>
)}
</Box>
)}
{/* BookStack Recent Pages Widget */}
{widgetVisible('bookstackRecent') && (
<Box>
<Fade in={!dataLoading} timeout={600} style={{ transitionDelay: '520ms' }}>
<Box>
@@ -127,8 +152,10 @@ function Dashboard() {
</Box>
</Fade>
</Box>
)}
{/* BookStack Search Widget */}
{widgetVisible('bookstackSearch') && (
<Box>
<Fade in={!dataLoading} timeout={600} style={{ transitionDelay: '560ms' }}>
<Box>
@@ -136,8 +163,10 @@ function Dashboard() {
</Box>
</Fade>
</Box>
)}
{/* Vikunja — My Tasks Widget */}
{widgetVisible('vikunjaTasks') && (
<Box>
<Fade in={!dataLoading} timeout={600} style={{ transitionDelay: '600ms' }}>
<Box>
@@ -145,8 +174,10 @@ function Dashboard() {
</Box>
</Fade>
</Box>
)}
{/* Vikunja — Quick Add Widget */}
{widgetVisible('vikunjaQuickAdd') && (
<Box>
<Fade in={!dataLoading} timeout={600} style={{ transitionDelay: '640ms' }}>
<Box>
@@ -154,9 +185,10 @@ function Dashboard() {
</Box>
</Fade>
</Box>
)}
{/* Vehicle Booking — Quick Add Widget */}
{canWrite && (
{canWrite && widgetVisible('vehicleBooking') && (
<Box>
<Fade in={!dataLoading} timeout={600} style={{ transitionDelay: '720ms' }}>
<Box>
@@ -167,7 +199,7 @@ function Dashboard() {
)}
{/* Event — Quick Add Widget */}
{canWrite && (
{canWrite && widgetVisible('eventQuickAdd') && (
<Box>
<Fade in={!dataLoading} timeout={600} style={{ transitionDelay: '760ms' }}>
<Box>
@@ -180,8 +212,19 @@ function Dashboard() {
{/* Vikunja — Overdue Notifier (invisible, polling component) */}
<VikunjaOverdueNotifier />
{/* Links Widget */}
{widgetVisible('links') && (
<Box>
<Fade in={!dataLoading} timeout={600} style={{ transitionDelay: '790ms' }}>
<Box>
<LinksWidget />
</Box>
</Fade>
</Box>
)}
{/* Admin Status Widget — only for admins */}
{isAdmin && (
{isAdmin && widgetVisible('adminStatus') && (
<Box>
<Fade in={!dataLoading} timeout={600} style={{ transitionDelay: '680ms' }}>
<Box>