mobile: fix flex-row text overflow; parallelize door-ingest poll; docs refresh
Mobile responsiveness: several flex rows had a `truncate` child without `min-w-0`, so a long value (member/door name, channel name, published form URL) would not shrink and pushed the row past the viewport on a phone. Added `min-w-0` (with `flex-1`/`shrink-0` where needed) on the dashboard presence + door-self-entry rows, the comms channel list, and the form-builder URL. Door inbound poll: `/api/cron/door-ingest` now polls connections with Promise.allSettled instead of a sequential loop, so one tick is bounded to the slowest single poll rather than the sum of all per-call timeouts; each connection stays isolated and the run is idempotent. Docs: refreshed ARCHITECTURE (door epic marked complete P1-P5), the README access-control feature bullet + project-status pointer, a historical banner on the older AUDIT.md, a door-epic CHANGELOG entry, and the rolling HANDOFF audit log (passes 65-68). No migration; no behavior change for existing users.
This commit is contained in:
@@ -215,7 +215,7 @@ export default function CommsClient({ member, space, channels }: Props) {
|
||||
}`}
|
||||
>
|
||||
<Hash className="w-3.5 h-3.5 flex-shrink-0" />
|
||||
<span className="truncate">{channel.name}</span>
|
||||
<span className="truncate min-w-0">{channel.name}</span>
|
||||
{channel.unread_count > 0 && (
|
||||
<span className="ml-auto w-4 h-4 bg-primary text-white text-[9px] font-bold rounded-full flex items-center justify-center">
|
||||
{channel.unread_count}
|
||||
@@ -240,7 +240,7 @@ export default function CommsClient({ member, space, channels }: Props) {
|
||||
}`}
|
||||
>
|
||||
<Hash className="w-3.5 h-3.5 flex-shrink-0" />
|
||||
<span className="truncate">{channel.name}</span>
|
||||
<span className="truncate min-w-0">{channel.name}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
@@ -262,7 +262,7 @@ export default function CommsClient({ member, space, channels }: Props) {
|
||||
<svg className="w-3.5 h-3.5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z" />
|
||||
</svg>
|
||||
<span className="truncate">{channel.name}</span>
|
||||
<span className="truncate min-w-0">{channel.name}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -31,8 +31,8 @@ export function DoorSelfEntry({ doors }: { doors: Door[] }) {
|
||||
<div className="bg-card rounded border border-border divide-y divide-border">
|
||||
{doors.map(d => (
|
||||
<div key={d.id} className="flex items-center justify-between gap-3 px-4 py-3">
|
||||
<span className="font-sans text-sm text-foreground truncate">{d.name}</span>
|
||||
<Button size="sm" disabled={busy === d.id} onClick={() => open(d)}>
|
||||
<span className="flex-1 min-w-0 font-sans text-sm text-foreground truncate">{d.name}</span>
|
||||
<Button size="sm" className="shrink-0" disabled={busy === d.id} onClick={() => open(d)}>
|
||||
{busy === d.id ? 'Opening…' : 'Open'}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -65,7 +65,7 @@ export function PresencePanel({ present }: { present: Present[] }) {
|
||||
<ul className="divide-y divide-border">
|
||||
{present.map(p => (
|
||||
<li key={p.id} className="flex items-center justify-between gap-3 px-4 py-2.5">
|
||||
<span className="font-sans text-sm text-foreground truncate">
|
||||
<span className="flex-1 min-w-0 font-sans text-sm text-foreground truncate">
|
||||
{p.name}{p.isMe ? ' (you)' : ''}
|
||||
{p.note && <span className="font-mono text-[10px] text-muted-foreground ml-2">{p.note}</span>}
|
||||
</span>
|
||||
|
||||
@@ -52,29 +52,39 @@ export async function POST(req: NextRequest) {
|
||||
.limit(MAX_CONNECTIONS)
|
||||
if (error) return NextResponse.json({ error: error.message }, { status: 500 })
|
||||
|
||||
let polled = 0
|
||||
// Poll connections concurrently: each connection is independent, and the
|
||||
// executor's per-call timeout means a sequential loop would take the SUM of
|
||||
// all timeouts (a slow/unreachable fleet could run minutes and overlap the
|
||||
// next tick). allSettled bounds a tick to roughly the slowest single poll;
|
||||
// each connection is isolated so one misbehaving controller (untrusted,
|
||||
// plaintext HTTP) cannot 500 the run or starve the rest. MAX_CONNECTIONS
|
||||
// caps the fan-out.
|
||||
const list = (conns ?? []) as unknown as IngestConn[]
|
||||
const settled = await Promise.allSettled(
|
||||
list.map(async c => {
|
||||
const poll = await pollConnectionLog(admin, c)
|
||||
if (!poll.ok) {
|
||||
console.error(`[door-ingest] connection ${c.id}: ${poll.detail}`)
|
||||
return { failed: true, inserted: 0, resolved: 0 }
|
||||
}
|
||||
const res = await ingestEvents(admin, c.space_id, c.id, poll.events)
|
||||
return { failed: false, inserted: res.inserted, resolved: res.resolved }
|
||||
}),
|
||||
)
|
||||
|
||||
let inserted = 0
|
||||
let resolved = 0
|
||||
let failed = 0
|
||||
for (const c of (conns ?? []) as unknown as IngestConn[]) {
|
||||
polled++
|
||||
// Isolate each connection: a single misbehaving controller (the body is
|
||||
// untrusted, plaintext HTTP) must not 500 the whole run and starve the rest.
|
||||
try {
|
||||
const poll = await pollConnectionLog(admin, c)
|
||||
if (!poll.ok) {
|
||||
failed++
|
||||
console.error(`[door-ingest] connection ${c.id}: ${poll.detail}`)
|
||||
continue
|
||||
}
|
||||
const res = await ingestEvents(admin, c.space_id, c.id, poll.events)
|
||||
inserted += res.inserted
|
||||
resolved += res.resolved
|
||||
} catch (e) {
|
||||
for (const r of settled) {
|
||||
if (r.status === 'rejected') {
|
||||
failed++
|
||||
console.error(`[door-ingest] connection ${c.id} threw:`, e instanceof Error ? e.message : e)
|
||||
console.error('[door-ingest] connection threw:', r.reason instanceof Error ? r.reason.message : r.reason)
|
||||
continue
|
||||
}
|
||||
if (r.value.failed) failed++
|
||||
inserted += r.value.inserted
|
||||
resolved += r.value.resolved
|
||||
}
|
||||
|
||||
return NextResponse.json({ ok: true, polled, inserted, resolved, failed })
|
||||
return NextResponse.json({ ok: true, polled: list.length, inserted, resolved, failed })
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user