feat(ui): add maintenance page for task management
Some checks failed
CI / build-and-test (push) Failing after -31m53s
Some checks failed
CI / build-and-test (push) Failing after -31m53s
* Implement maintenance page with task and log display * Add backfill and metadata retry functionality * Integrate grid component for project display in thoughts page * Update types for maintenance tasks and logs * Enhance sidebar and shell for new maintenance navigation
This commit is contained in:
124
ui/src/api.ts
124
ui/src/api.ts
@@ -94,20 +94,21 @@ async function rsCall<T>(
|
||||
|
||||
function rsReadMany<T>(
|
||||
entity: string,
|
||||
options?: { filters?: ResolveSpecFilter[]; limit?: number; sort?: { column: string; direction: 'asc' | 'desc' }[] }
|
||||
options?: { filters?: ResolveSpecFilter[]; limit?: number; offset?: number; sort?: { column: string; direction: 'asc' | 'desc' }[] }
|
||||
): Promise<T[]> {
|
||||
return rsCall<T[]>(`/api/rs/public/${entity}`, 'read', {
|
||||
return rsCall<T[] | null>(`/api/rs/public/${entity}`, 'read', {
|
||||
options: {
|
||||
...(options?.filters?.length ? { filters: options.filters } : {}),
|
||||
...(options?.sort?.length ? { sort: options.sort } : {}),
|
||||
...(options?.limit ? { limit: options.limit } : {})
|
||||
...(options?.limit ? { limit: options.limit } : {}),
|
||||
...(options?.offset ? { offset: options.offset } : {})
|
||||
}
|
||||
});
|
||||
}).then((rows) => (Array.isArray(rows) ? rows : []));
|
||||
}
|
||||
|
||||
export const api = {
|
||||
projects: {
|
||||
list: async () => {
|
||||
list: async (params?: { limit?: number; offset?: number; q?: string }) => {
|
||||
type ProjectRow = {
|
||||
guid: string;
|
||||
name: string;
|
||||
@@ -117,7 +118,12 @@ export const api = {
|
||||
thought_count?: number;
|
||||
};
|
||||
|
||||
const projects = await rsCall<ProjectRow[]>('/api/rs/public/projects', 'read', {
|
||||
const filters: ResolveSpecFilter[] = [];
|
||||
if (params?.q) {
|
||||
filters.push({ column: 'name', operator: 'ilike', value: `%${params.q}%` });
|
||||
}
|
||||
|
||||
const projectRows = await rsCall<ProjectRow[] | null>('/api/rs/public/projects', 'read', {
|
||||
options: {
|
||||
columns: ['guid', 'name', 'description', 'created_at', 'last_active_at'],
|
||||
computedColumns: [
|
||||
@@ -126,11 +132,14 @@ export const api = {
|
||||
expression: 'COALESCE((SELECT COUNT(*) FROM public.thoughts t WHERE t.project_id = projects.guid), 0)'
|
||||
}
|
||||
],
|
||||
...(filters.length ? { filters } : {}),
|
||||
sort: [{ column: 'created_at', direction: 'desc' }],
|
||||
limit: 500
|
||||
limit: params?.limit ?? 100,
|
||||
offset: params?.offset ?? 0
|
||||
}
|
||||
});
|
||||
|
||||
const projects = Array.isArray(projectRows) ? projectRows : [];
|
||||
return projects.map((project) => ({
|
||||
id: project.guid,
|
||||
name: project.name,
|
||||
@@ -146,7 +155,7 @@ export const api = {
|
||||
})
|
||||
},
|
||||
thoughts: {
|
||||
list: (params: { q?: string; project_id?: string; limit?: number; include_archived?: boolean }) => {
|
||||
list: (params: { q?: string; project_id?: string; limit?: number; offset?: number; include_archived?: boolean }) => {
|
||||
const filters: ResolveSpecFilter[] = [];
|
||||
if (params.q) {
|
||||
filters.push({ column: 'content', operator: 'ilike', value: `%${params.q}%` });
|
||||
@@ -160,10 +169,65 @@ export const api = {
|
||||
return rsReadMany<import('./types').Thought>('thoughts', {
|
||||
filters,
|
||||
limit: params.limit ?? 100,
|
||||
...(params.offset !== undefined ? { offset: params.offset } : {}),
|
||||
sort: [{ column: 'created_at', direction: 'desc' }]
|
||||
});
|
||||
}).then((rows) =>
|
||||
rows.map((row) => ({
|
||||
...row,
|
||||
content: typeof row.content === 'string' ? row.content : '',
|
||||
metadata: {
|
||||
...(row.metadata ?? {}),
|
||||
people: row.metadata?.people ?? [],
|
||||
action_items: row.metadata?.action_items ?? [],
|
||||
dates_mentioned: row.metadata?.dates_mentioned ?? [],
|
||||
topics: row.metadata?.topics ?? [],
|
||||
type: row.metadata?.type ?? '',
|
||||
source: row.metadata?.source ?? '',
|
||||
metadata_status: row.metadata?.metadata_status ?? ''
|
||||
}
|
||||
}))
|
||||
);
|
||||
},
|
||||
get: (id: string) => rsCall<import('./types').Thought>(`/api/rs/public/thoughts/${id}`, 'read'),
|
||||
links: async (thoughtID: string) => {
|
||||
const numericID = Number.parseInt(thoughtID, 10);
|
||||
if (Number.isNaN(numericID)) return [];
|
||||
|
||||
const [outbound, inbound] = await Promise.all([
|
||||
rsReadMany<import('./types').ThoughtLink>('thought_links', {
|
||||
filters: [{ column: 'from_id', operator: 'eq', value: numericID }],
|
||||
limit: 200,
|
||||
sort: [{ column: 'created_at', direction: 'desc' }]
|
||||
}),
|
||||
rsReadMany<import('./types').ThoughtLink>('thought_links', {
|
||||
filters: [{ column: 'to_id', operator: 'eq', value: numericID }],
|
||||
limit: 200,
|
||||
sort: [{ column: 'created_at', direction: 'desc' }]
|
||||
})
|
||||
]);
|
||||
|
||||
const byID = new Map<number, import('./types').ThoughtLink>();
|
||||
for (const link of outbound) byID.set(link.id, link);
|
||||
for (const link of inbound) byID.set(link.id, link);
|
||||
|
||||
return Array.from(byID.values()).sort(
|
||||
(a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime()
|
||||
);
|
||||
},
|
||||
get: (id: string) =>
|
||||
rsCall<import('./types').Thought>(`/api/rs/public/thoughts/${id}`, 'read').then((row) => ({
|
||||
...row,
|
||||
content: typeof row.content === 'string' ? row.content : '',
|
||||
metadata: {
|
||||
...(row.metadata ?? {}),
|
||||
people: row.metadata?.people ?? [],
|
||||
action_items: row.metadata?.action_items ?? [],
|
||||
dates_mentioned: row.metadata?.dates_mentioned ?? [],
|
||||
topics: row.metadata?.topics ?? [],
|
||||
type: row.metadata?.type ?? '',
|
||||
source: row.metadata?.source ?? '',
|
||||
metadata_status: row.metadata?.metadata_status ?? ''
|
||||
}
|
||||
})),
|
||||
delete: (id: string) => rsCall<void>(`/api/rs/public/thoughts/${id}`, 'delete'),
|
||||
archive: (id: string) =>
|
||||
rsCall<void>(`/api/rs/public/thoughts/${id}`, 'update', {
|
||||
@@ -209,6 +273,46 @@ export const api = {
|
||||
});
|
||||
}
|
||||
},
|
||||
maintenance: {
|
||||
tasks: () =>
|
||||
rsReadMany<import('./types').MaintenanceTask>('maintenance_tasks', {
|
||||
limit: 200,
|
||||
sort: [{ column: 'next_due', direction: 'asc' }]
|
||||
}),
|
||||
logs: () =>
|
||||
rsReadMany<import('./types').MaintenanceLog>('maintenance_logs', {
|
||||
limit: 200,
|
||||
sort: [{ column: 'completed_at', direction: 'desc' }]
|
||||
}),
|
||||
runBackfill: (input?: {
|
||||
project?: string;
|
||||
limit?: number;
|
||||
include_archived?: boolean;
|
||||
older_than_days?: number;
|
||||
dry_run?: boolean;
|
||||
}) =>
|
||||
post<import('./types').BackfillResult>('/api/admin/actions/backfill', {
|
||||
limit: input?.limit ?? 100,
|
||||
project: input?.project,
|
||||
include_archived: input?.include_archived ?? false,
|
||||
older_than_days: input?.older_than_days ?? 0,
|
||||
dry_run: input?.dry_run ?? false
|
||||
}),
|
||||
runRetryMetadata: (input?: {
|
||||
project?: string;
|
||||
limit?: number;
|
||||
include_archived?: boolean;
|
||||
older_than_days?: number;
|
||||
dry_run?: boolean;
|
||||
}) =>
|
||||
post<import('./types').MetadataRetryResult>('/api/admin/actions/retry-metadata', {
|
||||
limit: input?.limit ?? 100,
|
||||
project: input?.project,
|
||||
include_archived: input?.include_archived ?? false,
|
||||
older_than_days: input?.older_than_days ?? 1,
|
||||
dry_run: input?.dry_run ?? false
|
||||
})
|
||||
},
|
||||
stats: async () => {
|
||||
type StatsThoughtRow = {
|
||||
metadata?: {
|
||||
|
||||
Reference in New Issue
Block a user