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

@@ -27,7 +27,7 @@ const LEVEL_TITLE: Record<BannerLevel, string> = {
critical: 'Kritisch',
};
export default function AnnouncementBanner() {
export default function AnnouncementBanner({ gridColumn }: { gridColumn?: string }) {
const [dismissed, setDismissed] = useState<string[]>(() => getDismissed());
const { data: banners = [] } = useQuery({
@@ -48,7 +48,7 @@ export default function AnnouncementBanner() {
if (visible.length === 0) return null;
return (
<Box sx={{ mb: 2, display: 'flex', flexDirection: 'column', gap: 1 }}>
<Box sx={{ mb: 0, display: 'flex', flexDirection: 'column', gap: 1, ...(gridColumn ? { gridColumn } : {}) }}>
{visible.map(banner => (
<Collapse key={banner.id} in>
<Alert

View File

@@ -28,7 +28,7 @@ function DashboardLayoutInner({ children }: DashboardLayoutProps) {
const chatWidth = chatPanelOpen ? 360 : 60;
return (
<Box sx={{ display: 'flex' }}>
<Box sx={{ display: 'flex', height: '100vh', overflow: 'hidden' }}>
<Header onMenuClick={handleDrawerToggle} />
<Sidebar mobileOpen={mobileOpen} onMobileClose={() => setMobileOpen(false)} />
@@ -38,7 +38,7 @@ function DashboardLayoutInner({ children }: DashboardLayoutProps) {
flexGrow: 1,
p: 3,
width: { sm: `calc(100% - ${sidebarWidth}px - ${chatWidth}px)` },
minHeight: '100vh',
overflowY: 'auto',
backgroundColor: 'background.default',
transition: 'width 225ms cubic-bezier(0.4, 0, 0.6, 1)',
minWidth: 0,

View File

@@ -0,0 +1,63 @@
import {
Card,
CardContent,
Typography,
Link,
Stack,
Box,
Divider,
} from '@mui/material';
import { Link as LinkIcon, OpenInNew } from '@mui/icons-material';
import { useQuery } from '@tanstack/react-query';
import { configApi } from '../../services/config';
function LinksWidget() {
const { data: externalLinks } = useQuery({
queryKey: ['external-links'],
queryFn: () => configApi.getExternalLinks(),
staleTime: 10 * 60 * 1000,
});
const collections = externalLinks?.linkCollections ?? [];
const nonEmpty = collections.filter((c) => c.links.length > 0);
if (nonEmpty.length === 0) return null;
return (
<>
{nonEmpty.map((collection) => (
<Card key={collection.id}>
<CardContent>
<Box sx={{ display: 'flex', alignItems: 'center', mb: 1 }}>
<LinkIcon color="primary" sx={{ mr: 1 }} />
<Typography variant="h6">{collection.name}</Typography>
</Box>
<Divider sx={{ mb: 1.5 }} />
<Stack spacing={0.5}>
{collection.links.map((link, i) => (
<Link
key={i}
href={link.url}
target="_blank"
rel="noopener noreferrer"
underline="hover"
sx={{
display: 'flex',
alignItems: 'center',
gap: 0.5,
py: 0.5,
}}
>
{link.name}
<OpenInNew sx={{ fontSize: 14, opacity: 0.6 }} />
</Link>
))}
</Stack>
</CardContent>
</Card>
))}
</>
);
}
export default LinksWidget;

View File

@@ -152,6 +152,7 @@ const VehicleBookingQuickAddWidget: React.FC = () => {
onChange={(e) => setBeginn(e.target.value)}
required
InputLabelProps={{ shrink: true }}
sx={{ '& input': { color: 'text.primary' } }}
/>
<TextField
@@ -163,6 +164,7 @@ const VehicleBookingQuickAddWidget: React.FC = () => {
onChange={(e) => setEnde(e.target.value)}
required
InputLabelProps={{ shrink: true }}
sx={{ '& input': { color: 'text.primary' } }}
/>
<TextField

View File

@@ -13,3 +13,4 @@ export { default as AdminStatusWidget } from './AdminStatusWidget';
export { default as VehicleBookingQuickAddWidget } from './VehicleBookingQuickAddWidget';
export { default as EventQuickAddWidget } from './EventQuickAddWidget';
export { default as AnnouncementBanner } from './AnnouncementBanner';
export { default as LinksWidget } from './LinksWidget';