fix: remove redundant code in processing logic
Some checks failed
CI / build-and-test (push) Failing after -31m35s
Some checks failed
CI / build-and-test (push) Failing after -31m35s
This commit is contained in:
@@ -313,6 +313,21 @@ export const api = {
|
||||
dry_run: input?.dry_run ?? false
|
||||
})
|
||||
},
|
||||
plans: {
|
||||
list: async (params?: { status?: string; priority?: string; project_id?: string; limit?: number }) => {
|
||||
const filters: ResolveSpecFilter[] = [];
|
||||
if (params?.status) filters.push({ column: 'status', operator: 'eq', value: params.status });
|
||||
if (params?.priority) filters.push({ column: 'priority', operator: 'eq', value: params.priority });
|
||||
if (params?.project_id) filters.push({ column: 'project_id', operator: 'eq', value: params.project_id });
|
||||
|
||||
const rows = await rsReadMany<Omit<import('./types').Plan, 'tags'> & { tags?: unknown }>('plans', {
|
||||
filters,
|
||||
limit: params?.limit ?? 500,
|
||||
sort: [{ column: 'updated_at', direction: 'desc' }]
|
||||
});
|
||||
return rows.map((row) => ({ ...row, tags: normalizeTags(row.tags) }));
|
||||
}
|
||||
},
|
||||
stats: async () => {
|
||||
type StatsThoughtRow = {
|
||||
metadata?: {
|
||||
|
||||
197
ui/src/components/plans/PlansPage.svelte
Normal file
197
ui/src/components/plans/PlansPage.svelte
Normal file
@@ -0,0 +1,197 @@
|
||||
<script lang="ts">
|
||||
import { GridlerFull, type GridlerColumn } from "@warkypublic/svelix";
|
||||
import { GlobalStateStore } from "../../shellState";
|
||||
import { adminGridTheme } from "../../gridTheme";
|
||||
import type { Plan } from "../../types";
|
||||
|
||||
let selectedPlan = $state<Plan | null>(null);
|
||||
let gridTotal = $state<number | null>(null);
|
||||
|
||||
const plansDataSourceOptions = {
|
||||
url: "/api/rs",
|
||||
authToken: GlobalStateStore.getState().session.authToken,
|
||||
schema: "public",
|
||||
entity: "plans",
|
||||
uniqueID: "id",
|
||||
sort: [{ column: "updated_at", direction: "desc" }],
|
||||
} as unknown as {
|
||||
url: string;
|
||||
authToken?: string;
|
||||
schema: string;
|
||||
entity: string;
|
||||
uniqueID: string;
|
||||
};
|
||||
|
||||
const columns: GridlerColumn[] = [
|
||||
{ id: "title", title: "Title", dataKey: "title", width: 340 },
|
||||
{ id: "status", title: "Status", dataKey: "status", width: 120 },
|
||||
{ id: "priority", title: "Priority", dataKey: "priority", width: 110 },
|
||||
{ id: "owner", title: "Owner", dataKey: "owner", width: 160 },
|
||||
{ id: "due_date", title: "Due", dataKey: "due_date", width: 180, format: "datetime" },
|
||||
{ id: "last_reviewed_at", title: "Reviewed", dataKey: "last_reviewed_at", width: 180, format: "datetime" },
|
||||
{ id: "updated_at", title: "Updated", dataKey: "updated_at", width: 180, format: "datetime" },
|
||||
];
|
||||
|
||||
function normalizeTags(value: unknown): string[] {
|
||||
if (Array.isArray(value)) return value.map((t) => String(t).trim()).filter(Boolean);
|
||||
if (typeof value !== "string" || !value.trim()) return [];
|
||||
const trimmed = value.trim();
|
||||
if (trimmed.startsWith("{") && trimmed.endsWith("}")) {
|
||||
return trimmed.slice(1, -1).split(",").map((t) => t.trim().replace(/^"(.*)"$/, "$1")).filter(Boolean);
|
||||
}
|
||||
return trimmed.split(",").map((t) => t.trim()).filter(Boolean);
|
||||
}
|
||||
|
||||
function normalizePlan(rowData: Record<string, unknown>): Plan {
|
||||
return {
|
||||
id: String(rowData.id ?? ""),
|
||||
title: typeof rowData.title === "string" ? rowData.title : "",
|
||||
description: typeof rowData.description === "string" ? rowData.description : "",
|
||||
status: (typeof rowData.status === "string" ? rowData.status : "draft") as Plan["status"],
|
||||
priority: (typeof rowData.priority === "string" ? rowData.priority : "medium") as Plan["priority"],
|
||||
project_id: typeof rowData.project_id === "string" ? rowData.project_id : undefined,
|
||||
owner: typeof rowData.owner === "string" && rowData.owner ? rowData.owner : undefined,
|
||||
due_date: typeof rowData.due_date === "string" ? rowData.due_date : undefined,
|
||||
completed_at: typeof rowData.completed_at === "string" ? rowData.completed_at : undefined,
|
||||
reviewed_by: typeof rowData.reviewed_by === "string" ? rowData.reviewed_by : undefined,
|
||||
last_reviewed_at: typeof rowData.last_reviewed_at === "string" ? rowData.last_reviewed_at : undefined,
|
||||
supersedes_plan_id: typeof rowData.supersedes_plan_id === "string" ? rowData.supersedes_plan_id : undefined,
|
||||
tags: normalizeTags(rowData.tags),
|
||||
created_at: String(rowData.created_at ?? ""),
|
||||
updated_at: String(rowData.updated_at ?? ""),
|
||||
};
|
||||
}
|
||||
|
||||
function onRowClick(_row: number, rowData: Record<string, unknown> | undefined) {
|
||||
selectedPlan = rowData ? normalizePlan(rowData) : null;
|
||||
}
|
||||
|
||||
function onGridEvent(
|
||||
type: string,
|
||||
_item?: unknown,
|
||||
_column?: unknown,
|
||||
_coords?: unknown,
|
||||
detail?: Record<string, unknown>,
|
||||
) {
|
||||
if (type !== "page_loaded" && type !== "load") return;
|
||||
const total = detail?.total;
|
||||
if (typeof total === "number") gridTotal = total;
|
||||
}
|
||||
|
||||
function formatDate(value?: string): string {
|
||||
if (!value) return "—";
|
||||
return new Date(value).toLocaleString();
|
||||
}
|
||||
|
||||
const statusClasses: Record<string, string> = {
|
||||
draft: "bg-slate-700/60 text-slate-300",
|
||||
active: "bg-cyan-900/60 text-cyan-200",
|
||||
blocked: "bg-amber-900/60 text-amber-200",
|
||||
completed: "bg-emerald-900/60 text-emerald-200",
|
||||
cancelled: "bg-slate-800/60 text-slate-500",
|
||||
superseded: "bg-purple-900/60 text-purple-300",
|
||||
};
|
||||
|
||||
const priorityClasses: Record<string, string> = {
|
||||
low: "bg-slate-700/60 text-slate-400",
|
||||
medium: "bg-cyan-900/60 text-cyan-300",
|
||||
high: "bg-amber-900/60 text-amber-300",
|
||||
critical: "bg-rose-900/60 text-rose-300",
|
||||
};
|
||||
</script>
|
||||
|
||||
<div class="space-y-4 w-full">
|
||||
<div class="flex flex-col gap-3 sm:flex-row sm:items-end sm:justify-between">
|
||||
<div>
|
||||
<h2 class="text-2xl font-semibold text-white">Plans</h2>
|
||||
<p class="mt-1 text-sm text-slate-400">
|
||||
{#if gridTotal === null}
|
||||
Server-backed grid
|
||||
{:else}
|
||||
{gridTotal} plan{gridTotal !== 1 ? "s" : ""}
|
||||
{/if}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-4 xl:grid-cols-[1.6fr_1fr]">
|
||||
<div class="rounded-2xl border border-white/10 bg-slate-950/30 p-3">
|
||||
<GridlerFull
|
||||
{columns}
|
||||
theme={adminGridTheme}
|
||||
rowMarkers="number"
|
||||
height={560}
|
||||
width="100%"
|
||||
pageSize={40}
|
||||
dataSource="resolvespec"
|
||||
dataSourceOptions={plansDataSourceOptions}
|
||||
serverSideSearch={true}
|
||||
searchColumns={["title", "description", "status", "priority", "owner"]}
|
||||
{onGridEvent}
|
||||
{onRowClick}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<aside class="rounded-2xl border border-white/10 bg-slate-900/70 p-4">
|
||||
<h3 class="text-sm font-semibold text-white">Plan Inspector</h3>
|
||||
|
||||
{#if !selectedPlan}
|
||||
<p class="mt-3 text-sm text-slate-500">
|
||||
Select a plan row to inspect details and relationships.
|
||||
</p>
|
||||
{:else}
|
||||
<div class="mt-3 space-y-3 text-sm text-slate-300">
|
||||
<p class="text-base font-semibold text-slate-100">{selectedPlan.title}</p>
|
||||
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<span class={`inline-flex items-center rounded-lg px-2.5 py-0.5 text-xs font-medium ${statusClasses[selectedPlan.status] ?? "bg-slate-700/60 text-slate-300"}`}>
|
||||
{selectedPlan.status}
|
||||
</span>
|
||||
<span class={`inline-flex items-center rounded-lg px-2.5 py-0.5 text-xs font-medium ${priorityClasses[selectedPlan.priority] ?? "bg-slate-700/60 text-slate-300"}`}>
|
||||
{selectedPlan.priority}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="rounded-xl border border-white/10 bg-white/5 p-3 space-y-1">
|
||||
<p><strong class="text-slate-100">Owner:</strong> {selectedPlan.owner || "—"}</p>
|
||||
<p><strong class="text-slate-100">Due:</strong> {formatDate(selectedPlan.due_date)}</p>
|
||||
<p><strong class="text-slate-100">Completed:</strong> {formatDate(selectedPlan.completed_at)}</p>
|
||||
<p><strong class="text-slate-100">Last reviewed:</strong> {formatDate(selectedPlan.last_reviewed_at)}</p>
|
||||
<p><strong class="text-slate-100">Reviewed by:</strong> {selectedPlan.reviewed_by || "—"}</p>
|
||||
<p><strong class="text-slate-100">Created:</strong> {formatDate(selectedPlan.created_at)}</p>
|
||||
<p><strong class="text-slate-100">Updated:</strong> {formatDate(selectedPlan.updated_at)}</p>
|
||||
</div>
|
||||
|
||||
{#if selectedPlan.description}
|
||||
<div class="rounded-xl border border-white/10 bg-white/5 p-3">
|
||||
<p class="text-xs uppercase tracking-[0.18em] text-slate-500">Description</p>
|
||||
<p class="mt-2 whitespace-pre-wrap text-slate-300">{selectedPlan.description}</p>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if selectedPlan.tags.length > 0}
|
||||
<div class="rounded-xl border border-white/10 bg-white/5 p-3">
|
||||
<p class="text-xs uppercase tracking-[0.18em] text-slate-500">Tags</p>
|
||||
<div class="mt-2 flex flex-wrap gap-1.5">
|
||||
{#each selectedPlan.tags as tag}
|
||||
<span class="rounded-md bg-white/10 px-2 py-0.5 text-xs text-slate-300">{tag}</span>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if selectedPlan.project_id || selectedPlan.supersedes_plan_id}
|
||||
<div class="rounded-xl border border-white/10 bg-white/5 p-3 space-y-1">
|
||||
{#if selectedPlan.project_id}
|
||||
<p><strong class="text-slate-100">Project:</strong> <span class="font-mono text-xs text-slate-400">{selectedPlan.project_id}</span></p>
|
||||
{/if}
|
||||
{#if selectedPlan.supersedes_plan_id}
|
||||
<p><strong class="text-slate-100">Supersedes:</strong> <span class="font-mono text-xs text-slate-400">{selectedPlan.supersedes_plan_id}</span></p>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</aside>
|
||||
</div>
|
||||
</div>
|
||||
@@ -3,6 +3,7 @@
|
||||
import FilesPage from '../files/FilesPage.svelte';
|
||||
import GuardrailsPage from '../guardrails/GuardrailsPage.svelte';
|
||||
import LearningsPage from '../learnings/LearningsPage.svelte';
|
||||
import PlansPage from '../plans/PlansPage.svelte';
|
||||
import MaintenancePage from '../maintenance/MaintenancePage.svelte';
|
||||
import DashboardPage from '../dashboard/DashboardPage.svelte';
|
||||
import ProjectsPage from '../projects/ProjectsPage.svelte';
|
||||
@@ -41,6 +42,8 @@
|
||||
<ThoughtsPage />
|
||||
{:else if currentPage === 'learnings'}
|
||||
<LearningsPage />
|
||||
{:else if currentPage === 'plans'}
|
||||
<PlansPage />
|
||||
{:else if currentPage === 'skills'}
|
||||
<SkillsPage />
|
||||
{:else if currentPage === 'guardrails'}
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
{ id: 'projects', label: 'Projects', description: 'Browse and manage projects.' },
|
||||
{ id: 'thoughts', label: 'Thoughts', description: 'Search and inspect thoughts.' },
|
||||
{ id: 'learnings', label: 'Learnings', description: 'Curated insights and outcomes.' },
|
||||
{ id: 'plans', label: 'Plans', description: 'Structured plans and workstreams.' },
|
||||
{ id: 'skills', label: 'Skills', description: 'Agent skill registry.' },
|
||||
{ id: 'guardrails', label: 'Guardrails', description: 'Agent guardrail registry.' },
|
||||
{ id: 'files', label: 'Files', description: 'Stored file inventory.' },
|
||||
|
||||
@@ -56,7 +56,7 @@ export type NavItem = {
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
export type ShellPage = 'dashboard' | 'projects' | 'thoughts' | 'learnings' | 'skills' | 'guardrails' | 'files' | 'maintenance';
|
||||
export type ShellPage = 'dashboard' | 'projects' | 'thoughts' | 'learnings' | 'plans' | 'skills' | 'guardrails' | 'files' | 'maintenance';
|
||||
|
||||
export type Project = {
|
||||
id: string;
|
||||
@@ -181,6 +181,24 @@ export type Learning = {
|
||||
updated_at: string;
|
||||
};
|
||||
|
||||
export type Plan = {
|
||||
id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
status: 'draft' | 'active' | 'blocked' | 'completed' | 'cancelled' | 'superseded';
|
||||
priority: 'low' | 'medium' | 'high' | 'critical';
|
||||
project_id?: string;
|
||||
owner?: string;
|
||||
due_date?: string;
|
||||
completed_at?: string;
|
||||
reviewed_by?: string;
|
||||
last_reviewed_at?: string;
|
||||
supersedes_plan_id?: string;
|
||||
tags: string[];
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
};
|
||||
|
||||
export type MaintenanceTask = {
|
||||
id: string;
|
||||
name: string;
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user