stagent 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +191 -0
- package/README.md +399 -0
- package/components.json +21 -0
- package/dist/cli.js +171 -0
- package/drizzle.config.ts +12 -0
- package/next.config.mjs +15 -0
- package/package.json +114 -0
- package/postcss.config.mjs +8 -0
- package/public/icon-512.png +0 -0
- package/public/icon.svg +13 -0
- package/public/readme/home-workspace.png +0 -0
- package/public/readme/inbox-approvals.png +0 -0
- package/public/readme/workflow-blueprints.png +0 -0
- package/public/stagent-s-128.png +0 -0
- package/public/stagent-s-64.png +0 -0
- package/src/app/api/blueprints/[id]/instantiate/route.ts +27 -0
- package/src/app/api/blueprints/[id]/route.ts +39 -0
- package/src/app/api/blueprints/import/route.ts +68 -0
- package/src/app/api/blueprints/route.ts +29 -0
- package/src/app/api/command-palette/recent/route.ts +31 -0
- package/src/app/api/data/clear/route.ts +22 -0
- package/src/app/api/data/seed/route.ts +22 -0
- package/src/app/api/documents/[id]/file/route.ts +44 -0
- package/src/app/api/documents/[id]/route.ts +123 -0
- package/src/app/api/documents/route.ts +59 -0
- package/src/app/api/logs/stream/route.ts +101 -0
- package/src/app/api/notifications/[id]/route.ts +36 -0
- package/src/app/api/notifications/mark-all-read/route.ts +13 -0
- package/src/app/api/notifications/pending-approvals/route.ts +10 -0
- package/src/app/api/notifications/pending-approvals/stream/route.ts +101 -0
- package/src/app/api/notifications/route.ts +34 -0
- package/src/app/api/permissions/route.ts +46 -0
- package/src/app/api/profiles/[id]/route.ts +79 -0
- package/src/app/api/profiles/[id]/test/route.ts +42 -0
- package/src/app/api/profiles/import/route.ts +108 -0
- package/src/app/api/profiles/route.ts +50 -0
- package/src/app/api/projects/[id]/route.ts +72 -0
- package/src/app/api/projects/route.ts +53 -0
- package/src/app/api/schedules/[id]/route.ts +185 -0
- package/src/app/api/schedules/route.ts +117 -0
- package/src/app/api/settings/budgets/route.ts +24 -0
- package/src/app/api/settings/openai/route.ts +24 -0
- package/src/app/api/settings/route.ts +21 -0
- package/src/app/api/settings/test/route.ts +26 -0
- package/src/app/api/tasks/[id]/cancel/route.ts +21 -0
- package/src/app/api/tasks/[id]/execute/route.ts +90 -0
- package/src/app/api/tasks/[id]/logs/route.ts +95 -0
- package/src/app/api/tasks/[id]/output/route.ts +47 -0
- package/src/app/api/tasks/[id]/respond/route.ts +64 -0
- package/src/app/api/tasks/[id]/resume/route.ts +76 -0
- package/src/app/api/tasks/[id]/route.ts +77 -0
- package/src/app/api/tasks/assist/route.ts +35 -0
- package/src/app/api/tasks/route.ts +82 -0
- package/src/app/api/uploads/[id]/route.ts +81 -0
- package/src/app/api/uploads/cleanup/route.ts +7 -0
- package/src/app/api/uploads/route.ts +66 -0
- package/src/app/api/workflows/[id]/execute/route.ts +82 -0
- package/src/app/api/workflows/[id]/route.ts +133 -0
- package/src/app/api/workflows/[id]/status/route.ts +54 -0
- package/src/app/api/workflows/[id]/steps/[stepId]/retry/route.ts +22 -0
- package/src/app/api/workflows/route.ts +61 -0
- package/src/app/apple-icon.tsx +31 -0
- package/src/app/costs/page.tsx +256 -0
- package/src/app/dashboard/page.tsx +44 -0
- package/src/app/documents/[id]/page.tsx +46 -0
- package/src/app/documents/page.tsx +45 -0
- package/src/app/error.tsx +26 -0
- package/src/app/global-error.tsx +23 -0
- package/src/app/globals.css +733 -0
- package/src/app/icon.tsx +30 -0
- package/src/app/inbox/loading.tsx +15 -0
- package/src/app/inbox/page.tsx +35 -0
- package/src/app/layout.tsx +78 -0
- package/src/app/manifest.ts +32 -0
- package/src/app/monitor/page.tsx +37 -0
- package/src/app/page.tsx +162 -0
- package/src/app/profiles/[id]/edit/page.tsx +39 -0
- package/src/app/profiles/[id]/page.tsx +33 -0
- package/src/app/profiles/new/page.tsx +22 -0
- package/src/app/profiles/page.tsx +19 -0
- package/src/app/projects/[id]/page.tsx +134 -0
- package/src/app/projects/loading.tsx +17 -0
- package/src/app/projects/page.tsx +32 -0
- package/src/app/schedules/[id]/page.tsx +47 -0
- package/src/app/schedules/page.tsx +18 -0
- package/src/app/settings/loading.tsx +24 -0
- package/src/app/settings/page.tsx +27 -0
- package/src/app/tasks/[id]/page.tsx +45 -0
- package/src/app/tasks/new/page.tsx +27 -0
- package/src/app/workflows/[id]/edit/page.tsx +66 -0
- package/src/app/workflows/[id]/page.tsx +37 -0
- package/src/app/workflows/blueprints/[id]/page.tsx +40 -0
- package/src/app/workflows/blueprints/new/page.tsx +20 -0
- package/src/app/workflows/blueprints/page.tsx +11 -0
- package/src/app/workflows/new/page.tsx +36 -0
- package/src/app/workflows/page.tsx +18 -0
- package/src/components/charts/donut-ring.tsx +64 -0
- package/src/components/charts/mini-bar.tsx +75 -0
- package/src/components/charts/sparkline.tsx +107 -0
- package/src/components/costs/cost-dashboard.tsx +877 -0
- package/src/components/costs/cost-filters.tsx +179 -0
- package/src/components/dashboard/activity-feed.tsx +95 -0
- package/src/components/dashboard/greeting.tsx +30 -0
- package/src/components/dashboard/priority-queue.tsx +79 -0
- package/src/components/dashboard/quick-actions.tsx +62 -0
- package/src/components/dashboard/recent-projects.tsx +79 -0
- package/src/components/dashboard/stats-cards.tsx +114 -0
- package/src/components/documents/document-browser.tsx +235 -0
- package/src/components/documents/document-detail-view.tsx +367 -0
- package/src/components/documents/document-grid.tsx +78 -0
- package/src/components/documents/document-preview.tsx +68 -0
- package/src/components/documents/document-table.tsx +119 -0
- package/src/components/documents/document-upload-dialog.tsx +153 -0
- package/src/components/documents/types.ts +6 -0
- package/src/components/documents/utils.ts +57 -0
- package/src/components/monitoring/connection-indicator.tsx +14 -0
- package/src/components/monitoring/log-entry.tsx +79 -0
- package/src/components/monitoring/log-filters.tsx +57 -0
- package/src/components/monitoring/log-stream.tsx +144 -0
- package/src/components/monitoring/monitor-overview-wrapper.tsx +64 -0
- package/src/components/monitoring/monitor-overview.tsx +119 -0
- package/src/components/notifications/failure-action.tsx +38 -0
- package/src/components/notifications/inbox-list.tsx +165 -0
- package/src/components/notifications/message-response.tsx +196 -0
- package/src/components/notifications/notification-item.tsx +250 -0
- package/src/components/notifications/pending-approval-host.tsx +478 -0
- package/src/components/notifications/permission-action.tsx +37 -0
- package/src/components/notifications/permission-response-actions.tsx +126 -0
- package/src/components/notifications/unread-badge.tsx +35 -0
- package/src/components/profiles/profile-browser.tsx +117 -0
- package/src/components/profiles/profile-card.tsx +78 -0
- package/src/components/profiles/profile-detail-view.tsx +564 -0
- package/src/components/profiles/profile-form-view.tsx +480 -0
- package/src/components/profiles/profile-import-dialog.tsx +113 -0
- package/src/components/projects/project-card.tsx +58 -0
- package/src/components/projects/project-create-dialog.tsx +140 -0
- package/src/components/projects/project-detail.tsx +68 -0
- package/src/components/projects/project-edit-dialog.tsx +219 -0
- package/src/components/projects/project-list.tsx +108 -0
- package/src/components/schedules/schedule-create-dialog.tsx +403 -0
- package/src/components/schedules/schedule-detail-view.tsx +274 -0
- package/src/components/schedules/schedule-list.tsx +242 -0
- package/src/components/schedules/schedule-status-badge.tsx +16 -0
- package/src/components/settings/api-key-form.tsx +141 -0
- package/src/components/settings/auth-config-section.tsx +141 -0
- package/src/components/settings/auth-method-selector.tsx +67 -0
- package/src/components/settings/auth-status-badge.tsx +40 -0
- package/src/components/settings/auth-status-dot.tsx +59 -0
- package/src/components/settings/budget-guardrails-section.tsx +842 -0
- package/src/components/settings/data-management-section.tsx +141 -0
- package/src/components/settings/openai-runtime-section.tsx +104 -0
- package/src/components/settings/permissions-section.tsx +91 -0
- package/src/components/shared/app-sidebar.tsx +123 -0
- package/src/components/shared/card-skeleton.tsx +42 -0
- package/src/components/shared/command-palette.tsx +250 -0
- package/src/components/shared/confirm-dialog.tsx +52 -0
- package/src/components/shared/empty-state.tsx +24 -0
- package/src/components/shared/error-state.tsx +32 -0
- package/src/components/shared/form-section-card.tsx +33 -0
- package/src/components/shared/section-heading.tsx +14 -0
- package/src/components/shared/stagent-logo.tsx +21 -0
- package/src/components/shared/theme-toggle.tsx +46 -0
- package/src/components/tasks/ai-assist-panel.tsx +210 -0
- package/src/components/tasks/content-preview.tsx +89 -0
- package/src/components/tasks/empty-board.tsx +12 -0
- package/src/components/tasks/file-upload.tsx +120 -0
- package/src/components/tasks/kanban-board.tsx +275 -0
- package/src/components/tasks/kanban-column.tsx +75 -0
- package/src/components/tasks/skeleton-board.tsx +21 -0
- package/src/components/tasks/task-attachments.tsx +114 -0
- package/src/components/tasks/task-card.tsx +101 -0
- package/src/components/tasks/task-create-panel.tsx +360 -0
- package/src/components/tasks/task-detail-view.tsx +356 -0
- package/src/components/ui/alert-dialog.tsx +196 -0
- package/src/components/ui/badge.tsx +50 -0
- package/src/components/ui/button.tsx +71 -0
- package/src/components/ui/card.tsx +92 -0
- package/src/components/ui/checkbox.tsx +32 -0
- package/src/components/ui/command.tsx +184 -0
- package/src/components/ui/dialog.tsx +158 -0
- package/src/components/ui/dropdown-menu.tsx +257 -0
- package/src/components/ui/form.tsx +167 -0
- package/src/components/ui/input.tsx +21 -0
- package/src/components/ui/label.tsx +24 -0
- package/src/components/ui/popover.tsx +89 -0
- package/src/components/ui/progress.tsx +31 -0
- package/src/components/ui/radio-group.tsx +45 -0
- package/src/components/ui/scroll-area.tsx +58 -0
- package/src/components/ui/select.tsx +190 -0
- package/src/components/ui/separator.tsx +28 -0
- package/src/components/ui/sheet.tsx +143 -0
- package/src/components/ui/sidebar.tsx +726 -0
- package/src/components/ui/skeleton.tsx +13 -0
- package/src/components/ui/slider.tsx +63 -0
- package/src/components/ui/sonner.tsx +36 -0
- package/src/components/ui/switch.tsx +35 -0
- package/src/components/ui/table.tsx +116 -0
- package/src/components/ui/tabs.tsx +91 -0
- package/src/components/ui/textarea.tsx +18 -0
- package/src/components/ui/tooltip.tsx +57 -0
- package/src/components/workflows/blueprint-editor.tsx +109 -0
- package/src/components/workflows/blueprint-gallery.tsx +155 -0
- package/src/components/workflows/blueprint-preview.tsx +240 -0
- package/src/components/workflows/loop-status-view.tsx +272 -0
- package/src/components/workflows/swarm-dashboard.tsx +185 -0
- package/src/components/workflows/workflow-form-view.tsx +1376 -0
- package/src/components/workflows/workflow-list.tsx +230 -0
- package/src/components/workflows/workflow-status-view.tsx +477 -0
- package/src/hooks/use-mobile.ts +19 -0
- package/src/instrumentation.ts +7 -0
- package/src/lib/agents/claude-agent.ts +737 -0
- package/src/lib/agents/execution-manager.ts +27 -0
- package/src/lib/agents/profiles/assignment-validation.ts +75 -0
- package/src/lib/agents/profiles/builtins/code-reviewer/SKILL.md +21 -0
- package/src/lib/agents/profiles/builtins/code-reviewer/profile.yaml +28 -0
- package/src/lib/agents/profiles/builtins/data-analyst/SKILL.md +25 -0
- package/src/lib/agents/profiles/builtins/data-analyst/profile.yaml +27 -0
- package/src/lib/agents/profiles/builtins/devops-engineer/SKILL.md +34 -0
- package/src/lib/agents/profiles/builtins/devops-engineer/profile.yaml +27 -0
- package/src/lib/agents/profiles/builtins/document-writer/SKILL.md +16 -0
- package/src/lib/agents/profiles/builtins/document-writer/profile.yaml +27 -0
- package/src/lib/agents/profiles/builtins/general/SKILL.md +13 -0
- package/src/lib/agents/profiles/builtins/general/profile.yaml +18 -0
- package/src/lib/agents/profiles/builtins/health-fitness-coach/SKILL.md +34 -0
- package/src/lib/agents/profiles/builtins/health-fitness-coach/profile.yaml +26 -0
- package/src/lib/agents/profiles/builtins/learning-coach/SKILL.md +35 -0
- package/src/lib/agents/profiles/builtins/learning-coach/profile.yaml +26 -0
- package/src/lib/agents/profiles/builtins/project-manager/SKILL.md +26 -0
- package/src/lib/agents/profiles/builtins/project-manager/profile.yaml +26 -0
- package/src/lib/agents/profiles/builtins/researcher/SKILL.md +15 -0
- package/src/lib/agents/profiles/builtins/researcher/profile.yaml +27 -0
- package/src/lib/agents/profiles/builtins/shopping-assistant/SKILL.md +34 -0
- package/src/lib/agents/profiles/builtins/shopping-assistant/profile.yaml +26 -0
- package/src/lib/agents/profiles/builtins/technical-writer/SKILL.md +31 -0
- package/src/lib/agents/profiles/builtins/technical-writer/profile.yaml +29 -0
- package/src/lib/agents/profiles/builtins/travel-planner/SKILL.md +23 -0
- package/src/lib/agents/profiles/builtins/travel-planner/profile.yaml +26 -0
- package/src/lib/agents/profiles/builtins/wealth-manager/SKILL.md +24 -0
- package/src/lib/agents/profiles/builtins/wealth-manager/profile.yaml +26 -0
- package/src/lib/agents/profiles/compatibility.ts +109 -0
- package/src/lib/agents/profiles/registry.ts +293 -0
- package/src/lib/agents/profiles/test-runner.ts +18 -0
- package/src/lib/agents/profiles/test-types.ts +20 -0
- package/src/lib/agents/profiles/types.ts +43 -0
- package/src/lib/agents/router.ts +56 -0
- package/src/lib/agents/runtime/catalog.ts +85 -0
- package/src/lib/agents/runtime/claude-sdk.ts +12 -0
- package/src/lib/agents/runtime/claude.ts +370 -0
- package/src/lib/agents/runtime/codex-app-server-client.ts +289 -0
- package/src/lib/agents/runtime/index.ts +167 -0
- package/src/lib/agents/runtime/openai-codex.ts +1089 -0
- package/src/lib/agents/runtime/task-assist-types.ts +8 -0
- package/src/lib/agents/runtime/types.ts +30 -0
- package/src/lib/constants/settings.ts +13 -0
- package/src/lib/constants/status-colors.ts +44 -0
- package/src/lib/constants/task-status.ts +49 -0
- package/src/lib/data/clear.ts +63 -0
- package/src/lib/data/seed-data/documents.ts +715 -0
- package/src/lib/data/seed-data/logs.ts +195 -0
- package/src/lib/data/seed-data/notifications.ts +141 -0
- package/src/lib/data/seed-data/profiles.ts +175 -0
- package/src/lib/data/seed-data/projects.ts +61 -0
- package/src/lib/data/seed-data/schedules.ts +108 -0
- package/src/lib/data/seed-data/tasks.ts +341 -0
- package/src/lib/data/seed-data/usage-ledger.ts +130 -0
- package/src/lib/data/seed-data/workflows.ts +213 -0
- package/src/lib/data/seed.ts +129 -0
- package/src/lib/db/index.ts +221 -0
- package/src/lib/db/migrations/0000_aromatic_gargoyle.sql +59 -0
- package/src/lib/db/migrations/0001_first_iron_patriot.sql +6 -0
- package/src/lib/db/migrations/0002_add_resume_count.sql +1 -0
- package/src/lib/db/migrations/0003_add_settings.sql +5 -0
- package/src/lib/db/migrations/0004_add_documents.sql +20 -0
- package/src/lib/db/migrations/0005_add_document_preprocessing.sql +4 -0
- package/src/lib/db/migrations/0006_add_agent_profile.sql +2 -0
- package/src/lib/db/migrations/0007_add_usage_metering_ledger.sql +30 -0
- package/src/lib/db/migrations/0008_add_document_version.sql +1 -0
- package/src/lib/db/migrations/meta/0000_snapshot.json +416 -0
- package/src/lib/db/migrations/meta/0001_snapshot.json +461 -0
- package/src/lib/db/migrations/meta/0002_snapshot.json +469 -0
- package/src/lib/db/migrations/meta/_journal.json +27 -0
- package/src/lib/db/schema.ts +227 -0
- package/src/lib/documents/cleanup.ts +50 -0
- package/src/lib/documents/context-builder.ts +75 -0
- package/src/lib/documents/output-scanner.ts +166 -0
- package/src/lib/documents/processor.ts +120 -0
- package/src/lib/documents/processors/image.ts +21 -0
- package/src/lib/documents/processors/office.ts +36 -0
- package/src/lib/documents/processors/pdf.ts +12 -0
- package/src/lib/documents/processors/spreadsheet.ts +18 -0
- package/src/lib/documents/processors/text.ts +8 -0
- package/src/lib/documents/registry.ts +25 -0
- package/src/lib/notifications/actionable.ts +108 -0
- package/src/lib/notifications/permissions.ts +169 -0
- package/src/lib/queries/chart-data.ts +184 -0
- package/src/lib/schedules/interval-parser.ts +110 -0
- package/src/lib/schedules/scheduler.ts +220 -0
- package/src/lib/settings/auth.ts +98 -0
- package/src/lib/settings/budget-guardrails.ts +590 -0
- package/src/lib/settings/helpers.ts +23 -0
- package/src/lib/settings/openai-auth.ts +80 -0
- package/src/lib/settings/permissions.ts +102 -0
- package/src/lib/usage/ledger.ts +489 -0
- package/src/lib/usage/pricing.ts +68 -0
- package/src/lib/utils/crypto.ts +90 -0
- package/src/lib/utils/format-timestamp.ts +46 -0
- package/src/lib/utils/session-cleanup.ts +26 -0
- package/src/lib/utils/stagent-paths.ts +18 -0
- package/src/lib/utils.ts +6 -0
- package/src/lib/validators/blueprint.ts +43 -0
- package/src/lib/validators/profile.ts +64 -0
- package/src/lib/validators/project.ts +17 -0
- package/src/lib/validators/settings.ts +57 -0
- package/src/lib/validators/task.ts +30 -0
- package/src/lib/workflows/blueprints/builtins/code-review-pipeline.yaml +72 -0
- package/src/lib/workflows/blueprints/builtins/documentation-generation.yaml +62 -0
- package/src/lib/workflows/blueprints/builtins/investment-research.yaml +81 -0
- package/src/lib/workflows/blueprints/builtins/meal-planning.yaml +73 -0
- package/src/lib/workflows/blueprints/builtins/product-research.yaml +72 -0
- package/src/lib/workflows/blueprints/builtins/research-report.yaml +77 -0
- package/src/lib/workflows/blueprints/builtins/sprint-planning.yaml +77 -0
- package/src/lib/workflows/blueprints/builtins/travel-planning.yaml +80 -0
- package/src/lib/workflows/blueprints/instantiator.ts +131 -0
- package/src/lib/workflows/blueprints/registry.ts +128 -0
- package/src/lib/workflows/blueprints/template.ts +58 -0
- package/src/lib/workflows/blueprints/types.ts +38 -0
- package/src/lib/workflows/definition-validation.ts +121 -0
- package/src/lib/workflows/engine.ts +1113 -0
- package/src/lib/workflows/loop-executor.ts +270 -0
- package/src/lib/workflows/parallel.ts +55 -0
- package/src/lib/workflows/swarm.ts +97 -0
- package/src/lib/workflows/types.ts +112 -0
- package/tsconfig.json +41 -0
package/src/app/icon.tsx
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { ImageResponse } from "next/og";
|
|
2
|
+
import { readFileSync } from "fs";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
|
|
5
|
+
export const size = { width: 32, height: 32 };
|
|
6
|
+
export const contentType = "image/png";
|
|
7
|
+
|
|
8
|
+
export default function Icon() {
|
|
9
|
+
const logoData = readFileSync(join(process.cwd(), "public/stagent-s-64.png"));
|
|
10
|
+
const logoSrc = `data:image/png;base64,${logoData.toString("base64")}`;
|
|
11
|
+
|
|
12
|
+
return new ImageResponse(
|
|
13
|
+
(
|
|
14
|
+
<div
|
|
15
|
+
style={{
|
|
16
|
+
width: "100%",
|
|
17
|
+
height: "100%",
|
|
18
|
+
display: "flex",
|
|
19
|
+
alignItems: "center",
|
|
20
|
+
justifyContent: "center",
|
|
21
|
+
background: "transparent",
|
|
22
|
+
}}
|
|
23
|
+
>
|
|
24
|
+
{/* eslint-disable-next-line @next/next/no-img-element */}
|
|
25
|
+
<img src={logoSrc} width="30" height="30" alt="" />
|
|
26
|
+
</div>
|
|
27
|
+
),
|
|
28
|
+
{ ...size }
|
|
29
|
+
);
|
|
30
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Skeleton } from "@/components/ui/skeleton";
|
|
2
|
+
|
|
3
|
+
export default function InboxLoading() {
|
|
4
|
+
return (
|
|
5
|
+
<div className="p-6">
|
|
6
|
+
<Skeleton className="h-8 w-24 mb-6" />
|
|
7
|
+
<Skeleton className="h-10 w-80 mb-4 rounded-md" />
|
|
8
|
+
<div className="space-y-3">
|
|
9
|
+
{Array.from({ length: 5 }).map((_, i) => (
|
|
10
|
+
<Skeleton key={i} className="h-24 rounded-lg" />
|
|
11
|
+
))}
|
|
12
|
+
</div>
|
|
13
|
+
</div>
|
|
14
|
+
);
|
|
15
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { db } from "@/lib/db";
|
|
2
|
+
import { notifications } from "@/lib/db/schema";
|
|
3
|
+
import { desc } from "drizzle-orm";
|
|
4
|
+
import { InboxList } from "@/components/notifications/inbox-list";
|
|
5
|
+
|
|
6
|
+
export const dynamic = "force-dynamic";
|
|
7
|
+
|
|
8
|
+
export default async function InboxPage() {
|
|
9
|
+
const rows = await db
|
|
10
|
+
.select()
|
|
11
|
+
.from(notifications)
|
|
12
|
+
.orderBy(desc(notifications.createdAt))
|
|
13
|
+
.limit(100);
|
|
14
|
+
|
|
15
|
+
// Serialize Date objects for client component consumption
|
|
16
|
+
const initialNotifications = rows.map((n) => ({
|
|
17
|
+
...n,
|
|
18
|
+
createdAt: n.createdAt.toISOString(),
|
|
19
|
+
respondedAt: n.respondedAt?.toISOString() ?? null,
|
|
20
|
+
}));
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<div className="gradient-sunset-glow min-h-screen p-4 sm:p-6">
|
|
24
|
+
<div className="surface-page surface-page-shell mx-auto min-h-[calc(100dvh-2rem)] max-w-6xl rounded-[30px] p-5 sm:p-6 lg:p-7">
|
|
25
|
+
<div className="mb-5 space-y-1">
|
|
26
|
+
<h1 className="text-2xl font-bold">Inbox</h1>
|
|
27
|
+
<p className="text-sm text-muted-foreground">
|
|
28
|
+
Review approvals, questions, failures, and completions without leaving the supervision flow.
|
|
29
|
+
</p>
|
|
30
|
+
</div>
|
|
31
|
+
<InboxList initialNotifications={initialNotifications} />
|
|
32
|
+
</div>
|
|
33
|
+
</div>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import type { Metadata } from "next";
|
|
2
|
+
import { Geist, Geist_Mono } from "next/font/google";
|
|
3
|
+
import { SidebarProvider, SidebarInset } from "@/components/ui/sidebar";
|
|
4
|
+
import { TooltipProvider } from "@/components/ui/tooltip";
|
|
5
|
+
import { AppSidebar } from "@/components/shared/app-sidebar";
|
|
6
|
+
import { CommandPalette } from "@/components/shared/command-palette";
|
|
7
|
+
import { PendingApprovalHost } from "@/components/notifications/pending-approval-host";
|
|
8
|
+
import { Toaster } from "@/components/ui/sonner";
|
|
9
|
+
import "./globals.css";
|
|
10
|
+
|
|
11
|
+
const geistSans = Geist({
|
|
12
|
+
variable: "--font-geist-sans",
|
|
13
|
+
subsets: ["latin"],
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
const geistMono = Geist_Mono({
|
|
17
|
+
variable: "--font-geist-mono",
|
|
18
|
+
subsets: ["latin"],
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
export const metadata: Metadata = {
|
|
22
|
+
title: "Stagent",
|
|
23
|
+
description: "AI agent task management",
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
// Inline theme bootstrap prevents a flash between the server render and local theme preference.
|
|
27
|
+
const CRITICAL_THEME_CSS = `
|
|
28
|
+
:root {
|
|
29
|
+
color-scheme: light;
|
|
30
|
+
--background: oklch(0.98 0.005 260);
|
|
31
|
+
--foreground: oklch(0.15 0.02 260);
|
|
32
|
+
--surface-1: rgba(255, 255, 255, 0.92);
|
|
33
|
+
--surface-2: rgba(255, 255, 255, 0.82);
|
|
34
|
+
--border: oklch(0.82 0.01 260 / 0.2);
|
|
35
|
+
}
|
|
36
|
+
html.dark {
|
|
37
|
+
color-scheme: dark;
|
|
38
|
+
--background: oklch(0.09 0.02 265);
|
|
39
|
+
--foreground: oklch(0.93 0.015 270);
|
|
40
|
+
--surface-1: oklch(0.16 0.02 268 / 0.96);
|
|
41
|
+
--surface-2: oklch(0.14 0.018 272 / 0.9);
|
|
42
|
+
--border: oklch(0.36 0.03 270 / 0.28);
|
|
43
|
+
}
|
|
44
|
+
html { background: var(--background); }
|
|
45
|
+
`.replace(/\s+/g, " ").trim();
|
|
46
|
+
|
|
47
|
+
// Static theme initialization script — no user input, safe from XSS.
|
|
48
|
+
const THEME_INIT_SCRIPT = `(function(){try{var d=document.documentElement;var s=localStorage.getItem('stagent-theme');var t=s==='dark'||s==='light'?s:(window.matchMedia('(prefers-color-scheme:dark)').matches?'dark':'light');d.classList.toggle('dark',t==='dark');d.dataset.theme=t;d.style.colorScheme=t;d.style.backgroundColor=t==='dark'?'oklch(0.09 0.02 265)':'oklch(0.98 0.005 260)';document.cookie='stagent-theme='+t+';path=/;max-age=31536000;SameSite=Lax';}catch(e){}})()`;
|
|
49
|
+
|
|
50
|
+
export default function RootLayout({
|
|
51
|
+
children,
|
|
52
|
+
}: {
|
|
53
|
+
children: React.ReactNode;
|
|
54
|
+
}) {
|
|
55
|
+
return (
|
|
56
|
+
<html lang="en" suppressHydrationWarning>
|
|
57
|
+
<head>
|
|
58
|
+
<style dangerouslySetInnerHTML={{ __html: CRITICAL_THEME_CSS }} />
|
|
59
|
+
<script dangerouslySetInnerHTML={{ __html: THEME_INIT_SCRIPT }} />
|
|
60
|
+
</head>
|
|
61
|
+
<body
|
|
62
|
+
className={`${geistSans.variable} ${geistMono.variable} font-sans antialiased bg-background text-foreground`}
|
|
63
|
+
>
|
|
64
|
+
<TooltipProvider>
|
|
65
|
+
<SidebarProvider>
|
|
66
|
+
<AppSidebar />
|
|
67
|
+
<SidebarInset className="min-w-0">
|
|
68
|
+
{children}
|
|
69
|
+
</SidebarInset>
|
|
70
|
+
</SidebarProvider>
|
|
71
|
+
<PendingApprovalHost />
|
|
72
|
+
<CommandPalette />
|
|
73
|
+
<Toaster />
|
|
74
|
+
</TooltipProvider>
|
|
75
|
+
</body>
|
|
76
|
+
</html>
|
|
77
|
+
);
|
|
78
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { MetadataRoute } from "next";
|
|
2
|
+
|
|
3
|
+
export default function manifest(): MetadataRoute.Manifest {
|
|
4
|
+
return {
|
|
5
|
+
name: "Stagent",
|
|
6
|
+
short_name: "Stagent",
|
|
7
|
+
description: "AI agent task management",
|
|
8
|
+
start_url: "/",
|
|
9
|
+
display: "standalone",
|
|
10
|
+
background_color: "#0f172a",
|
|
11
|
+
theme_color: "#2563eb",
|
|
12
|
+
icons: [
|
|
13
|
+
{
|
|
14
|
+
src: "/icon-512.png",
|
|
15
|
+
sizes: "512x512",
|
|
16
|
+
type: "image/png",
|
|
17
|
+
purpose: "any",
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
src: "/icon-512.png",
|
|
21
|
+
sizes: "512x512",
|
|
22
|
+
type: "image/png",
|
|
23
|
+
purpose: "maskable",
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
src: "/icon.svg",
|
|
27
|
+
sizes: "any",
|
|
28
|
+
type: "image/svg+xml",
|
|
29
|
+
},
|
|
30
|
+
],
|
|
31
|
+
};
|
|
32
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { Suspense } from "react";
|
|
2
|
+
import { db } from "@/lib/db";
|
|
3
|
+
import { tasks } from "@/lib/db/schema";
|
|
4
|
+
import { MonitorOverview } from "@/components/monitoring/monitor-overview";
|
|
5
|
+
import { MonitorRefreshButton } from "@/components/monitoring/monitor-overview-wrapper";
|
|
6
|
+
import { LogStream } from "@/components/monitoring/log-stream";
|
|
7
|
+
import { Skeleton } from "@/components/ui/skeleton";
|
|
8
|
+
|
|
9
|
+
export const dynamic = "force-dynamic";
|
|
10
|
+
|
|
11
|
+
export default async function MonitorPage() {
|
|
12
|
+
const activeTasks = await db
|
|
13
|
+
.select({ id: tasks.id, title: tasks.title })
|
|
14
|
+
.from(tasks)
|
|
15
|
+
.orderBy(tasks.createdAt);
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<div className="gradient-forest-dawn min-h-screen p-6">
|
|
19
|
+
<div className="flex items-center justify-between mb-6">
|
|
20
|
+
<h1 className="text-2xl font-bold">Monitor</h1>
|
|
21
|
+
<MonitorRefreshButton />
|
|
22
|
+
</div>
|
|
23
|
+
<Suspense
|
|
24
|
+
fallback={
|
|
25
|
+
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
|
|
26
|
+
{Array.from({ length: 4 }).map((_, i) => (
|
|
27
|
+
<Skeleton key={i} className="h-24 rounded-lg" />
|
|
28
|
+
))}
|
|
29
|
+
</div>
|
|
30
|
+
}
|
|
31
|
+
>
|
|
32
|
+
<MonitorOverview />
|
|
33
|
+
</Suspense>
|
|
34
|
+
<LogStream tasks={activeTasks} />
|
|
35
|
+
</div>
|
|
36
|
+
);
|
|
37
|
+
}
|
package/src/app/page.tsx
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { db } from "@/lib/db";
|
|
2
|
+
import { tasks, projects, agentLogs, notifications } from "@/lib/db/schema";
|
|
3
|
+
import { eq, count, gte, and, desc, sql, inArray } from "drizzle-orm";
|
|
4
|
+
import { Greeting } from "@/components/dashboard/greeting";
|
|
5
|
+
import { StatsCards } from "@/components/dashboard/stats-cards";
|
|
6
|
+
import { PriorityQueue } from "@/components/dashboard/priority-queue";
|
|
7
|
+
import type { PriorityTask } from "@/components/dashboard/priority-queue";
|
|
8
|
+
import { ActivityFeed } from "@/components/dashboard/activity-feed";
|
|
9
|
+
import type { ActivityEntry } from "@/components/dashboard/activity-feed";
|
|
10
|
+
import { QuickActions } from "@/components/dashboard/quick-actions";
|
|
11
|
+
import { RecentProjects } from "@/components/dashboard/recent-projects";
|
|
12
|
+
import type { RecentProject } from "@/components/dashboard/recent-projects";
|
|
13
|
+
import {
|
|
14
|
+
getCompletionsByDay,
|
|
15
|
+
getTaskCreationsByDay,
|
|
16
|
+
getActiveProjectActivityByDay,
|
|
17
|
+
getAgentActivityByHour,
|
|
18
|
+
getNotificationsByDay,
|
|
19
|
+
} from "@/lib/queries/chart-data";
|
|
20
|
+
|
|
21
|
+
export const dynamic = "force-dynamic";
|
|
22
|
+
|
|
23
|
+
export default async function HomePage() {
|
|
24
|
+
const today = new Date();
|
|
25
|
+
today.setHours(0, 0, 0, 0);
|
|
26
|
+
|
|
27
|
+
// Run all DB queries in parallel
|
|
28
|
+
const [
|
|
29
|
+
[runningResult],
|
|
30
|
+
[failedResult],
|
|
31
|
+
[completedTodayResult],
|
|
32
|
+
[completedAllTimeResult],
|
|
33
|
+
[awaitingResult],
|
|
34
|
+
[activeProjectsResult],
|
|
35
|
+
priorityTasks,
|
|
36
|
+
recentLogs,
|
|
37
|
+
allProjects,
|
|
38
|
+
recentActiveProjects,
|
|
39
|
+
completionsByDay,
|
|
40
|
+
taskCreationsByDay,
|
|
41
|
+
projectCreationsByDay,
|
|
42
|
+
agentActivityByHour,
|
|
43
|
+
notificationsByDay,
|
|
44
|
+
] = await Promise.all([
|
|
45
|
+
db.select({ count: count() }).from(tasks).where(eq(tasks.status, "running")),
|
|
46
|
+
db.select({ count: count() }).from(tasks).where(eq(tasks.status, "failed")),
|
|
47
|
+
db.select({ count: count() }).from(tasks).where(
|
|
48
|
+
and(eq(tasks.status, "completed"), gte(tasks.updatedAt, today))
|
|
49
|
+
),
|
|
50
|
+
db.select({ count: count() }).from(tasks).where(eq(tasks.status, "completed")),
|
|
51
|
+
db.select({ count: count() }).from(notifications).where(
|
|
52
|
+
and(
|
|
53
|
+
eq(notifications.read, false),
|
|
54
|
+
inArray(notifications.type, [
|
|
55
|
+
"permission_required",
|
|
56
|
+
"agent_message",
|
|
57
|
+
"budget_alert",
|
|
58
|
+
])
|
|
59
|
+
)
|
|
60
|
+
),
|
|
61
|
+
db.select({ count: count() }).from(projects).where(eq(projects.status, "active")),
|
|
62
|
+
// Priority queue: failed + running tasks, sorted by priority
|
|
63
|
+
db.select().from(tasks).where(
|
|
64
|
+
inArray(tasks.status, ["failed", "running", "queued"])
|
|
65
|
+
).orderBy(tasks.priority, desc(tasks.updatedAt)).limit(5),
|
|
66
|
+
// Recent agent logs
|
|
67
|
+
db.select().from(agentLogs).orderBy(desc(agentLogs.timestamp)).limit(6),
|
|
68
|
+
// All projects for quick actions
|
|
69
|
+
db.select({ id: projects.id, name: projects.name }).from(projects).orderBy(projects.name),
|
|
70
|
+
// Recent active projects with task counts
|
|
71
|
+
db.select({
|
|
72
|
+
id: projects.id,
|
|
73
|
+
name: projects.name,
|
|
74
|
+
}).from(projects)
|
|
75
|
+
.where(eq(projects.status, "active"))
|
|
76
|
+
.orderBy(desc(projects.updatedAt))
|
|
77
|
+
.limit(3),
|
|
78
|
+
// Chart data queries
|
|
79
|
+
getCompletionsByDay(7),
|
|
80
|
+
getTaskCreationsByDay(7),
|
|
81
|
+
getActiveProjectActivityByDay(7),
|
|
82
|
+
getAgentActivityByHour(),
|
|
83
|
+
getNotificationsByDay(7),
|
|
84
|
+
]);
|
|
85
|
+
|
|
86
|
+
// Build project name lookup for priority tasks
|
|
87
|
+
const projectMap = new Map(allProjects.map((p) => [p.id, p.name]));
|
|
88
|
+
|
|
89
|
+
const serializedPriorityTasks: PriorityTask[] = priorityTasks.map((t) => ({
|
|
90
|
+
id: t.id,
|
|
91
|
+
title: t.title,
|
|
92
|
+
status: t.status,
|
|
93
|
+
priority: t.priority,
|
|
94
|
+
projectName: t.projectId ? projectMap.get(t.projectId) ?? undefined : undefined,
|
|
95
|
+
}));
|
|
96
|
+
|
|
97
|
+
// Get task titles for log entries
|
|
98
|
+
const logTaskIds = [...new Set(recentLogs.filter((l) => l.taskId).map((l) => l.taskId!))];
|
|
99
|
+
const logTasks = logTaskIds.length > 0
|
|
100
|
+
? await db.select({ id: tasks.id, title: tasks.title }).from(tasks).where(inArray(tasks.id, logTaskIds))
|
|
101
|
+
: [];
|
|
102
|
+
const taskTitleMap = new Map(logTasks.map((t) => [t.id, t.title]));
|
|
103
|
+
|
|
104
|
+
const serializedLogs: ActivityEntry[] = recentLogs.map((l) => ({
|
|
105
|
+
id: l.id,
|
|
106
|
+
event: l.event,
|
|
107
|
+
payload: l.payload,
|
|
108
|
+
timestamp: l.timestamp.toISOString(),
|
|
109
|
+
taskTitle: l.taskId ? taskTitleMap.get(l.taskId) ?? undefined : undefined,
|
|
110
|
+
}));
|
|
111
|
+
|
|
112
|
+
// Get task counts per project for recent projects
|
|
113
|
+
const recentProjectData: RecentProject[] = await Promise.all(
|
|
114
|
+
recentActiveProjects.map(async (p) => {
|
|
115
|
+
const [total] = await db.select({ count: count() }).from(tasks).where(eq(tasks.projectId, p.id));
|
|
116
|
+
const [completed] = await db.select({ count: count() }).from(tasks).where(
|
|
117
|
+
and(eq(tasks.projectId, p.id), eq(tasks.status, "completed"))
|
|
118
|
+
);
|
|
119
|
+
return {
|
|
120
|
+
id: p.id,
|
|
121
|
+
name: p.name,
|
|
122
|
+
totalTasks: total.count,
|
|
123
|
+
completedTasks: completed.count,
|
|
124
|
+
};
|
|
125
|
+
})
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
return (
|
|
129
|
+
<div className="gradient-morning-sky min-h-screen p-4 sm:p-6">
|
|
130
|
+
<div className="surface-page surface-page-shell mx-auto min-h-[calc(100dvh-2rem)] max-w-7xl rounded-[30px] p-5 sm:p-6 lg:p-7">
|
|
131
|
+
<Greeting
|
|
132
|
+
runningCount={runningResult.count}
|
|
133
|
+
awaitingCount={awaitingResult.count}
|
|
134
|
+
failedCount={failedResult.count}
|
|
135
|
+
/>
|
|
136
|
+
<StatsCards
|
|
137
|
+
runningCount={runningResult.count}
|
|
138
|
+
completedToday={completedTodayResult.count}
|
|
139
|
+
completedAllTime={completedAllTimeResult.count}
|
|
140
|
+
awaitingReview={awaitingResult.count}
|
|
141
|
+
activeProjects={activeProjectsResult.count}
|
|
142
|
+
sparklines={{
|
|
143
|
+
completions: completionsByDay,
|
|
144
|
+
creations: taskCreationsByDay,
|
|
145
|
+
projects: projectCreationsByDay,
|
|
146
|
+
notifications: notificationsByDay,
|
|
147
|
+
}}
|
|
148
|
+
/>
|
|
149
|
+
<div className="grid grid-cols-1 gap-6 lg:grid-cols-5 mb-6">
|
|
150
|
+
<div className="lg:col-span-3">
|
|
151
|
+
<PriorityQueue tasks={serializedPriorityTasks} />
|
|
152
|
+
</div>
|
|
153
|
+
<div className="lg:col-span-2">
|
|
154
|
+
<ActivityFeed entries={serializedLogs} hourlyActivity={agentActivityByHour} />
|
|
155
|
+
</div>
|
|
156
|
+
</div>
|
|
157
|
+
<QuickActions />
|
|
158
|
+
<RecentProjects projects={recentProjectData} />
|
|
159
|
+
</div>
|
|
160
|
+
</div>
|
|
161
|
+
);
|
|
162
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { notFound } from "next/navigation";
|
|
2
|
+
import Link from "next/link";
|
|
3
|
+
import { getProfile, isBuiltin } from "@/lib/agents/profiles/registry";
|
|
4
|
+
import { Button } from "@/components/ui/button";
|
|
5
|
+
import { ArrowLeft } from "lucide-react";
|
|
6
|
+
import { ProfileFormView } from "@/components/profiles/profile-form-view";
|
|
7
|
+
|
|
8
|
+
export const dynamic = "force-dynamic";
|
|
9
|
+
|
|
10
|
+
export default async function EditProfilePage({
|
|
11
|
+
params,
|
|
12
|
+
searchParams,
|
|
13
|
+
}: {
|
|
14
|
+
params: Promise<{ id: string }>;
|
|
15
|
+
searchParams: Promise<{ duplicate?: string }>;
|
|
16
|
+
}) {
|
|
17
|
+
const { id } = await params;
|
|
18
|
+
const { duplicate } = await searchParams;
|
|
19
|
+
|
|
20
|
+
const profile = getProfile(id);
|
|
21
|
+
if (!profile) notFound();
|
|
22
|
+
|
|
23
|
+
// Builtins can't be edited, but can be duplicated
|
|
24
|
+
if (isBuiltin(id) && duplicate !== "true") notFound();
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<div className="gradient-ocean-mist min-h-screen p-6">
|
|
28
|
+
<div className="max-w-6xl mx-auto">
|
|
29
|
+
<Link href={duplicate === "true" ? "/profiles" : `/profiles/${id}`}>
|
|
30
|
+
<Button variant="ghost" size="sm" className="mb-4">
|
|
31
|
+
<ArrowLeft className="h-4 w-4 mr-1" />
|
|
32
|
+
{duplicate === "true" ? "Back to Profiles" : "Back to Profile"}
|
|
33
|
+
</Button>
|
|
34
|
+
</Link>
|
|
35
|
+
<ProfileFormView profileId={id} duplicate={duplicate === "true"} />
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { notFound } from "next/navigation";
|
|
2
|
+
import Link from "next/link";
|
|
3
|
+
import { getProfile, isBuiltin } from "@/lib/agents/profiles/registry";
|
|
4
|
+
import { Button } from "@/components/ui/button";
|
|
5
|
+
import { ArrowLeft } from "lucide-react";
|
|
6
|
+
import { ProfileDetailView } from "@/components/profiles/profile-detail-view";
|
|
7
|
+
|
|
8
|
+
export const dynamic = "force-dynamic";
|
|
9
|
+
|
|
10
|
+
export default async function ProfileDetailPage({
|
|
11
|
+
params,
|
|
12
|
+
}: {
|
|
13
|
+
params: Promise<{ id: string }>;
|
|
14
|
+
}) {
|
|
15
|
+
const { id } = await params;
|
|
16
|
+
|
|
17
|
+
const profile = getProfile(id);
|
|
18
|
+
if (!profile) notFound();
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<div className="gradient-ocean-mist min-h-[100dvh] p-4 sm:p-6">
|
|
22
|
+
<div className="surface-page mx-auto max-w-7xl rounded-[28px] border border-border/60 p-6 shadow-[0_18px_48px_oklch(0.12_0.02_260_/_0.08)]">
|
|
23
|
+
<Link href="/profiles">
|
|
24
|
+
<Button variant="ghost" size="sm" className="mb-4">
|
|
25
|
+
<ArrowLeft className="h-4 w-4 mr-1" />
|
|
26
|
+
Back to Profiles
|
|
27
|
+
</Button>
|
|
28
|
+
</Link>
|
|
29
|
+
<ProfileDetailView profileId={id} isBuiltin={isBuiltin(id)} initialProfile={profile} />
|
|
30
|
+
</div>
|
|
31
|
+
</div>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import Link from "next/link";
|
|
2
|
+
import { Button } from "@/components/ui/button";
|
|
3
|
+
import { ArrowLeft } from "lucide-react";
|
|
4
|
+
import { ProfileFormView } from "@/components/profiles/profile-form-view";
|
|
5
|
+
|
|
6
|
+
export const dynamic = "force-dynamic";
|
|
7
|
+
|
|
8
|
+
export default async function NewProfilePage() {
|
|
9
|
+
return (
|
|
10
|
+
<div className="gradient-ocean-mist min-h-screen p-6">
|
|
11
|
+
<div className="max-w-6xl mx-auto">
|
|
12
|
+
<Link href="/profiles">
|
|
13
|
+
<Button variant="ghost" size="sm" className="mb-4">
|
|
14
|
+
<ArrowLeft className="h-4 w-4 mr-1" />
|
|
15
|
+
Back to Profiles
|
|
16
|
+
</Button>
|
|
17
|
+
</Link>
|
|
18
|
+
<ProfileFormView />
|
|
19
|
+
</div>
|
|
20
|
+
</div>
|
|
21
|
+
);
|
|
22
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { listProfiles, isBuiltin } from "@/lib/agents/profiles/registry";
|
|
2
|
+
import { ProfileBrowser } from "@/components/profiles/profile-browser";
|
|
3
|
+
|
|
4
|
+
export const dynamic = "force-dynamic";
|
|
5
|
+
|
|
6
|
+
export default async function ProfilesPage() {
|
|
7
|
+
const profiles = listProfiles().map((p) => ({
|
|
8
|
+
...p,
|
|
9
|
+
isBuiltin: isBuiltin(p.id),
|
|
10
|
+
}));
|
|
11
|
+
|
|
12
|
+
return (
|
|
13
|
+
<div className="gradient-ocean-mist min-h-[100dvh] p-4 sm:p-6">
|
|
14
|
+
<div className="surface-page mx-auto max-w-7xl rounded-[28px] border border-border/60 p-6 shadow-[0_18px_48px_oklch(0.12_0.02_260_/_0.08)]">
|
|
15
|
+
<ProfileBrowser initialProfiles={profiles} />
|
|
16
|
+
</div>
|
|
17
|
+
</div>
|
|
18
|
+
);
|
|
19
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { notFound } from "next/navigation";
|
|
2
|
+
import Link from "next/link";
|
|
3
|
+
import { db } from "@/lib/db";
|
|
4
|
+
import { projects, tasks } from "@/lib/db/schema";
|
|
5
|
+
import { eq, count } from "drizzle-orm";
|
|
6
|
+
import { Badge } from "@/components/ui/badge";
|
|
7
|
+
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
8
|
+
import { Button } from "@/components/ui/button";
|
|
9
|
+
import { ArrowLeft } from "lucide-react";
|
|
10
|
+
import { COLUMN_ORDER } from "@/lib/constants/task-status";
|
|
11
|
+
import { ProjectDetailClient } from "@/components/projects/project-detail";
|
|
12
|
+
import { Sparkline } from "@/components/charts/sparkline";
|
|
13
|
+
import { getProjectCompletionTrend } from "@/lib/queries/chart-data";
|
|
14
|
+
|
|
15
|
+
export const dynamic = "force-dynamic";
|
|
16
|
+
|
|
17
|
+
export default async function ProjectDetailPage({
|
|
18
|
+
params,
|
|
19
|
+
}: {
|
|
20
|
+
params: Promise<{ id: string }>;
|
|
21
|
+
}) {
|
|
22
|
+
const { id } = await params;
|
|
23
|
+
|
|
24
|
+
const [project] = await db
|
|
25
|
+
.select()
|
|
26
|
+
.from(projects)
|
|
27
|
+
.where(eq(projects.id, id));
|
|
28
|
+
|
|
29
|
+
if (!project) notFound();
|
|
30
|
+
|
|
31
|
+
const projectTasks = await db
|
|
32
|
+
.select()
|
|
33
|
+
.from(tasks)
|
|
34
|
+
.where(eq(tasks.projectId, id))
|
|
35
|
+
.orderBy(tasks.priority, tasks.createdAt);
|
|
36
|
+
|
|
37
|
+
// Status breakdown
|
|
38
|
+
const statusCounts: Record<string, number> = {};
|
|
39
|
+
for (const status of COLUMN_ORDER) {
|
|
40
|
+
statusCounts[status] = projectTasks.filter((t) => t.status === status).length;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const completionTrend = await getProjectCompletionTrend(id, 14);
|
|
44
|
+
const totalTasks = projectTasks.length;
|
|
45
|
+
|
|
46
|
+
const serializedTasks = projectTasks.map((t) => ({
|
|
47
|
+
...t,
|
|
48
|
+
createdAt: t.createdAt.toISOString(),
|
|
49
|
+
updatedAt: t.updatedAt.toISOString(),
|
|
50
|
+
}));
|
|
51
|
+
|
|
52
|
+
const statusVariant: Record<string, "default" | "secondary" | "outline"> = {
|
|
53
|
+
active: "default",
|
|
54
|
+
paused: "secondary",
|
|
55
|
+
completed: "outline",
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<div className="gradient-ocean-mist min-h-screen p-6">
|
|
60
|
+
<div className="mb-6">
|
|
61
|
+
<Link href="/projects">
|
|
62
|
+
<Button variant="ghost" size="sm" className="mb-2">
|
|
63
|
+
<ArrowLeft className="h-4 w-4 mr-1" />
|
|
64
|
+
Back to Projects
|
|
65
|
+
</Button>
|
|
66
|
+
</Link>
|
|
67
|
+
<div className="flex items-center gap-3">
|
|
68
|
+
<h1 className="text-2xl font-bold">{project.name}</h1>
|
|
69
|
+
<Badge variant={statusVariant[project.status] ?? "secondary"}>
|
|
70
|
+
{project.status}
|
|
71
|
+
</Badge>
|
|
72
|
+
</div>
|
|
73
|
+
{project.description && (
|
|
74
|
+
<p className="text-muted-foreground mt-1">{project.description}</p>
|
|
75
|
+
)}
|
|
76
|
+
</div>
|
|
77
|
+
|
|
78
|
+
{/* Status breakdown */}
|
|
79
|
+
<div className="grid grid-cols-3 sm:grid-cols-6 gap-3 mb-6">
|
|
80
|
+
{COLUMN_ORDER.map((status) => (
|
|
81
|
+
<Card key={status}>
|
|
82
|
+
<CardHeader className="pb-1 pt-3 px-3">
|
|
83
|
+
<CardTitle className="text-xs font-medium text-muted-foreground capitalize">
|
|
84
|
+
{status}
|
|
85
|
+
</CardTitle>
|
|
86
|
+
</CardHeader>
|
|
87
|
+
<CardContent className="px-3 pb-3">
|
|
88
|
+
<div className="text-xl font-bold">{statusCounts[status]}</div>
|
|
89
|
+
</CardContent>
|
|
90
|
+
</Card>
|
|
91
|
+
))}
|
|
92
|
+
</div>
|
|
93
|
+
|
|
94
|
+
{/* Stacked status bar + completion sparkline */}
|
|
95
|
+
{totalTasks > 0 && (
|
|
96
|
+
<div className="mb-6 space-y-3">
|
|
97
|
+
<div className="flex h-1.5 rounded-full overflow-hidden" role="img" aria-label="Task status distribution">
|
|
98
|
+
{COLUMN_ORDER.map((status) => {
|
|
99
|
+
const pct = (statusCounts[status] / totalTasks) * 100;
|
|
100
|
+
if (pct === 0) return null;
|
|
101
|
+
const statusColors: Record<string, string> = {
|
|
102
|
+
planned: "var(--muted-foreground)",
|
|
103
|
+
queued: "var(--chart-4)",
|
|
104
|
+
running: "var(--chart-1)",
|
|
105
|
+
completed: "var(--chart-2)",
|
|
106
|
+
failed: "var(--destructive)",
|
|
107
|
+
};
|
|
108
|
+
return (
|
|
109
|
+
<div
|
|
110
|
+
key={status}
|
|
111
|
+
style={{ width: `${pct}%`, backgroundColor: statusColors[status] ?? "var(--muted)" }}
|
|
112
|
+
title={`${status}: ${statusCounts[status]}`}
|
|
113
|
+
/>
|
|
114
|
+
);
|
|
115
|
+
})}
|
|
116
|
+
</div>
|
|
117
|
+
<div className="flex items-center gap-3">
|
|
118
|
+
<span className="text-xs text-muted-foreground shrink-0">14-day completions</span>
|
|
119
|
+
<Sparkline
|
|
120
|
+
data={completionTrend}
|
|
121
|
+
width={200}
|
|
122
|
+
height={24}
|
|
123
|
+
color="var(--chart-2)"
|
|
124
|
+
label="14-day completion trend"
|
|
125
|
+
className="flex-1"
|
|
126
|
+
/>
|
|
127
|
+
</div>
|
|
128
|
+
</div>
|
|
129
|
+
)}
|
|
130
|
+
|
|
131
|
+
<ProjectDetailClient tasks={serializedTasks} projectId={id} />
|
|
132
|
+
</div>
|
|
133
|
+
);
|
|
134
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Skeleton } from "@/components/ui/skeleton";
|
|
2
|
+
|
|
3
|
+
export default function ProjectsLoading() {
|
|
4
|
+
return (
|
|
5
|
+
<div className="p-6">
|
|
6
|
+
<div className="flex items-center justify-between mb-6">
|
|
7
|
+
<Skeleton className="h-8 w-32" />
|
|
8
|
+
<Skeleton className="h-10 w-32 rounded-md" />
|
|
9
|
+
</div>
|
|
10
|
+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
|
11
|
+
{Array.from({ length: 6 }).map((_, i) => (
|
|
12
|
+
<Skeleton key={i} className="h-32 rounded-lg" />
|
|
13
|
+
))}
|
|
14
|
+
</div>
|
|
15
|
+
</div>
|
|
16
|
+
);
|
|
17
|
+
}
|