feat: The Other Dude v9.0.1 — full-featured email system

ci: add GitHub Pages deployment workflow for docs site

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jason Staack
2026-03-08 17:46:37 -05:00
commit b840047e19
511 changed files with 106948 additions and 0 deletions

View File

@@ -0,0 +1,322 @@
/**
* MaintenanceForm -- Dialog-based form for creating/editing maintenance windows.
*
* Supports device multi-select (or "All Devices" checkbox), datetime range,
* suppress alerts toggle, and notes.
*/
import { useState, useEffect } from 'react'
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogFooter,
DialogDescription,
} from '@/components/ui/dialog'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Checkbox } from '@/components/ui/checkbox'
import { toast } from '@/components/ui/toast'
import {
maintenanceApi,
devicesApi,
type MaintenanceWindow,
type MaintenanceWindowCreate,
} from '@/lib/api'
interface MaintenanceFormProps {
tenantId: string
open: boolean
onOpenChange: (open: boolean) => void
editWindow?: MaintenanceWindow | null
}
export function MaintenanceForm({
tenantId,
open,
onOpenChange,
editWindow,
}: MaintenanceFormProps) {
const queryClient = useQueryClient()
const isEdit = !!editWindow
const [name, setName] = useState('')
const [startAt, setStartAt] = useState('')
const [endAt, setEndAt] = useState('')
const [suppressAlerts, setSuppressAlerts] = useState(true)
const [notes, setNotes] = useState('')
const [allDevices, setAllDevices] = useState(true)
const [selectedDevices, setSelectedDevices] = useState<string[]>([])
// Fetch devices for multi-select
const { data: deviceData } = useQuery({
queryKey: ['devices', tenantId, 'maintenance-form'],
queryFn: () => devicesApi.list(tenantId, { page_size: 500 }),
enabled: open && !!tenantId,
})
const devices = deviceData?.items ?? []
// Populate form when editing
useEffect(() => {
if (editWindow) {
setName(editWindow.name)
// Convert ISO to datetime-local format
setStartAt(toDatetimeLocal(editWindow.start_at))
setEndAt(toDatetimeLocal(editWindow.end_at))
setSuppressAlerts(editWindow.suppress_alerts)
setNotes(editWindow.notes ?? '')
const hasDevices = editWindow.device_ids.length > 0
setAllDevices(!hasDevices)
setSelectedDevices(hasDevices ? editWindow.device_ids : [])
} else {
resetForm()
}
}, [editWindow, open])
function resetForm() {
setName('')
setStartAt('')
setEndAt('')
setSuppressAlerts(true)
setNotes('')
setAllDevices(true)
setSelectedDevices([])
}
function toDatetimeLocal(iso: string): string {
const d = new Date(iso)
const offset = d.getTimezoneOffset()
const local = new Date(d.getTime() - offset * 60000)
return local.toISOString().slice(0, 16)
}
const createMutation = useMutation({
mutationFn: (data: MaintenanceWindowCreate) =>
maintenanceApi.create(tenantId, data),
onSuccess: () => {
toast({ title: 'Maintenance window created' })
queryClient.invalidateQueries({ queryKey: ['maintenance-windows', tenantId] })
onOpenChange(false)
resetForm()
},
onError: () => {
toast({ title: 'Failed to create maintenance window', variant: 'destructive' })
},
})
const updateMutation = useMutation({
mutationFn: (data: MaintenanceWindowCreate) =>
maintenanceApi.update(tenantId, editWindow!.id, data),
onSuccess: () => {
toast({ title: 'Maintenance window updated' })
queryClient.invalidateQueries({ queryKey: ['maintenance-windows', tenantId] })
onOpenChange(false)
},
onError: () => {
toast({ title: 'Failed to update maintenance window', variant: 'destructive' })
},
})
function handleSubmit(e: React.FormEvent) {
e.preventDefault()
// Validation
if (!name.trim()) {
toast({ title: 'Name is required', variant: 'destructive' })
return
}
if (!startAt || !endAt) {
toast({ title: 'Start and end times are required', variant: 'destructive' })
return
}
if (new Date(endAt) <= new Date(startAt)) {
toast({ title: 'End time must be after start time', variant: 'destructive' })
return
}
if (!allDevices && selectedDevices.length === 0) {
toast({
title: 'Select at least one device or choose "All Devices"',
variant: 'destructive',
})
return
}
const data: MaintenanceWindowCreate = {
name: name.trim(),
device_ids: allDevices ? [] : selectedDevices,
start_at: new Date(startAt).toISOString(),
end_at: new Date(endAt).toISOString(),
suppress_alerts: suppressAlerts,
notes: notes.trim() || undefined,
}
if (isEdit) {
updateMutation.mutate(data)
} else {
createMutation.mutate(data)
}
}
function toggleDevice(deviceId: string) {
setSelectedDevices((prev) =>
prev.includes(deviceId)
? prev.filter((id) => id !== deviceId)
: [...prev, deviceId],
)
}
const isLoading = createMutation.isPending || updateMutation.isPending
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-lg max-h-[85vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>
{isEdit ? 'Edit Maintenance Window' : 'New Maintenance Window'}
</DialogTitle>
<DialogDescription>
Schedule a maintenance window to suppress alerts during planned downtime.
</DialogDescription>
</DialogHeader>
<form onSubmit={handleSubmit} className="space-y-4">
{/* Name */}
<div className="space-y-1.5">
<Label htmlFor="mw-name">Name</Label>
<Input
id="mw-name"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="e.g. Firmware upgrade - Branch offices"
autoFocus
/>
</div>
{/* Start / End */}
<div className="grid grid-cols-2 gap-3">
<div className="space-y-1.5">
<Label htmlFor="mw-start">Start</Label>
<Input
id="mw-start"
type="datetime-local"
value={startAt}
onChange={(e) => setStartAt(e.target.value)}
/>
</div>
<div className="space-y-1.5">
<Label htmlFor="mw-end">End</Label>
<Input
id="mw-end"
type="datetime-local"
value={endAt}
onChange={(e) => setEndAt(e.target.value)}
/>
</div>
</div>
{/* Suppress Alerts */}
<div className="flex items-center gap-2">
<Checkbox
id="mw-suppress"
checked={suppressAlerts}
onCheckedChange={(checked) => setSuppressAlerts(checked === true)}
/>
<Label htmlFor="mw-suppress" className="cursor-pointer">
Suppress alerts during this window
</Label>
</div>
{/* Device Selection */}
<div className="space-y-2">
<Label>Devices</Label>
<div className="flex items-center gap-2 mb-2">
<Checkbox
id="mw-all-devices"
checked={allDevices}
onCheckedChange={(checked) => {
setAllDevices(checked === true)
if (checked) setSelectedDevices([])
}}
/>
<Label htmlFor="mw-all-devices" className="cursor-pointer">
All devices in tenant
</Label>
</div>
{!allDevices && (
<div className="max-h-40 overflow-y-auto rounded-md border border-border bg-elevated/30 p-2 space-y-1">
{devices.length === 0 ? (
<p className="text-xs text-text-muted py-2 text-center">
No devices found
</p>
) : (
devices.map((device) => (
<div
key={device.id}
className="flex items-center gap-2 px-1 py-0.5 rounded hover:bg-elevated/50"
>
<Checkbox
id={`dev-${device.id}`}
checked={selectedDevices.includes(device.id)}
onCheckedChange={() => toggleDevice(device.id)}
/>
<Label
htmlFor={`dev-${device.id}`}
className="cursor-pointer flex-1 text-xs"
>
{device.hostname}{' '}
<span className="text-text-muted">({device.ip_address})</span>
</Label>
</div>
))
)}
</div>
)}
{!allDevices && selectedDevices.length > 0 && (
<p className="text-xs text-text-muted">
{selectedDevices.length} device{selectedDevices.length !== 1 ? 's' : ''} selected
</p>
)}
</div>
{/* Notes */}
<div className="space-y-1.5">
<Label htmlFor="mw-notes">Notes (optional)</Label>
<textarea
id="mw-notes"
value={notes}
onChange={(e) => setNotes(e.target.value)}
placeholder="Reason for maintenance, ticket number, etc."
rows={2}
className="w-full rounded-md border border-border bg-elevated/50 px-3 py-2 text-sm text-text-primary placeholder:text-text-muted focus:outline-none focus:ring-1 focus:ring-ring resize-none"
/>
</div>
<DialogFooter>
<Button
type="button"
variant="outline"
onClick={() => onOpenChange(false)}
disabled={isLoading}
>
Cancel
</Button>
<Button type="submit" disabled={isLoading}>
{isLoading
? isEdit
? 'Updating...'
: 'Creating...'
: isEdit
? 'Update Window'
: 'Create Window'}
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
)
}

View File

@@ -0,0 +1,342 @@
/**
* MaintenanceList -- Three-section layout showing active, upcoming, and past
* maintenance windows with create/edit/delete actions.
*/
import { useState } from 'react'
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import {
Calendar,
Plus,
Pencil,
Trash2,
BellOff,
Bell,
Monitor,
} from 'lucide-react'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogFooter,
DialogDescription,
} from '@/components/ui/dialog'
import { toast } from '@/components/ui/toast'
import { EmptyState } from '@/components/ui/empty-state'
import { maintenanceApi, type MaintenanceWindow } from '@/lib/api'
import { MaintenanceForm } from './MaintenanceForm'
interface MaintenanceListProps {
tenantId: string
}
export function MaintenanceList({ tenantId }: MaintenanceListProps) {
const queryClient = useQueryClient()
const [formOpen, setFormOpen] = useState(false)
const [editWindow, setEditWindow] = useState<MaintenanceWindow | null>(null)
const [deleteTarget, setDeleteTarget] = useState<MaintenanceWindow | null>(null)
const { data: windows, isLoading } = useQuery({
queryKey: ['maintenance-windows', tenantId],
queryFn: () => maintenanceApi.list(tenantId),
enabled: !!tenantId,
})
const deleteMutation = useMutation({
mutationFn: (windowId: string) => maintenanceApi.delete(tenantId, windowId),
onSuccess: () => {
toast({ title: 'Maintenance window deleted' })
queryClient.invalidateQueries({ queryKey: ['maintenance-windows', tenantId] })
setDeleteTarget(null)
},
onError: () => {
toast({ title: 'Failed to delete maintenance window', variant: 'destructive' })
},
})
const now = new Date()
const active = (windows ?? []).filter(
(w) => new Date(w.start_at) <= now && new Date(w.end_at) >= now,
)
const upcoming = (windows ?? []).filter((w) => new Date(w.start_at) > now)
const past = (windows ?? [])
.filter((w) => new Date(w.end_at) < now)
.slice(0, 20)
function openEdit(w: MaintenanceWindow) {
setEditWindow(w)
setFormOpen(true)
}
function openCreate() {
setEditWindow(null)
setFormOpen(true)
}
function formatRange(startAt: string, endAt: string): string {
const start = new Date(startAt)
const end = new Date(endAt)
const opts: Intl.DateTimeFormatOptions = {
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
}
return `${start.toLocaleDateString(undefined, opts)} - ${end.toLocaleDateString(undefined, opts)}`
}
function formatDuration(startAt: string, endAt: string): string {
const ms = new Date(endAt).getTime() - new Date(startAt).getTime()
const hours = Math.floor(ms / 3600000)
const mins = Math.floor((ms % 3600000) / 60000)
if (hours > 0) return `${hours}h ${mins}m`
return `${mins}m`
}
if (isLoading) {
return (
<div className="space-y-4">
{[1, 2, 3].map((i) => (
<div
key={i}
className="h-20 rounded-lg border border-border bg-surface animate-pulse"
/>
))}
</div>
)
}
return (
<div className="space-y-6">
{/* Header */}
<div className="flex items-center justify-between">
<p className="text-sm text-text-muted">
{(windows ?? []).length} maintenance window{(windows ?? []).length !== 1 ? 's' : ''}
</p>
<Button size="sm" onClick={openCreate}>
<Plus className="h-4 w-4 mr-1" />
New Window
</Button>
</div>
{/* Active */}
{active.length > 0 && (
<Section title="Active" count={active.length} color="border-l-success">
{active.map((w) => (
<WindowCard
key={w.id}
window={w}
variant="active"
onEdit={() => openEdit(w)}
onDelete={() => setDeleteTarget(w)}
formatRange={formatRange}
formatDuration={formatDuration}
/>
))}
</Section>
)}
{/* Upcoming */}
{upcoming.length > 0 && (
<Section title="Upcoming" count={upcoming.length} color="border-l-warning">
{upcoming.map((w) => (
<WindowCard
key={w.id}
window={w}
variant="upcoming"
onEdit={() => openEdit(w)}
onDelete={() => setDeleteTarget(w)}
formatRange={formatRange}
formatDuration={formatDuration}
/>
))}
</Section>
)}
{/* Past */}
{past.length > 0 && (
<Section title="Past" count={past.length} color="border-l-border">
{past.map((w) => (
<WindowCard
key={w.id}
window={w}
variant="past"
onEdit={() => openEdit(w)}
onDelete={() => setDeleteTarget(w)}
formatRange={formatRange}
formatDuration={formatDuration}
/>
))}
</Section>
)}
{/* Empty state */}
{(windows ?? []).length === 0 && (
<EmptyState
icon={Calendar}
title="No maintenance windows"
description="Schedule maintenance windows to suppress alerts during planned work."
action={{ label: 'Create Window', onClick: openCreate }}
/>
)}
{/* Form dialog */}
<MaintenanceForm
tenantId={tenantId}
open={formOpen}
onOpenChange={setFormOpen}
editWindow={editWindow}
/>
{/* Delete confirmation dialog */}
<Dialog
open={!!deleteTarget}
onOpenChange={(open) => !open && setDeleteTarget(null)}
>
<DialogContent className="max-w-sm">
<DialogHeader>
<DialogTitle>Delete Maintenance Window</DialogTitle>
<DialogDescription>
Are you sure you want to delete "{deleteTarget?.name}"? This action cannot
be undone.
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button variant="outline" onClick={() => setDeleteTarget(null)}>
Cancel
</Button>
<Button
variant="destructive"
onClick={() => deleteTarget && deleteMutation.mutate(deleteTarget.id)}
disabled={deleteMutation.isPending}
>
{deleteMutation.isPending ? 'Deleting...' : 'Delete'}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
)
}
// ─── Sub-components ─────────────────────────────────────────────────────────
function Section({
title,
count,
color,
children,
}: {
title: string
count: number
color: string
children: React.ReactNode
}) {
return (
<div>
<h3 className="text-xs font-semibold uppercase tracking-wider text-text-muted mb-2">
{title}{' '}
<span className="text-text-muted/60">({count})</span>
</h3>
<div className={`space-y-2 border-l-2 ${color} pl-3`}>{children}</div>
</div>
)
}
function WindowCard({
window: w,
variant,
onEdit,
onDelete,
formatRange,
formatDuration,
}: {
window: MaintenanceWindow
variant: 'active' | 'upcoming' | 'past'
onEdit: () => void
onDelete: () => void
formatRange: (s: string, e: string) => string
formatDuration: (s: string, e: string) => string
}) {
const isPast = variant === 'past'
return (
<div
className={`rounded-lg border border-border bg-surface p-3 ${
isPast ? 'opacity-60' : ''
}`}
>
<div className="flex items-start justify-between gap-2">
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1">
{variant === 'active' && (
<span className="relative flex h-2 w-2">
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-success opacity-75" />
<span className="relative inline-flex h-2 w-2 rounded-full bg-success" />
</span>
)}
<span className="text-sm font-medium text-text-primary truncate">
{w.name}
</span>
</div>
<p className="text-xs text-text-muted mb-1.5">
{formatRange(w.start_at, w.end_at)}{' '}
<span className="text-text-muted/60">
({formatDuration(w.start_at, w.end_at)})
</span>
</p>
<div className="flex items-center gap-2 flex-wrap">
{/* Device count */}
<Badge variant="outline" className="text-[10px] gap-1">
<Monitor className="h-3 w-3" />
{w.device_ids.length === 0
? 'All Devices'
: `${w.device_ids.length} device${w.device_ids.length !== 1 ? 's' : ''}`}
</Badge>
{/* Suppress status */}
{w.suppress_alerts ? (
<Badge variant="outline" className="text-[10px] gap-1 text-warning">
<BellOff className="h-3 w-3" />
Alerts Suppressed
</Badge>
) : (
<Badge variant="outline" className="text-[10px] gap-1 text-text-muted">
<Bell className="h-3 w-3" />
Alerts Active
</Badge>
)}
</div>
{w.notes && (
<p className="text-xs text-text-muted mt-1.5 line-clamp-1">{w.notes}</p>
)}
</div>
{/* Actions */}
<div className="flex items-center gap-1 flex-shrink-0">
<button
onClick={onEdit}
className="p-1.5 rounded-md text-text-muted hover:text-text-primary hover:bg-elevated/50 transition-colors"
title="Edit"
>
<Pencil className="h-3.5 w-3.5" />
</button>
<button
onClick={onDelete}
className="p-1.5 rounded-md text-text-muted hover:text-error hover:bg-error/10 transition-colors"
title="Delete"
>
<Trash2 className="h-3.5 w-3.5" />
</button>
</div>
</div>
</div>
)
}