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,326 @@
/**
* BulkDeployDialog -- Multi-device certificate deployment dialog.
*
* Shows a checkbox list of devices without deployed certs, with Select All / Deselect All.
* On deploy, calls bulkDeploy API and shows progress + results summary.
*/
import { useState } from 'react'
import { useQuery, useQueryClient } from '@tanstack/react-query'
import {
Layers,
Loader2,
CheckCircle,
XCircle,
Check,
} from 'lucide-react'
import { certificatesApi } from '@/lib/certificatesApi'
import { devicesApi, type DeviceResponse } from '@/lib/api'
import { Button } from '@/components/ui/button'
import { Checkbox } from '@/components/ui/checkbox'
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import { toast } from '@/components/ui/toast'
import { cn } from '@/lib/utils'
type BulkStep = 'select' | 'deploying' | 'results'
interface BulkResult {
success: number
failed: number
errors: Array<{ device_id: string; error: string }>
}
interface BulkDeployDialogProps {
open: boolean
onClose: () => void
tenantId: string
}
export function BulkDeployDialog({
open,
onClose,
tenantId,
}: BulkDeployDialogProps) {
const queryClient = useQueryClient()
const [selected, setSelected] = useState<Set<string>>(new Set())
const [step, setStep] = useState<BulkStep>('select')
const [result, setResult] = useState<BulkResult | null>(null)
// Fetch devices
const { data: deviceList = [] } = useQuery({
queryKey: ['devices-for-cert', tenantId],
queryFn: async () => {
const result = await devicesApi.list(tenantId)
return (result as any).items ?? result
},
enabled: !!tenantId && open,
})
// Fetch existing device certs to filter
const { data: existingCerts = [] } = useQuery({
queryKey: ['deviceCerts', tenantId],
queryFn: () => certificatesApi.getDeviceCerts(undefined, tenantId),
enabled: !!tenantId && open,
})
const deployedDeviceIds = new Set(
existingCerts
.filter((c) => c.status === 'deployed' || c.status === 'deploying')
.map((c) => c.device_id),
)
const availableDevices = (deviceList as DeviceResponse[]).filter(
(d) => !deployedDeviceIds.has(d.id),
)
const toggleDevice = (id: string) => {
setSelected((prev) => {
const next = new Set(prev)
if (next.has(id)) {
next.delete(id)
} else {
next.add(id)
}
return next
})
}
const selectAll = () => {
setSelected(new Set(availableDevices.map((d) => d.id)))
}
const deselectAll = () => {
setSelected(new Set())
}
const handleDeploy = async () => {
if (selected.size === 0) return
setStep('deploying')
try {
const responses = await certificatesApi.bulkDeploy(Array.from(selected), tenantId)
const succeeded = responses.filter((r) => r.success).length
const failed = responses.filter((r) => !r.success)
const bulkResult: BulkResult = {
success: succeeded,
failed: failed.length,
errors: failed.map((f) => ({
device_id: f.device_id,
error: f.error ?? 'Unknown error',
})),
}
setResult(bulkResult)
setStep('results')
void queryClient.invalidateQueries({ queryKey: ['deviceCerts'] })
if (failed.length === 0) {
toast({ title: `${succeeded} certificate(s) deployed successfully` })
} else {
toast({
title: `${succeeded} deployed, ${failed.length} failed`,
variant: 'destructive',
})
}
} catch (e: any) {
setResult({
success: 0,
failed: selected.size,
errors: [
{
device_id: 'bulk',
error: e?.response?.data?.detail || 'Bulk deployment failed',
},
],
})
setStep('results')
toast({ title: 'Bulk deployment failed', variant: 'destructive' })
}
}
const handleClose = () => {
onClose()
setSelected(new Set())
setStep('select')
setResult(null)
}
return (
<Dialog open={open} onOpenChange={(v) => !v && handleClose()}>
<DialogContent className="max-w-lg">
<DialogHeader>
<DialogTitle>Bulk Certificate Deployment</DialogTitle>
</DialogHeader>
<div className="space-y-4 pt-2">
{step === 'select' && (
<>
<p className="text-sm text-text-secondary">
Select devices to sign and deploy TLS certificates in batch.
</p>
{availableDevices.length === 0 ? (
<div className="rounded-lg border border-border bg-elevated/50 p-4 text-center">
<CheckCircle className="h-6 w-6 text-green-500 mx-auto mb-2" />
<p className="text-sm font-medium text-text-primary">
All devices have certificates
</p>
<p className="text-xs text-text-muted mt-1">
Every device already has a deployed certificate.
</p>
</div>
) : (
<>
{/* Select All / Deselect All */}
<div className="flex items-center justify-between">
<span className="text-xs text-text-muted">
{selected.size} of {availableDevices.length} selected
</span>
<div className="flex gap-2">
<button
className="text-xs text-accent hover:underline"
onClick={selectAll}
>
Select All
</button>
<button
className="text-xs text-text-muted hover:underline"
onClick={deselectAll}
>
Deselect All
</button>
</div>
</div>
{/* Device list */}
<div className="max-h-64 overflow-y-auto rounded-lg border border-border divide-y divide-border">
{availableDevices.map((d: DeviceResponse) => (
<label
key={d.id}
className="flex items-center gap-3 px-3 py-2.5 hover:bg-elevated/30 cursor-pointer transition-colors"
>
<Checkbox
checked={selected.has(d.id)}
onCheckedChange={() => toggleDevice(d.id)}
/>
<div className="min-w-0 flex-1">
<span className="text-sm font-medium text-text-primary block truncate">
{d.hostname}
</span>
<span className="text-xs text-text-muted">
{d.ip_address}
</span>
</div>
<span
className={cn(
'text-[10px] uppercase px-1.5 py-0.5 rounded',
d.status === 'online'
? 'bg-green-500/10 text-green-500'
: 'bg-text-muted/10 text-text-muted',
)}
>
{d.status}
</span>
</label>
))}
</div>
<Button
className="w-full"
disabled={selected.size === 0}
onClick={handleDeploy}
>
<Layers className="h-4 w-4 mr-2" />
Deploy to {selected.size} device
{selected.size !== 1 ? 's' : ''}
</Button>
</>
)}
</>
)}
{step === 'deploying' && (
<div className="py-8 text-center space-y-3">
<Loader2 className="h-8 w-8 text-accent mx-auto animate-spin" />
<p className="text-sm font-medium text-text-primary">
Deploying certificates...
</p>
<p className="text-xs text-text-muted">
Signing and deploying to {selected.size} device
{selected.size !== 1 ? 's' : ''}. This may take a moment.
</p>
</div>
)}
{step === 'results' && result && (
<div className="space-y-4">
{/* Summary */}
<div className="grid grid-cols-2 gap-3">
<div className="rounded-lg border border-green-500/30 bg-green-500/5 p-4 text-center">
<CheckCircle className="h-6 w-6 text-green-500 mx-auto mb-1" />
<p className="text-2xl font-bold text-green-500">
{result.success}
</p>
<p className="text-xs text-text-muted">Succeeded</p>
</div>
<div
className={cn(
'rounded-lg border p-4 text-center',
result.failed > 0
? 'border-error/30 bg-error/5'
: 'border-border bg-surface',
)}
>
<XCircle
className={cn(
'h-6 w-6 mx-auto mb-1',
result.failed > 0 ? 'text-error' : 'text-text-muted',
)}
/>
<p
className={cn(
'text-2xl font-bold',
result.failed > 0 ? 'text-error' : 'text-text-muted',
)}
>
{result.failed}
</p>
<p className="text-xs text-text-muted">Failed</p>
</div>
</div>
{/* Error details */}
{result.errors.length > 0 && (
<div className="rounded-lg border border-error/30 bg-error/5 p-3 space-y-2">
<p className="text-xs font-medium text-error">
Failed deployments:
</p>
{result.errors.map((err, i) => (
<div
key={i}
className="text-xs text-text-secondary flex items-start gap-2"
>
<XCircle className="h-3 w-3 text-error mt-0.5 flex-shrink-0" />
<span>{err.error}</span>
</div>
))}
</div>
)}
<Button className="w-full" onClick={handleClose}>
<Check className="h-4 w-4 mr-2" />
Done
</Button>
</div>
)}
</div>
</DialogContent>
</Dialog>
)
}