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:
@@ -19,13 +19,14 @@ class ConfigController {
|
||||
if (environment.bookstack.url) envLinks.bookstack = environment.bookstack.url;
|
||||
if (environment.vikunja.url) envLinks.vikunja = environment.vikunja.url;
|
||||
|
||||
const customLinks = await settingsService.getExternalLinks();
|
||||
const linkCollections = await settingsService.getExternalLinks();
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: {
|
||||
...envLinks,
|
||||
customLinks,
|
||||
customLinks: linkCollections.flatMap(c => c.links),
|
||||
linkCollections,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -137,7 +137,7 @@ class ServiceMonitorController {
|
||||
async getUsers(_req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const result = await pool.query(
|
||||
`SELECT id, email, name, role, authentik_groups as groups, is_active, last_login_at
|
||||
`SELECT id, email, name, authentik_groups as groups, is_active, last_login_at
|
||||
FROM users ORDER BY name`
|
||||
);
|
||||
res.json({ success: true, data: result.rows });
|
||||
@@ -147,6 +147,17 @@ class ServiceMonitorController {
|
||||
}
|
||||
}
|
||||
|
||||
async getPingHistory(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const { serviceId } = req.params;
|
||||
const data = await serviceMonitorService.getPingHistory(serviceId as string);
|
||||
res.json({ success: true, data });
|
||||
} catch (error) {
|
||||
logger.error('Failed to get ping history', { error });
|
||||
res.status(500).json({ success: false, message: 'Failed to get ping history' });
|
||||
}
|
||||
}
|
||||
|
||||
async broadcastNotification(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const { titel, nachricht, schwere, targetGroup } = broadcastSchema.parse(req.body);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Request, Response } from 'express';
|
||||
import { z } from 'zod';
|
||||
import settingsService from '../services/settings.service';
|
||||
import pool from '../config/database';
|
||||
import logger from '../utils/logger';
|
||||
|
||||
const updateSchema = z.object({
|
||||
@@ -8,8 +9,12 @@ const updateSchema = z.object({
|
||||
});
|
||||
|
||||
const externalLinkSchema = z.array(z.object({
|
||||
id: z.string().min(1),
|
||||
name: z.string().min(1).max(200),
|
||||
url: z.string().url().max(500),
|
||||
links: z.array(z.object({
|
||||
name: z.string().min(1).max(200),
|
||||
url: z.string().url().max(500),
|
||||
})),
|
||||
}));
|
||||
|
||||
class SettingsController {
|
||||
@@ -57,6 +62,32 @@ class SettingsController {
|
||||
res.status(500).json({ success: false, message: 'Failed to update setting' });
|
||||
}
|
||||
}
|
||||
async getUserPreferences(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const userId = (req as any).user.id;
|
||||
const result = await pool.query('SELECT preferences FROM users WHERE id = $1', [userId]);
|
||||
const prefs = result.rows[0]?.preferences ?? {};
|
||||
res.json({ success: true, data: prefs });
|
||||
} catch (error) {
|
||||
logger.error('Failed to get user preferences', { error });
|
||||
res.status(500).json({ success: false, message: 'Failed to get user preferences' });
|
||||
}
|
||||
}
|
||||
|
||||
async updateUserPreferences(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const userId = (req as any).user.id;
|
||||
const preferences = req.body;
|
||||
await pool.query(
|
||||
'UPDATE users SET preferences = $1 WHERE id = $2',
|
||||
[JSON.stringify(preferences), userId]
|
||||
);
|
||||
res.json({ success: true });
|
||||
} catch (error) {
|
||||
logger.error('Failed to update user preferences', { error });
|
||||
res.status(500).json({ success: false, message: 'Failed to update user preferences' });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default new SettingsController();
|
||||
|
||||
@@ -7,6 +7,7 @@ import logger from '../utils/logger';
|
||||
class VikunjaController {
|
||||
async getMyTasks(_req: Request, res: Response): Promise<void> {
|
||||
if (!environment.vikunja.url) {
|
||||
logger.warn('Vikunja not configured – VIKUNJA_URL is empty');
|
||||
res.status(200).json({ success: true, data: [], configured: false });
|
||||
return;
|
||||
}
|
||||
@@ -21,6 +22,7 @@ class VikunjaController {
|
||||
|
||||
async getOverdueTasks(req: Request, res: Response): Promise<void> {
|
||||
if (!environment.vikunja.url) {
|
||||
logger.warn('Vikunja not configured – VIKUNJA_URL is empty');
|
||||
res.status(200).json({ success: true, data: [], configured: false });
|
||||
return;
|
||||
}
|
||||
@@ -53,6 +55,7 @@ class VikunjaController {
|
||||
|
||||
async getProjects(_req: Request, res: Response): Promise<void> {
|
||||
if (!environment.vikunja.url) {
|
||||
logger.warn('Vikunja not configured – VIKUNJA_URL is empty');
|
||||
res.status(200).json({ success: true, data: [], configured: false });
|
||||
return;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user