Address code review findings across API, web and deployment
release gate / check (push) Failing after 6m43s

High severity:
- wire TRUSTED_PROXY through Compose/deploy/docs and warn at web startup
- bucket public rate limits on effective client IP, add per-bookmark budget,
  and forward the signed browser identity from the share page
- disable first-run bootstrap in server deployments and document seeding
- serialize last-admin demotion in one advisory-locked transaction

Medium/low:
- cap and batch share recipient inserts
- add a shared outbound budget for preview/CardDAV/IMAP/signal work
- pin CardDAV dials to 443 and bound IMAP logout
- fix rightward kanban column drags and normalize cards on column delete
- classify backend outages in auth guards, add request timeouts and an
  error boundary, remove the dead login route, surface throttling
- enforce pinned/unpinned page reorder grouping server-side
- avoid preview work for stale bookmark versions; drop duplicate dialog fetch
- sequence-guard collection/tag/calendar/quick-find loads
- validate property config and fix MULTI_SELECT/DATE filter inputs
- document index maintenance; apply same-origin check to sign-out
- trim signup names, unique shared dates and kanban keys (migration 010),
  reset reference cache on identity changes, hide OIDC issuer, return 413
  for oversized direct bodies, escape Markdown link destinations
- remove dead template files and duplicate test helper

Verified with go build/vet, full PostgreSQL race suite, db convergence,
tsc, both ESLint configs and a Next production build.
This commit is contained in:
Andrew S Erwin
2026-10-20 00:13:30 -05:00
parent 5eab58f99c
commit 4e421f6ea2
58 changed files with 1419 additions and 333 deletions
+37 -9
View File
@@ -1,6 +1,6 @@
'use client'
import { useCallback, useEffect, useState } from 'react'
import { useCallback, useEffect, useRef, useState } from 'react'
import Link from 'next/link'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
@@ -109,7 +109,7 @@ function plainValue(prop: CollectionProperty, value: unknown): string {
if (prop.type === 'DATE' && typeof value === 'string' && value) {
if (prop.config.mode === 'date') return value.slice(0, 10)
const date = new Date(value)
return Number.isNaN(date.getTime()) ? value : date.toLocaleString()
return Number.isNaN(date.getTime()) ? value : date.toLocaleString('en-US', { timeZone: 'UTC' })
}
if (value === null || value === undefined || value === '') return '—'
return String(value)
@@ -246,6 +246,12 @@ export function CollectionsClient() {
const [shareQuery, setShareQuery] = useState('')
const [shareResults, setShareResults] = useState<{ id: string; name: string | null; email: string }[]>([])
// Review M-11: switching collections/views quickly used to let an older
// response overwrite the newer selection. Each loader has a request
// sequence and only the latest may apply its result.
const detailSeq = useRef(0)
const itemsSeq = useRef(0)
const loadCollections = useCallback(async () => {
try {
const res = await fetch('/api/collections')
@@ -254,27 +260,33 @@ export function CollectionsClient() {
}, [])
const loadDetail = useCallback(async (id: string) => {
const seq = ++detailSeq.current
try {
const res = await fetch(`/api/collections/${id}`)
if (seq !== detailSeq.current) return
if (!res.ok) { setDetail(null); return }
const data: CollectionDetail = await res.json()
if (seq !== detailSeq.current) return
setDetail(data)
setNameDraft(data.name)
setViewId((current) => (current && data.views.some((v) => v.id === current) ? current : data.views[0]?.id ?? null))
} catch { toast.error('Could not load collection') }
} catch { if (seq === detailSeq.current) toast.error('Could not load collection') }
}, [])
const loadItems = useCallback(async (id: string, view: string | null, append?: string | null) => {
const seq = ++itemsSeq.current
try {
const params = new URLSearchParams({ limit: '200' })
if (view) params.set('viewId', view)
if (append) params.set('cursor', append)
const res = await fetch(`/api/collections/${id}/items?${params}`)
if (seq !== itemsSeq.current) return
if (!res.ok) return
const rows: CollectionItem[] = await res.json()
if (seq !== itemsSeq.current) return
setItems((current) => (append ? [...current, ...rows] : rows))
setCursor(res.headers.get('X-Next-Cursor'))
} catch { toast.error('Could not load items') }
} catch { if (seq === itemsSeq.current) toast.error('Could not load items') }
}, [])
useEffect(() => { const t = setTimeout(loadCollections, 0); return () => clearTimeout(t) }, [loadCollections])
@@ -286,14 +298,14 @@ export function CollectionsClient() {
}, [collections, activeId])
useEffect(() => {
const t = setTimeout(() => {
if (!activeId) { setDetail(null); return }
if (!activeId) { detailSeq.current++; setDetail(null); return }
loadDetail(activeId)
}, 0)
return () => clearTimeout(t)
}, [activeId, loadDetail])
useEffect(() => {
const t = setTimeout(() => {
if (!activeId) { setItems([]); return }
if (!activeId) { itemsSeq.current++; setItems([]); return }
loadItems(activeId, viewId)
}, 0)
return () => clearTimeout(t)
@@ -529,7 +541,12 @@ export function CollectionsClient() {
if (!['empty', 'notEmpty'].includes(fOp)) {
if (prop.type === 'NUMBER') value = Number(fValue)
else if (prop.type === 'CHECKBOX') value = fValue === 'true'
else value = fValue
else if (prop.type === 'DATE' && prop.config?.mode === 'datetime') {
// Review M-14: datetime-local emits a local string; the server stores
// fixed-width UTC.
const parsed = new Date(fValue)
value = Number.isNaN(parsed.getTime()) ? '' : parsed.toISOString()
} else value = fValue
if (fValue === '' || (prop.type === 'NUMBER' && Number.isNaN(value))) { toast.error('Filter value required'); return }
}
setClause([...filterClauses, value === undefined ? { propertyId: fProp, op: fOp } : { propertyId: fProp, op: fOp, value }])
@@ -539,7 +556,9 @@ export function CollectionsClient() {
const prop = detail?.properties.find((p) => p.id === c.propertyId)
if (!prop) return 'unknown'
let shown = c.value
if (prop.type === 'SELECT' && typeof c.value === 'string') shown = prop.config.options?.find((o) => o.id === c.value)?.label ?? c.value
// Review M-14: MULTI_SELECT filters also carry an option id; show its
// label like SELECT instead of the raw id.
if ((prop.type === 'SELECT' || prop.type === 'MULTI_SELECT') && typeof c.value === 'string') shown = prop.config.options?.find((o) => o.id === c.value)?.label ?? c.value
if (prop.type === 'CHECKBOX') shown = c.value === true ? 'checked' : 'unchecked'
return `${prop.name} ${OP_LABELS[c.op] ?? c.op}${shown !== undefined ? ` ${shown}` : ''}`
}
@@ -637,6 +656,9 @@ export function CollectionsClient() {
onDrop={(e) => {
e.preventDefault()
const item = items.find((i) => i.id === draggedItemId)
// This is a drop handler, not render; the compiler rule cannot
// see through the async helper's captured state.
// eslint-disable-next-line react-hooks/refs
if (item) void moveItemToGroup(item, col.key === NONE ? null : col.key)
setDraggedItemId(null)
setBoardDropKey(null)
@@ -824,12 +846,18 @@ export function CollectionsClient() {
</Select>
{!['empty', 'notEmpty'].includes(fOp) && (() => {
const p = detail.properties.find((x) => x.id === fProp)
if (p?.type === 'SELECT') return (
if (p?.type === 'SELECT' || p?.type === 'MULTI_SELECT') return (
<Select value={fValue || undefined} onValueChange={setFValue}>
<SelectTrigger aria-label="Filter value"><SelectValue placeholder="Value" /></SelectTrigger>
<SelectContent>{(p.config.options ?? []).map((o) => <SelectItem key={o.id} value={o.id}>{o.label}</SelectItem>)}</SelectContent>
</Select>
)
// Review M-14: DATE filters used to render a plain
// text box, so a normal date always failed server
// validation. Match the property editor's inputs.
if (p?.type === 'DATE') return p.config?.mode === 'datetime'
? <Input type="datetime-local" value={fValue} onChange={(e) => setFValue(e.target.value)} aria-label="Filter value" className="h-8 text-sm" />
: <Input type="date" value={fValue} onChange={(e) => setFValue(e.target.value)} aria-label="Filter value" className="h-8 text-sm" />
if (p?.type === 'CHECKBOX') return (
<Select value={fValue || undefined} onValueChange={setFValue}>
<SelectTrigger aria-label="Filter value"><SelectValue placeholder="Value" /></SelectTrigger>