feat: CheckMK host link in inventory, system logs hidden by default in logbook
This commit is contained in:
@ -88,6 +88,7 @@ function ensureColumn(table: string, column: string, ddl: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ensureColumn('devices', 'checkMkUrl', "checkMkUrl TEXT NOT NULL DEFAULT ''");
|
ensureColumn('devices', 'checkMkUrl', "checkMkUrl TEXT NOT NULL DEFAULT ''");
|
||||||
|
ensureColumn('devices', 'cmkHostname', "cmkHostname TEXT NOT NULL DEFAULT ''");
|
||||||
|
|
||||||
// Seed default settings (INSERT OR IGNORE = only if key absent)
|
// Seed default settings (INSERT OR IGNORE = only if key absent)
|
||||||
const _insertDefault = db.prepare('INSERT OR IGNORE INTO settings (key, value) VALUES (?, ?)');
|
const _insertDefault = db.prepare('INSERT OR IGNORE INTO settings (key, value) VALUES (?, ?)');
|
||||||
|
|||||||
@ -155,10 +155,12 @@ async function startServer() {
|
|||||||
const secret = getSetting('azure_client_secret');
|
const secret = getSetting('azure_client_secret');
|
||||||
const appUrl = process.env.APP_URL || `http://localhost:${PORT}`;
|
const appUrl = process.env.APP_URL || `http://localhost:${PORT}`;
|
||||||
const effectiveRedirectUri = getSetting('azure_redirect_uri') || `${appUrl}/api/auth/azure/callback`;
|
const effectiveRedirectUri = getSetting('azure_redirect_uri') || `${appUrl}/api/auth/azure/callback`;
|
||||||
|
const cmkApiUrl = getSetting('checkmk_api_url') || process.env.CHECKMK_API_URL || '';
|
||||||
res.json({
|
res.json({
|
||||||
azureEnabled: enabled && Boolean(clientId) && Boolean(tenantId) && Boolean(secret),
|
azureEnabled: enabled && Boolean(clientId) && Boolean(tenantId) && Boolean(secret),
|
||||||
effectiveRedirectUri,
|
effectiveRedirectUri,
|
||||||
checkmkEnabled: getSetting('checkmk_enabled') === 'true',
|
checkmkEnabled: getSetting('checkmk_enabled') === 'true',
|
||||||
|
checkmkBaseUrl: cmkApiUrl.replace(/\/api\/.*$/, ''),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -751,7 +753,7 @@ async function startServer() {
|
|||||||
const cmkHost = ipToHostname.get(dev.ip);
|
const cmkHost = ipToHostname.get(dev.ip);
|
||||||
if (!cmkHost) {
|
if (!cmkHost) {
|
||||||
if (dev.status !== 'unknown') {
|
if (dev.status !== 'unknown') {
|
||||||
db.prepare('UPDATE devices SET status = ?, lastCheckedAt = ? WHERE id = ?').run('unknown', now, dev.id);
|
db.prepare('UPDATE devices SET status = ?, lastCheckedAt = ?, cmkHostname = ? WHERE id = ?').run('unknown', now, '', dev.id);
|
||||||
db.prepare('INSERT INTO logs (id, timestamp, type, message, deviceId) VALUES (?, ?, ?, ?, ?)')
|
db.prepare('INSERT INTO logs (id, timestamp, type, message, deviceId) VALUES (?, ?, ?, ?, ?)')
|
||||||
.run(uid('log'), now, 'status', `CheckMK: ${dev.hostname} (${dev.ip}) not found in monitoring — status set to unknown.`, dev.id);
|
.run(uid('log'), now, 'status', `CheckMK: ${dev.hostname} (${dev.ip}) not found in monitoring — status set to unknown.`, dev.id);
|
||||||
}
|
}
|
||||||
@ -768,7 +770,7 @@ async function startServer() {
|
|||||||
|
|
||||||
const state: number = hostData?.extensions?.state ?? -1;
|
const state: number = hostData?.extensions?.state ?? -1;
|
||||||
const newStatus = state === 0 ? 'online' : state === 1 || state === 2 ? 'offline' : 'unknown';
|
const newStatus = state === 0 ? 'online' : state === 1 || state === 2 ? 'offline' : 'unknown';
|
||||||
db.prepare('UPDATE devices SET status = ?, lastCheckedAt = ? WHERE id = ?').run(newStatus, now, dev.id);
|
db.prepare('UPDATE devices SET status = ?, lastCheckedAt = ?, cmkHostname = ? WHERE id = ?').run(newStatus, now, cmkHost, dev.id);
|
||||||
if (dev.status !== newStatus) {
|
if (dev.status !== newStatus) {
|
||||||
db.prepare('INSERT INTO logs (id, timestamp, type, message, deviceId) VALUES (?, ?, ?, ?, ?)')
|
db.prepare('INSERT INTO logs (id, timestamp, type, message, deviceId) VALUES (?, ?, ?, ?, ?)')
|
||||||
.run(uid('log'), now, 'status', `CheckMK: ${dev.hostname} (${dev.ip}) status changed to ${newStatus} (was: ${dev.status}).`, dev.id);
|
.run(uid('log'), now, 'status', `CheckMK: ${dev.hostname} (${dev.ip}) status changed to ${newStatus} (was: ${dev.status}).`, dev.id);
|
||||||
|
|||||||
@ -54,6 +54,7 @@ export default function App() {
|
|||||||
|
|
||||||
const [inventoryHighlightDevice, setInventoryHighlightDevice] = useState<Device | null>(null);
|
const [inventoryHighlightDevice, setInventoryHighlightDevice] = useState<Device | null>(null);
|
||||||
const [checkmkEnabled, setCheckmkEnabled] = useState(false);
|
const [checkmkEnabled, setCheckmkEnabled] = useState(false);
|
||||||
|
const [checkmkBaseUrl, setCheckmkBaseUrl] = useState('');
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const root = document.documentElement;
|
const root = document.documentElement;
|
||||||
@ -142,7 +143,7 @@ export default function App() {
|
|||||||
if (bookingsRes.ok) setBookings(await bookingsRes.json());
|
if (bookingsRes.ok) setBookings(await bookingsRes.json());
|
||||||
if (logsRes.ok) setLogs(await logsRes.json());
|
if (logsRes.ok) setLogs(await logsRes.json());
|
||||||
if (linksRes.ok) setLinks(await linksRes.json());
|
if (linksRes.ok) setLinks(await linksRes.json());
|
||||||
if (configRes.ok) { const cfg = await configRes.json(); setCheckmkEnabled(!!cfg.checkmkEnabled); }
|
if (configRes.ok) { const cfg = await configRes.json(); setCheckmkEnabled(!!cfg.checkmkEnabled); setCheckmkBaseUrl(cfg.checkmkBaseUrl || ''); }
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('[App] Failed to load data:', err);
|
console.error('[App] Failed to load data:', err);
|
||||||
} finally {
|
} finally {
|
||||||
@ -584,6 +585,7 @@ export default function App() {
|
|||||||
<DeviceInventory
|
<DeviceInventory
|
||||||
devices={devices}
|
devices={devices}
|
||||||
checkmkEnabled={checkmkEnabled}
|
checkmkEnabled={checkmkEnabled}
|
||||||
|
checkmkBaseUrl={checkmkBaseUrl}
|
||||||
onAddDevice={handleAddDevice}
|
onAddDevice={handleAddDevice}
|
||||||
onUpdateDevice={handleUpdateDevice}
|
onUpdateDevice={handleUpdateDevice}
|
||||||
onDeleteDevice={handleDeleteDevice}
|
onDeleteDevice={handleDeleteDevice}
|
||||||
|
|||||||
@ -7,7 +7,7 @@ import React, { useState, useMemo } from 'react';
|
|||||||
import { Device, DeviceType } from '../types';
|
import { Device, DeviceType } from '../types';
|
||||||
import {
|
import {
|
||||||
Server, Search, Plus, Trash, Edit2, MapPin, Gauge,
|
Server, Search, Plus, Trash, Edit2, MapPin, Gauge,
|
||||||
BookOpen, Save, X, Info
|
BookOpen, Save, X, Info, ExternalLink
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
|
||||||
// Built-in device class presets shown in the dropdown.
|
// Built-in device class presets shown in the dropdown.
|
||||||
@ -16,6 +16,7 @@ const DEVICE_CLASS_PRESETS = ['Switch', 'Firewall', 'Access-Point', 'Controller'
|
|||||||
interface DeviceInventoryProps {
|
interface DeviceInventoryProps {
|
||||||
devices: Device[];
|
devices: Device[];
|
||||||
checkmkEnabled: boolean;
|
checkmkEnabled: boolean;
|
||||||
|
checkmkBaseUrl: string;
|
||||||
onAddDevice: (device: Omit<Device, 'id'>) => void;
|
onAddDevice: (device: Omit<Device, 'id'>) => void;
|
||||||
onUpdateDevice: (device: Device) => void;
|
onUpdateDevice: (device: Device) => void;
|
||||||
onDeleteDevice: (id: string) => void;
|
onDeleteDevice: (id: string) => void;
|
||||||
@ -24,6 +25,7 @@ interface DeviceInventoryProps {
|
|||||||
export default function DeviceInventory({
|
export default function DeviceInventory({
|
||||||
devices,
|
devices,
|
||||||
checkmkEnabled,
|
checkmkEnabled,
|
||||||
|
checkmkBaseUrl,
|
||||||
onAddDevice,
|
onAddDevice,
|
||||||
onUpdateDevice,
|
onUpdateDevice,
|
||||||
onDeleteDevice,
|
onDeleteDevice,
|
||||||
@ -62,6 +64,10 @@ export default function DeviceInventory({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const effectiveStatus = (d: Device): 'online' | 'offline' | 'unknown' => d.status;
|
const effectiveStatus = (d: Device): 'online' | 'offline' | 'unknown' => d.status;
|
||||||
|
const cmkHostUrl = (d: Device) =>
|
||||||
|
checkmkEnabled && checkmkBaseUrl && d.cmkHostname
|
||||||
|
? `${checkmkBaseUrl}/index.py?host=${encodeURIComponent(d.cmkHostname)}`
|
||||||
|
: null;
|
||||||
|
|
||||||
const statusMeta = (s: 'online' | 'offline' | 'unknown') => {
|
const statusMeta = (s: 'online' | 'offline' | 'unknown') => {
|
||||||
if (s === 'online') return { label: 'online', badge: 'bg-emerald-950/60 border-emerald-900/80 text-emerald-400', dot: 'bg-emerald-400' };
|
if (s === 'online') return { label: 'online', badge: 'bg-emerald-950/60 border-emerald-900/80 text-emerald-400', dot: 'bg-emerald-400' };
|
||||||
@ -330,6 +336,17 @@ export default function DeviceInventory({
|
|||||||
|
|
||||||
{/* Action Panel */}
|
{/* Action Panel */}
|
||||||
<div className="flex items-center gap-1.5 border-l border-slate-800/80 pl-3">
|
<div className="flex items-center gap-1.5 border-l border-slate-800/80 pl-3">
|
||||||
|
{cmkHostUrl(device) && (
|
||||||
|
<a
|
||||||
|
href={cmkHostUrl(device)!}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="p-1 px-1.5 rounded hover:bg-slate-800 text-slate-400 hover:text-cyan-400 transition-colors"
|
||||||
|
title="Open host in CheckMK"
|
||||||
|
>
|
||||||
|
<ExternalLink className="w-3.5 h-3.5" />
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
<button
|
<button
|
||||||
onClick={() => handleOpenEdit(device)}
|
onClick={() => handleOpenEdit(device)}
|
||||||
className="p-1 px-1.5 rounded hover:bg-slate-800 text-slate-400 hover:text-indigo-400 transition-colors"
|
className="p-1 px-1.5 rounded hover:bg-slate-800 text-slate-400 hover:text-indigo-400 transition-colors"
|
||||||
@ -400,6 +417,17 @@ export default function DeviceInventory({
|
|||||||
</span>
|
</span>
|
||||||
); })()}
|
); })()}
|
||||||
</div>
|
</div>
|
||||||
|
{cmkHostUrl(selectedDevice) && (
|
||||||
|
<a
|
||||||
|
href={cmkHostUrl(selectedDevice)!}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="flex items-center justify-center gap-1.5 px-3 py-1.5 bg-slate-900 border border-slate-700 text-slate-200 hover:text-cyan-400 hover:border-cyan-500 rounded text-xs transition-colors font-mono"
|
||||||
|
>
|
||||||
|
<ExternalLink className="w-3.5 h-3.5" />
|
||||||
|
Open host in CheckMK
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
{selectedDevice.lastCheckedAt && (
|
{selectedDevice.lastCheckedAt && (
|
||||||
<p className="text-[10px] text-slate-500 font-mono">
|
<p className="text-[10px] text-slate-500 font-mono">
|
||||||
Last checked: {new Date(selectedDevice.lastCheckedAt).toLocaleString()}
|
Last checked: {new Date(selectedDevice.lastCheckedAt).toLocaleString()}
|
||||||
|
|||||||
@ -21,6 +21,7 @@ interface LogbookProps {
|
|||||||
export default function Logbook({ logs, devices, users, currentUser, onAddLog }: LogbookProps) {
|
export default function Logbook({ logs, devices, users, currentUser, onAddLog }: LogbookProps) {
|
||||||
const [searchTerm, setSearchTerm] = useState('');
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
const [typeFilter, setTypeFilter] = useState<string>('all');
|
const [typeFilter, setTypeFilter] = useState<string>('all');
|
||||||
|
const [showSystem, setShowSystem] = useState(false);
|
||||||
|
|
||||||
// Custom Maintenance Log state
|
// Custom Maintenance Log state
|
||||||
const [showAddLog, setShowAddLog] = useState(false);
|
const [showAddLog, setShowAddLog] = useState(false);
|
||||||
@ -32,6 +33,7 @@ export default function Logbook({ logs, devices, users, currentUser, onAddLog }:
|
|||||||
|
|
||||||
// Filter logs
|
// Filter logs
|
||||||
const filteredLogs = sortedLogs.filter(log => {
|
const filteredLogs = sortedLogs.filter(log => {
|
||||||
|
if (!showSystem && log.type === 'system') return false;
|
||||||
const matchesSearch = log.message.toLowerCase().includes(searchTerm.toLowerCase());
|
const matchesSearch = log.message.toLowerCase().includes(searchTerm.toLowerCase());
|
||||||
const matchesType = typeFilter === 'all' || log.type === typeFilter;
|
const matchesType = typeFilter === 'all' || log.type === typeFilter;
|
||||||
return matchesSearch && matchesType;
|
return matchesSearch && matchesType;
|
||||||
@ -121,8 +123,8 @@ export default function Logbook({ logs, devices, users, currentUser, onAddLog }:
|
|||||||
className="w-full bg-slate-950 text-slate-101 border border-slate-800 rounded-lg pl-9 pr-4 py-1.5 text-xs focus:outline-none"
|
className="w-full bg-slate-950 text-slate-101 border border-slate-800 rounded-lg pl-9 pr-4 py-1.5 text-xs focus:outline-none"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-1 shrink-0 text-xs font-medium">
|
<div className="flex gap-1 shrink-0 text-xs font-medium flex-wrap">
|
||||||
{['all', 'booking', 'maintenance'].map((type) => (
|
{['all', 'booking', 'maintenance', 'status'].map((type) => (
|
||||||
<button
|
<button
|
||||||
key={type}
|
key={type}
|
||||||
onClick={() => setTypeFilter(type)}
|
onClick={() => setTypeFilter(type)}
|
||||||
@ -135,6 +137,17 @@ export default function Logbook({ logs, devices, users, currentUser, onAddLog }:
|
|||||||
{type === 'all' ? 'All' : getLogTypeLabel(type)}
|
{type === 'all' ? 'All' : getLogTypeLabel(type)}
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
|
<button
|
||||||
|
onClick={() => setShowSystem(v => !v)}
|
||||||
|
className={`px-3 py-1.5 rounded-lg text-xs font-medium transition-all ${
|
||||||
|
showSystem
|
||||||
|
? 'bg-slate-700/40 border border-slate-600 text-slate-300'
|
||||||
|
: 'bg-slate-950 text-slate-600 border border-slate-850 hover:text-slate-400'
|
||||||
|
}`}
|
||||||
|
title="Show/hide automated system & CheckMK entries"
|
||||||
|
>
|
||||||
|
System
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -16,6 +16,7 @@ export interface Device {
|
|||||||
type: DeviceType;
|
type: DeviceType;
|
||||||
status: 'online' | 'offline' | 'unknown';
|
status: 'online' | 'offline' | 'unknown';
|
||||||
emergencySheet: string; // Markdown text
|
emergencySheet: string; // Markdown text
|
||||||
|
cmkHostname?: string;
|
||||||
lastCheckedAt?: string;
|
lastCheckedAt?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user