feat(ri-050): RI-5-001 typed freshness states and stale-safe Mission Control surfaces (#1275) (#1300)
ci/woodpecker/push/publish Pipeline was canceled

This commit was merged in pull request #1300.
This commit is contained in:
2026-08-18 05:56:58 +00:00
parent d92de53399
commit 7c7dab3898
15 changed files with 2525 additions and 188 deletions
+26 -33
View File
@@ -1,45 +1,32 @@
import { useEffect, useState, type ReactElement } from 'react';
import { useState, type ReactElement } from 'react';
import { KanbanBoard } from '@/components/tasks/kanban-board';
import { TaskDetailModal } from '@/components/tasks/task-detail-modal';
import { TaskListView } from '@/components/tasks/task-list-view';
import { StaleDataNotice, UnavailableDataNotice } from '@/components/freshness/freshness-notices';
import { api } from '@/lib/api';
import { cn } from '@/lib/cn';
import type { Task } from '@/lib/types';
import { getErrorMessage } from './page-errors';
import { useFreshCollection, describeFailure } from '@/lib/freshness/use-fresh-collection';
import { validateTaskCollection } from '@/lib/freshness/validators';
type ViewMode = 'list' | 'kanban';
export function TasksPage(): ReactElement {
const [tasks, setTasks] = useState<Task[]>([]);
const tasks = useFreshCollection<Task[]>({
source: 'gateway:/api/tasks',
fetcher: (signal) => api<unknown>('/api/tasks', { signal }),
validate: validateTaskCollection,
cacheKey: 'tasks',
});
const [view, setView] = useState<ViewMode>('kanban');
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [selectedTask, setSelectedTask] = useState<Task | null>(null);
useEffect(() => {
let cancelled = false;
void api<Task[]>('/api/tasks')
.then((response) => {
if (cancelled) return;
setTasks(response);
})
.catch((caught: unknown) => {
if (cancelled) return;
setError(getErrorMessage(caught, 'Failed to load tasks.'));
})
.finally(() => {
if (cancelled) return;
setLoading(false);
});
return () => {
cancelled = true;
};
}, []);
const retry = (): void => {
void tasks.revalidate();
};
return (
<div className="flex min-h-screen flex-col px-4 py-6 sm:px-6">
<div data-freshness={tasks.freshness} className="flex min-h-screen flex-col px-4 py-6 sm:px-6">
<header className="mb-6 flex items-center justify-between gap-4 border-b px-1 pb-3">
<h1 className="text-2xl font-semibold">Tasks</h1>
<div className="flex rounded-lg border border-surface-border">
@@ -70,18 +57,24 @@ export function TasksPage(): ReactElement {
</div>
</header>
{error ? (
<div role="alert" className="mb-6 rounded-lg border border-error/40 px-4 py-3 text-sm">
{error}
{tasks.freshness === 'stale' && tasks.snapshot ? (
<div className="mb-6">
<StaleDataNotice label={tasks.snapshot} onRetry={retry} />
</div>
) : null}
{loading ? (
{tasks.freshness === 'unknown' ? (
<p className="py-8 text-center text-sm text-text-muted">Loading tasks...</p>
) : tasks.freshness === 'unavailable' ? (
<UnavailableDataNotice
title="Tasks"
detail={describeFailure(tasks.failure)}
onRetry={retry}
/>
) : view === 'kanban' ? (
<KanbanBoard tasks={tasks} onTaskClick={setSelectedTask} />
<KanbanBoard tasks={tasks.data ?? []} onTaskClick={setSelectedTask} />
) : (
<TaskListView tasks={tasks} onTaskClick={setSelectedTask} />
<TaskListView tasks={tasks.data ?? []} onTaskClick={setSelectedTask} />
)}
{selectedTask ? (