diff --git a/app/api/services/route.ts b/app/api/services/route.ts index a40f427..e8ec359 100644 --- a/app/api/services/route.ts +++ b/app/api/services/route.ts @@ -43,6 +43,39 @@ export async function POST(request: Request) { } } +export async function PATCH(request: Request) { + try { + const { id, name } = await request.json(); + + if (!id || !name) { + return NextResponse.json({ error: 'ID and new name are required' }, { status: 400 }); + } + + const db = await getDb(); + + const service = await db.get('SELECT name FROM monitored_services WHERE id = ?', id); + if (!service) { + return NextResponse.json({ error: 'Service not found' }, { status: 404 }); + } + + const oldName = service.name; + const newName = name.trim(); + + await db.run('UPDATE monitored_services SET name = ? WHERE id = ?', newName, id); + // Update existing logs to match the new name + await db.run('UPDATE uptime_logs SET service_name = ? WHERE service_name = ?', newName, oldName); + + return NextResponse.json({ success: true }); + } catch (error: unknown) { + const msg = error instanceof Error ? error.message : 'Unknown error'; + if (msg.includes('UNIQUE constraint')) { + return NextResponse.json({ error: 'A service with that name already exists' }, { status: 409 }); + } + console.error('Service rename error:', error); + return NextResponse.json({ error: 'Failed to rename service' }, { status: 500 }); + } +} + export async function DELETE(request: Request) { try { const { id } = await request.json(); diff --git a/app/globals.css b/app/globals.css index a2dc41e..5413518 100644 --- a/app/globals.css +++ b/app/globals.css @@ -24,3 +24,22 @@ body { color: var(--foreground); font-family: Arial, Helvetica, sans-serif; } + +/* Thin scrollbar for widget overflow areas */ +.scrollbar-thin { + scrollbar-width: thin; + scrollbar-color: #404040 transparent; +} +.scrollbar-thin::-webkit-scrollbar { + width: 4px; +} +.scrollbar-thin::-webkit-scrollbar-track { + background: transparent; +} +.scrollbar-thin::-webkit-scrollbar-thumb { + background: #404040; + border-radius: 2px; +} +.scrollbar-thin::-webkit-scrollbar-thumb:hover { + background: #525252; +} diff --git a/components/widgets/UptimeCard.tsx b/components/widgets/UptimeCard.tsx index c9ddb5a..b07cbdc 100644 --- a/components/widgets/UptimeCard.tsx +++ b/components/widgets/UptimeCard.tsx @@ -1,6 +1,6 @@ 'use client'; -import { Activity, RefreshCcw, AlertCircle, X, Plus, Trash2, ExternalLink } from 'lucide-react'; +import { Activity, RefreshCcw, AlertCircle, X, Plus, Trash2, ExternalLink, ChevronDown, ChevronUp, Pencil, Check } from 'lucide-react'; import { useState, useEffect, useCallback, useRef } from 'react'; interface ServiceStatus { @@ -15,12 +15,10 @@ interface ServiceStatus { history?: Array<{ hour: string; up: boolean }>; } -// Build exactly 24 bars from whatever history data exists function build24Bars(history: Array<{ hour: string; up: boolean }> | undefined) { const bars: Array<{ hour: string; status: 'up' | 'down' | 'unknown' }> = []; const now = new Date(); - // Create a map of hour-string -> up/down from available data const hourMap = new Map(); if (history) { for (const h of history) { @@ -28,12 +26,10 @@ function build24Bars(history: Array<{ hour: string; up: boolean }> | undefined) } } - // Generate 24 hour slots ending at current hour for (let i = 23; i >= 0; i--) { const d = new Date(now); d.setMinutes(0, 0, 0); d.setHours(d.getHours() - i); - // Format to match the API: "YYYY-MM-DD HH:00" const key = d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0') + '-' + String(d.getDate()).padStart(2, '0') + ' ' + @@ -71,14 +67,99 @@ function HistoryBars({ history, large }: { history?: Array<{ hour: string; up: b ); } +function InlineNameEditor({ service, onRenamed }: { service: ServiceStatus; onRenamed: () => void }) { + const [editing, setEditing] = useState(false); + const [name, setName] = useState(service.name); + const [saving, setSaving] = useState(false); + const inputRef = useRef(null); + + useEffect(() => { + if (editing) inputRef.current?.focus(); + }, [editing]); + + const save = async () => { + const trimmed = name.trim(); + if (!trimmed || trimmed === service.name) { + setName(service.name); + setEditing(false); + return; + } + setSaving(true); + try { + const listRes = await fetch('/api/services'); + const list = await listRes.json(); + const svc = list.find((s: { name: string }) => s.name === service.name); + if (!svc) return; + + const res = await fetch('/api/services', { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ id: svc.id, name: trimmed }), + }); + if (res.ok) { + setEditing(false); + onRenamed(); + } else { + const data = await res.json(); + alert(data.error || 'Rename failed'); + setName(service.name); + setEditing(false); + } + } catch { + setName(service.name); + setEditing(false); + } finally { + setSaving(false); + } + }; + + if (!editing) { + return ( +
+ {service.name} + +
+ ); + } + + return ( +
e.stopPropagation()}> + setName(e.target.value)} + onKeyDown={(e) => { + if (e.key === 'Enter') save(); + if (e.key === 'Escape') { setName(service.name); setEditing(false); } + }} + onBlur={save} + disabled={saving} + className="bg-neutral-800 border border-neutral-600 rounded px-1.5 py-0.5 text-sm text-white font-medium focus:outline-none focus:border-neutral-400 w-24 min-w-0" + /> + +
+ ); +} + function DetailModal({ service, onClose, onDelete, + onRenamed, }: { service: ServiceStatus; onClose: () => void; onDelete: (name: string) => void; + onRenamed: () => void; }) { const overlayRef = useRef(null); @@ -102,8 +183,31 @@ function DetailModal({ {/* Header */}
-
+

{service.name}

+
{error ? ( -
+
Failed to load status
) : ( -
+ /* Scrollable content area — fills remaining space */ +
{services.map((service) => (
setSelectedService(service)} > -
- {service.name} -
+
+ +
{service.status === 'up' ? 'UP' : 'DOWN'} @@ -358,6 +474,7 @@ export function UptimeCard() { service={selectedService} onClose={() => setSelectedService(null)} onDelete={handleDelete} + onRenamed={fetchStatus} /> )}