feed-the-machine 1.1.0 → 1.3.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/bin/generate-manifest.mjs +253 -0
- package/bin/install.mjs +372 -26
- package/docs/INBOX.md +233 -0
- package/ftm/SKILL.md +34 -0
- package/ftm-audit/SKILL.md +69 -0
- package/ftm-brainstorm/SKILL.md +51 -0
- package/ftm-browse/SKILL.md +39 -0
- package/ftm-capture/SKILL.md +370 -0
- package/ftm-capture.yml +4 -0
- package/ftm-codex-gate/SKILL.md +59 -0
- package/ftm-config/SKILL.md +35 -0
- package/ftm-council/SKILL.md +56 -0
- package/ftm-dashboard/SKILL.md +34 -0
- package/ftm-debug/SKILL.md +84 -0
- package/ftm-diagram/SKILL.md +44 -0
- package/ftm-executor/SKILL.md +97 -0
- package/ftm-git/SKILL.md +60 -0
- package/ftm-inbox/backend/__init__.py +0 -0
- package/ftm-inbox/backend/adapters/__init__.py +0 -0
- package/ftm-inbox/backend/adapters/_retry.py +64 -0
- package/ftm-inbox/backend/adapters/base.py +230 -0
- package/ftm-inbox/backend/adapters/freshservice.py +104 -0
- package/ftm-inbox/backend/adapters/gmail.py +125 -0
- package/ftm-inbox/backend/adapters/jira.py +136 -0
- package/ftm-inbox/backend/adapters/registry.py +192 -0
- package/ftm-inbox/backend/adapters/slack.py +110 -0
- package/ftm-inbox/backend/db/__init__.py +0 -0
- package/ftm-inbox/backend/db/connection.py +54 -0
- package/ftm-inbox/backend/db/schema.py +78 -0
- package/ftm-inbox/backend/executor/__init__.py +7 -0
- package/ftm-inbox/backend/executor/engine.py +149 -0
- package/ftm-inbox/backend/executor/step_runner.py +98 -0
- package/ftm-inbox/backend/main.py +103 -0
- package/ftm-inbox/backend/models/__init__.py +1 -0
- package/ftm-inbox/backend/models/unified_task.py +36 -0
- package/ftm-inbox/backend/planner/__init__.py +6 -0
- package/ftm-inbox/backend/planner/generator.py +127 -0
- package/ftm-inbox/backend/planner/schema.py +34 -0
- package/ftm-inbox/backend/requirements.txt +5 -0
- package/ftm-inbox/backend/routes/__init__.py +0 -0
- package/ftm-inbox/backend/routes/execute.py +186 -0
- package/ftm-inbox/backend/routes/health.py +52 -0
- package/ftm-inbox/backend/routes/inbox.py +68 -0
- package/ftm-inbox/backend/routes/plan.py +271 -0
- package/ftm-inbox/bin/launchagent.mjs +91 -0
- package/ftm-inbox/bin/setup.mjs +188 -0
- package/ftm-inbox/bin/start.sh +10 -0
- package/ftm-inbox/bin/status.sh +17 -0
- package/ftm-inbox/bin/stop.sh +8 -0
- package/ftm-inbox/config.example.yml +55 -0
- package/ftm-inbox/package-lock.json +2898 -0
- package/ftm-inbox/package.json +26 -0
- package/ftm-inbox/postcss.config.js +6 -0
- package/ftm-inbox/src/app.css +199 -0
- package/ftm-inbox/src/app.html +18 -0
- package/ftm-inbox/src/lib/api.ts +166 -0
- package/ftm-inbox/src/lib/components/ExecutionLog.svelte +81 -0
- package/ftm-inbox/src/lib/components/InboxFeed.svelte +143 -0
- package/ftm-inbox/src/lib/components/PlanStep.svelte +271 -0
- package/ftm-inbox/src/lib/components/PlanView.svelte +206 -0
- package/ftm-inbox/src/lib/components/StreamPanel.svelte +99 -0
- package/ftm-inbox/src/lib/components/TaskCard.svelte +190 -0
- package/ftm-inbox/src/lib/components/ui/EmptyState.svelte +63 -0
- package/ftm-inbox/src/lib/components/ui/KawaiiCard.svelte +86 -0
- package/ftm-inbox/src/lib/components/ui/PillButton.svelte +106 -0
- package/ftm-inbox/src/lib/components/ui/StatusBadge.svelte +67 -0
- package/ftm-inbox/src/lib/components/ui/StreamDrawer.svelte +149 -0
- package/ftm-inbox/src/lib/components/ui/ThemeToggle.svelte +80 -0
- package/ftm-inbox/src/lib/theme.ts +47 -0
- package/ftm-inbox/src/routes/+layout.svelte +76 -0
- package/ftm-inbox/src/routes/+page.svelte +401 -0
- package/ftm-inbox/static/favicon.png +0 -0
- package/ftm-inbox/svelte.config.js +12 -0
- package/ftm-inbox/tailwind.config.ts +63 -0
- package/ftm-inbox/tsconfig.json +13 -0
- package/ftm-inbox/vite.config.ts +6 -0
- package/ftm-intent/SKILL.md +44 -0
- package/ftm-manifest.json +3794 -0
- package/ftm-map/SKILL.md +50 -0
- package/ftm-mind/SKILL.md +173 -66
- package/ftm-pause/SKILL.md +43 -0
- package/ftm-researcher/SKILL.md +55 -0
- package/ftm-resume/SKILL.md +47 -0
- package/ftm-retro/SKILL.md +54 -0
- package/ftm-routine/SKILL.md +36 -0
- package/ftm-state/blackboard/capabilities.json +5 -0
- package/ftm-state/blackboard/capabilities.schema.json +27 -0
- package/ftm-upgrade/SKILL.md +41 -0
- package/hooks/ftm-blackboard-enforcer.sh +28 -27
- package/hooks/ftm-plan-gate.sh +21 -25
- package/install.sh +238 -111
- package/package.json +6 -2
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ftm-inbox",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "vite dev",
|
|
8
|
+
"build": "vite build",
|
|
9
|
+
"preview": "vite preview",
|
|
10
|
+
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
|
11
|
+
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch"
|
|
12
|
+
},
|
|
13
|
+
"devDependencies": {
|
|
14
|
+
"@sveltejs/adapter-auto": "^3.0.0",
|
|
15
|
+
"@sveltejs/kit": "^2.0.0",
|
|
16
|
+
"@sveltejs/vite-plugin-svelte": "^3.0.0",
|
|
17
|
+
"autoprefixer": "^10.4.16",
|
|
18
|
+
"postcss": "^8.4.32",
|
|
19
|
+
"svelte": "^4.2.7",
|
|
20
|
+
"svelte-check": "^3.6.0",
|
|
21
|
+
"tailwindcss": "^3.4.0",
|
|
22
|
+
"tslib": "^2.4.1",
|
|
23
|
+
"typescript": "^5.0.0",
|
|
24
|
+
"vite": "^5.0.3"
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
@tailwind base;
|
|
2
|
+
@tailwind components;
|
|
3
|
+
@tailwind utilities;
|
|
4
|
+
|
|
5
|
+
/* ─── Kawaii CSS Custom Properties ─── */
|
|
6
|
+
|
|
7
|
+
.theme-light {
|
|
8
|
+
--bg-primary: #e8f5e9;
|
|
9
|
+
--bg-secondary: #f1f8f1;
|
|
10
|
+
--bg-card: #fefefe;
|
|
11
|
+
--bg-sidebar: #f5fbf5;
|
|
12
|
+
--bg-drawer: #fafafa;
|
|
13
|
+
--border-accent: #a5d6a7;
|
|
14
|
+
--border-card: #c8e6c9;
|
|
15
|
+
--text-primary: #1b3a1b;
|
|
16
|
+
--text-secondary: #4a7a4a;
|
|
17
|
+
--text-muted: #7aaa7a;
|
|
18
|
+
--accent-primary: #4caf50;
|
|
19
|
+
--accent-hover: #388e3c;
|
|
20
|
+
--shadow-card: 0 4px 24px 0 rgba(76, 175, 80, 0.10);
|
|
21
|
+
--shadow-card-hover: 0 6px 32px 0 rgba(76, 175, 80, 0.18);
|
|
22
|
+
--grid-color: rgba(76, 175, 80, 0.07);
|
|
23
|
+
--scrollbar-thumb: #c8e6c9;
|
|
24
|
+
--scrollbar-track: #f1f8f1;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.theme-dark {
|
|
28
|
+
--bg-primary: #1b2e1b;
|
|
29
|
+
--bg-secondary: #1e331e;
|
|
30
|
+
--bg-card: #243824;
|
|
31
|
+
--bg-sidebar: #1e331e;
|
|
32
|
+
--bg-drawer: #1a2a1a;
|
|
33
|
+
--border-accent: #69f0ae;
|
|
34
|
+
--border-card: #2e7d32;
|
|
35
|
+
--text-primary: #e8f5e9;
|
|
36
|
+
--text-secondary: #a5d6a7;
|
|
37
|
+
--text-muted: #66bb6a;
|
|
38
|
+
--accent-primary: #69f0ae;
|
|
39
|
+
--accent-hover: #b2ffdb;
|
|
40
|
+
--shadow-card: 0 0 0 1px #69f0ae33, 0 4px 24px 0 rgba(105, 240, 174, 0.12);
|
|
41
|
+
--shadow-card-hover: 0 0 0 2px #69f0ae55, 0 6px 32px 0 rgba(105, 240, 174, 0.22);
|
|
42
|
+
--grid-color: rgba(105, 240, 174, 0.05);
|
|
43
|
+
--scrollbar-thumb: #2e7d32;
|
|
44
|
+
--scrollbar-track: #1b2e1b;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/* ─── Base Styles ─── */
|
|
48
|
+
|
|
49
|
+
*,
|
|
50
|
+
*::before,
|
|
51
|
+
*::after {
|
|
52
|
+
box-sizing: border-box;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
html {
|
|
56
|
+
height: 100%;
|
|
57
|
+
font-size: 16px;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
body {
|
|
61
|
+
margin: 0;
|
|
62
|
+
padding: 0;
|
|
63
|
+
min-height: 100vh;
|
|
64
|
+
background-color: var(--bg-primary);
|
|
65
|
+
color: var(--text-primary);
|
|
66
|
+
font-family: 'Nunito', 'Quicksand', sans-serif;
|
|
67
|
+
transition: background-color 0.3s ease, color 0.3s ease;
|
|
68
|
+
/* Graph-paper grid background */
|
|
69
|
+
background-image:
|
|
70
|
+
linear-gradient(var(--grid-color) 1px, transparent 1px),
|
|
71
|
+
linear-gradient(90deg, var(--grid-color) 1px, transparent 1px);
|
|
72
|
+
background-size: 30px 30px;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/* ─── Scrollbar ─── */
|
|
76
|
+
|
|
77
|
+
::-webkit-scrollbar {
|
|
78
|
+
width: 6px;
|
|
79
|
+
height: 6px;
|
|
80
|
+
}
|
|
81
|
+
::-webkit-scrollbar-track {
|
|
82
|
+
background: var(--scrollbar-track);
|
|
83
|
+
}
|
|
84
|
+
::-webkit-scrollbar-thumb {
|
|
85
|
+
background: var(--scrollbar-thumb);
|
|
86
|
+
border-radius: 9999px;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/* ─── Typography ─── */
|
|
90
|
+
|
|
91
|
+
h1, h2, h3, h4, h5, h6 {
|
|
92
|
+
font-family: 'Nunito', sans-serif;
|
|
93
|
+
font-weight: 800;
|
|
94
|
+
color: var(--text-primary);
|
|
95
|
+
margin: 0;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
p {
|
|
99
|
+
margin: 0;
|
|
100
|
+
line-height: 1.6;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/* ─── Kawaii Card ─── */
|
|
104
|
+
|
|
105
|
+
.card-kawaii {
|
|
106
|
+
background: var(--bg-card);
|
|
107
|
+
border: 2px solid var(--border-card);
|
|
108
|
+
border-radius: 16px;
|
|
109
|
+
box-shadow: var(--shadow-card);
|
|
110
|
+
transition: box-shadow 0.2s ease, transform 0.2s cubic-bezier(0.68, -0.55, 0.265, 1.55);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
.card-kawaii:hover {
|
|
114
|
+
box-shadow: var(--shadow-card-hover);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/* ─── Pill Button ─── */
|
|
118
|
+
|
|
119
|
+
.btn-pill {
|
|
120
|
+
display: inline-flex;
|
|
121
|
+
align-items: center;
|
|
122
|
+
gap: 0.5rem;
|
|
123
|
+
padding: 0.5rem 1.25rem;
|
|
124
|
+
border-radius: 9999px;
|
|
125
|
+
font-family: 'Nunito', sans-serif;
|
|
126
|
+
font-weight: 700;
|
|
127
|
+
font-size: 0.875rem;
|
|
128
|
+
cursor: pointer;
|
|
129
|
+
border: none;
|
|
130
|
+
transition: transform 0.18s cubic-bezier(0.68, -0.55, 0.265, 1.55),
|
|
131
|
+
box-shadow 0.18s ease,
|
|
132
|
+
background-color 0.18s ease;
|
|
133
|
+
user-select: none;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
.btn-pill:hover {
|
|
137
|
+
transform: scale(1.06) translateY(-1px);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
.btn-pill:active {
|
|
141
|
+
transform: scale(0.96);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
.btn-pill-primary {
|
|
145
|
+
background-color: var(--accent-primary);
|
|
146
|
+
color: #fff;
|
|
147
|
+
box-shadow: 0 2px 8px rgba(76, 175, 80, 0.3);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
.btn-pill-primary:hover {
|
|
151
|
+
background-color: var(--accent-hover);
|
|
152
|
+
box-shadow: 0 4px 16px rgba(76, 175, 80, 0.4);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
.btn-pill-ghost {
|
|
156
|
+
background-color: transparent;
|
|
157
|
+
color: var(--text-secondary);
|
|
158
|
+
border: 2px solid var(--border-card);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
.btn-pill-ghost:hover {
|
|
162
|
+
border-color: var(--accent-primary);
|
|
163
|
+
color: var(--accent-primary);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/* ─── Status Badge Colors ─── */
|
|
167
|
+
|
|
168
|
+
.badge-pending { background: #fff9c4; color: #5d4037; }
|
|
169
|
+
.badge-planning { background: #bbdefb; color: #0d47a1; }
|
|
170
|
+
.badge-approved { background: #c8e6c9; color: #1b5e20; }
|
|
171
|
+
.badge-executing { background: #ffe0b2; color: #bf360c; }
|
|
172
|
+
.badge-complete { background: #b2dfdb; color: #004d40; }
|
|
173
|
+
.badge-failed { background: #ffcdd2; color: #b71c1c; }
|
|
174
|
+
|
|
175
|
+
/* ─── Animations ─── */
|
|
176
|
+
|
|
177
|
+
@keyframes bounceIn {
|
|
178
|
+
0% { opacity: 0; transform: scale(0.8); }
|
|
179
|
+
100% { opacity: 1; transform: scale(1); }
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
@keyframes fadeUp {
|
|
183
|
+
0% { opacity: 0; transform: translateY(8px); }
|
|
184
|
+
100% { opacity: 1; transform: translateY(0); }
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
@keyframes slideDown {
|
|
188
|
+
0% { opacity: 0; transform: translateY(-8px); }
|
|
189
|
+
100% { opacity: 1; transform: translateY(0); }
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
@keyframes drawerOpen {
|
|
193
|
+
0% { transform: translateY(100%); }
|
|
194
|
+
100% { transform: translateY(0); }
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
.animate-bounce-in { animation: bounceIn 0.4s cubic-bezier(0.68, -0.55, 0.265, 1.55) both; }
|
|
198
|
+
.animate-fade-up { animation: fadeUp 0.3s ease-out both; }
|
|
199
|
+
.animate-slide-down { animation: slideDown 0.3s ease-out both; }
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
7
|
+
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
8
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin="anonymous" />
|
|
9
|
+
<link
|
|
10
|
+
href="https://fonts.googleapis.com/css2?family=Nunito:ital,wght@0,400;0,600;0,700;0,800;1,400&display=swap"
|
|
11
|
+
rel="stylesheet"
|
|
12
|
+
/>
|
|
13
|
+
%sveltekit.head%
|
|
14
|
+
</head>
|
|
15
|
+
<body data-sveltekit-preload-data="hover" class="theme-light">
|
|
16
|
+
<div style="display: contents">%sveltekit.body%</div>
|
|
17
|
+
</body>
|
|
18
|
+
</html>
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API client for the FTM Inbox backend.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const API_BASE = 'http://localhost:8042';
|
|
6
|
+
|
|
7
|
+
export interface UnifiedTask {
|
|
8
|
+
id: number;
|
|
9
|
+
source: string;
|
|
10
|
+
source_id: string;
|
|
11
|
+
title: string;
|
|
12
|
+
body: string;
|
|
13
|
+
status: string;
|
|
14
|
+
priority: string;
|
|
15
|
+
assignee: string | null;
|
|
16
|
+
requester: string | null;
|
|
17
|
+
created_at: string | null;
|
|
18
|
+
updated_at: string | null;
|
|
19
|
+
tags: string[];
|
|
20
|
+
custom_fields: Record<string, unknown>;
|
|
21
|
+
source_url: string | null;
|
|
22
|
+
content_hash: string | null;
|
|
23
|
+
ingested_at: string | null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface InboxResponse {
|
|
27
|
+
tasks: UnifiedTask[];
|
|
28
|
+
total: number;
|
|
29
|
+
page: number;
|
|
30
|
+
per_page: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export async function fetchInbox(source?: string, page = 1): Promise<InboxResponse> {
|
|
34
|
+
const params = new URLSearchParams({ page: String(page) });
|
|
35
|
+
if (source && source !== 'all') params.set('source', source);
|
|
36
|
+
const res = await fetch(`${API_BASE}/api/inbox?${params}`);
|
|
37
|
+
if (!res.ok) throw new Error(`API error: ${res.status}`);
|
|
38
|
+
return res.json();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function fetchSources(): Promise<{ name: string; count: number }[]> {
|
|
42
|
+
const res = await fetch(`${API_BASE}/api/inbox/sources`);
|
|
43
|
+
if (!res.ok) return [];
|
|
44
|
+
const data = await res.json();
|
|
45
|
+
return data.sources ?? [];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
// Plan types
|
|
50
|
+
// ---------------------------------------------------------------------------
|
|
51
|
+
|
|
52
|
+
export interface PlanStep {
|
|
53
|
+
id: number;
|
|
54
|
+
title: string;
|
|
55
|
+
target_system: string;
|
|
56
|
+
method_primary: string;
|
|
57
|
+
method_fallback: string;
|
|
58
|
+
risk_level: string;
|
|
59
|
+
approval_required: boolean;
|
|
60
|
+
rollback: string;
|
|
61
|
+
status: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface Plan {
|
|
65
|
+
id: number;
|
|
66
|
+
task_id: number;
|
|
67
|
+
steps: PlanStep[];
|
|
68
|
+
status: string;
|
|
69
|
+
yaml_content: string;
|
|
70
|
+
created_at: string | null;
|
|
71
|
+
updated_at: string | null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
// Plan API functions
|
|
76
|
+
// ---------------------------------------------------------------------------
|
|
77
|
+
|
|
78
|
+
export async function generatePlan(taskId: number): Promise<Plan> {
|
|
79
|
+
const res = await fetch(`${API_BASE}/api/tasks/${taskId}/generate-plan`, {
|
|
80
|
+
method: 'POST'
|
|
81
|
+
});
|
|
82
|
+
if (!res.ok) {
|
|
83
|
+
const err = await res.json().catch(() => ({ detail: res.statusText }));
|
|
84
|
+
throw new Error(err.detail ?? `API error: ${res.status}`);
|
|
85
|
+
}
|
|
86
|
+
return res.json();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export async function getPlan(taskId: number): Promise<Plan | null> {
|
|
90
|
+
const res = await fetch(`${API_BASE}/api/tasks/${taskId}/plan`);
|
|
91
|
+
if (!res.ok) return null;
|
|
92
|
+
const data = await res.json();
|
|
93
|
+
// Backend returns { plan: null } when no plan exists
|
|
94
|
+
if ('plan' in data && data.plan === null) return null;
|
|
95
|
+
return data as Plan;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export async function approveStep(taskId: number, stepId: number): Promise<Plan> {
|
|
99
|
+
const res = await fetch(`${API_BASE}/api/tasks/${taskId}/plan/steps/${stepId}/approve`, {
|
|
100
|
+
method: 'POST'
|
|
101
|
+
});
|
|
102
|
+
if (!res.ok) {
|
|
103
|
+
const err = await res.json().catch(() => ({ detail: res.statusText }));
|
|
104
|
+
throw new Error(err.detail ?? `API error: ${res.status}`);
|
|
105
|
+
}
|
|
106
|
+
return res.json();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export async function approveAllSteps(taskId: number): Promise<Plan> {
|
|
110
|
+
const res = await fetch(`${API_BASE}/api/tasks/${taskId}/plan/approve-all`, {
|
|
111
|
+
method: 'POST'
|
|
112
|
+
});
|
|
113
|
+
if (!res.ok) {
|
|
114
|
+
const err = await res.json().catch(() => ({ detail: res.statusText }));
|
|
115
|
+
throw new Error(err.detail ?? `API error: ${res.status}`);
|
|
116
|
+
}
|
|
117
|
+
return res.json();
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ---------------------------------------------------------------------------
|
|
121
|
+
// Execution types
|
|
122
|
+
// ---------------------------------------------------------------------------
|
|
123
|
+
|
|
124
|
+
export interface AuditEntry {
|
|
125
|
+
id: number;
|
|
126
|
+
step_id: string;
|
|
127
|
+
action_type: string;
|
|
128
|
+
target_system: string;
|
|
129
|
+
target_object: string;
|
|
130
|
+
mutation_performed: string;
|
|
131
|
+
result: Record<string, unknown>;
|
|
132
|
+
rollback_available: boolean;
|
|
133
|
+
created_at: string;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// ---------------------------------------------------------------------------
|
|
137
|
+
// Execution API functions
|
|
138
|
+
// ---------------------------------------------------------------------------
|
|
139
|
+
|
|
140
|
+
export async function startExecution(taskId: number): Promise<Record<string, unknown>> {
|
|
141
|
+
const res = await fetch(`${API_BASE}/api/tasks/${taskId}/execute`, { method: 'POST' });
|
|
142
|
+
if (!res.ok) {
|
|
143
|
+
const err = await res.json().catch(() => ({ detail: res.statusText }));
|
|
144
|
+
throw new Error(err.detail ?? `API error: ${res.status}`);
|
|
145
|
+
}
|
|
146
|
+
return res.json();
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export async function pauseExecution(taskId: number): Promise<void> {
|
|
150
|
+
await fetch(`${API_BASE}/api/tasks/${taskId}/pause`, { method: 'POST' });
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export async function resumeExecution(taskId: number): Promise<void> {
|
|
154
|
+
await fetch(`${API_BASE}/api/tasks/${taskId}/resume`, { method: 'POST' });
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export async function retryStep(taskId: number, stepId: number): Promise<void> {
|
|
158
|
+
await fetch(`${API_BASE}/api/tasks/${taskId}/steps/${stepId}/retry`, { method: 'POST' });
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export async function getAuditLog(taskId: number): Promise<AuditEntry[]> {
|
|
162
|
+
const res = await fetch(`${API_BASE}/api/tasks/${taskId}/audit-log`);
|
|
163
|
+
if (!res.ok) return [];
|
|
164
|
+
const data = await res.json();
|
|
165
|
+
return data.entries ?? [];
|
|
166
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { AuditEntry } from '$lib/api';
|
|
3
|
+
|
|
4
|
+
export let entries: AuditEntry[] = [];
|
|
5
|
+
|
|
6
|
+
const levelColors: Record<string, string> = {
|
|
7
|
+
info: '#66bb6a',
|
|
8
|
+
warn: '#ffd54f',
|
|
9
|
+
success: '#4caf50',
|
|
10
|
+
error: '#ef5350'
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
function levelFromAction(entry: AuditEntry): string {
|
|
14
|
+
const result = entry.result as Record<string, unknown>;
|
|
15
|
+
if (result?.status === 'failed') return 'error';
|
|
16
|
+
if (result?.status === 'completed') return 'success';
|
|
17
|
+
return 'info';
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function formatTime(dateStr: string): string {
|
|
21
|
+
try {
|
|
22
|
+
return new Date(dateStr).toLocaleTimeString('en-GB', { hour12: false });
|
|
23
|
+
} catch {
|
|
24
|
+
return dateStr;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
</script>
|
|
28
|
+
|
|
29
|
+
<div class="exec-log">
|
|
30
|
+
{#each entries as entry (entry.id)}
|
|
31
|
+
{@const level = levelFromAction(entry)}
|
|
32
|
+
<div class="log-entry" style="border-left-color: {levelColors[level] ?? '#66bb6a'}">
|
|
33
|
+
<span class="log-time">{formatTime(entry.created_at)}</span>
|
|
34
|
+
<span class="log-action">{entry.action_type}</span>
|
|
35
|
+
<span class="log-target">{entry.target_object}</span>
|
|
36
|
+
</div>
|
|
37
|
+
{/each}
|
|
38
|
+
</div>
|
|
39
|
+
|
|
40
|
+
<style>
|
|
41
|
+
.exec-log {
|
|
42
|
+
display: flex;
|
|
43
|
+
flex-direction: column;
|
|
44
|
+
gap: 0.25rem;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.log-entry {
|
|
48
|
+
display: flex;
|
|
49
|
+
flex-direction: column;
|
|
50
|
+
gap: 0.1rem;
|
|
51
|
+
padding: 0.35rem 0.5rem;
|
|
52
|
+
border-radius: 8px;
|
|
53
|
+
font-size: 0.72rem;
|
|
54
|
+
border-left: 3px solid #66bb6a;
|
|
55
|
+
transition: background 0.1s;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.log-entry:hover {
|
|
59
|
+
background: rgba(76, 175, 80, 0.04);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.log-time {
|
|
63
|
+
font-family: 'Menlo', monospace;
|
|
64
|
+
font-size: 0.65rem;
|
|
65
|
+
color: var(--text-muted);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.log-action {
|
|
69
|
+
font-weight: 700;
|
|
70
|
+
color: var(--text-secondary);
|
|
71
|
+
text-transform: uppercase;
|
|
72
|
+
font-size: 0.6rem;
|
|
73
|
+
letter-spacing: 0.05em;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.log-target {
|
|
77
|
+
color: var(--text-primary);
|
|
78
|
+
font-weight: 600;
|
|
79
|
+
line-height: 1.4;
|
|
80
|
+
}
|
|
81
|
+
</style>
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { onMount, onDestroy, createEventDispatcher } from 'svelte';
|
|
3
|
+
import TaskCard from './TaskCard.svelte';
|
|
4
|
+
import PillButton from './ui/PillButton.svelte';
|
|
5
|
+
import EmptyState from './ui/EmptyState.svelte';
|
|
6
|
+
import { fetchInbox, type UnifiedTask } from '$lib/api';
|
|
7
|
+
|
|
8
|
+
export let selectedTaskId: number | null = null;
|
|
9
|
+
|
|
10
|
+
const dispatch = createEventDispatcher<{
|
|
11
|
+
selectTask: UnifiedTask;
|
|
12
|
+
generatePlan: UnifiedTask;
|
|
13
|
+
}>();
|
|
14
|
+
|
|
15
|
+
const sources = ['all', 'jira', 'freshservice', 'slack', 'gmail'] as const;
|
|
16
|
+
let activeSource: string = 'all';
|
|
17
|
+
let tasks: UnifiedTask[] = [];
|
|
18
|
+
let total = 0;
|
|
19
|
+
let loading = true;
|
|
20
|
+
let error = '';
|
|
21
|
+
let interval: ReturnType<typeof setInterval>;
|
|
22
|
+
|
|
23
|
+
async function loadTasks() {
|
|
24
|
+
try {
|
|
25
|
+
const res = await fetchInbox(activeSource === 'all' ? undefined : activeSource);
|
|
26
|
+
tasks = res.tasks;
|
|
27
|
+
total = res.total;
|
|
28
|
+
error = '';
|
|
29
|
+
} catch (e) {
|
|
30
|
+
error = e instanceof Error ? e.message : 'Failed to load';
|
|
31
|
+
} finally {
|
|
32
|
+
loading = false;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function switchSource(source: string) {
|
|
37
|
+
activeSource = source;
|
|
38
|
+
loading = true;
|
|
39
|
+
loadTasks();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
onMount(() => {
|
|
43
|
+
loadTasks();
|
|
44
|
+
interval = setInterval(loadTasks, 15000);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
onDestroy(() => {
|
|
48
|
+
if (interval) clearInterval(interval);
|
|
49
|
+
});
|
|
50
|
+
</script>
|
|
51
|
+
|
|
52
|
+
<div class="inbox-feed">
|
|
53
|
+
<!-- Source tabs -->
|
|
54
|
+
<div class="source-tabs">
|
|
55
|
+
{#each sources as source}
|
|
56
|
+
<PillButton
|
|
57
|
+
variant={activeSource === source ? 'primary' : 'ghost'}
|
|
58
|
+
size="sm"
|
|
59
|
+
on:click={() => switchSource(source)}
|
|
60
|
+
>
|
|
61
|
+
{source === 'all' ? 'All' : source.charAt(0).toUpperCase() + source.slice(1)}
|
|
62
|
+
</PillButton>
|
|
63
|
+
{/each}
|
|
64
|
+
</div>
|
|
65
|
+
|
|
66
|
+
<!-- Task count -->
|
|
67
|
+
{#if !loading && tasks.length > 0}
|
|
68
|
+
<div class="task-count">{total} task{total !== 1 ? 's' : ''}</div>
|
|
69
|
+
{/if}
|
|
70
|
+
|
|
71
|
+
<!-- Task list -->
|
|
72
|
+
<div class="task-list">
|
|
73
|
+
{#if loading}
|
|
74
|
+
<div class="loading-placeholder">
|
|
75
|
+
{#each Array(3) as _}
|
|
76
|
+
<div class="skeleton-card"></div>
|
|
77
|
+
{/each}
|
|
78
|
+
</div>
|
|
79
|
+
{:else if error}
|
|
80
|
+
<EmptyState emoji="⚠️" title="Connection error" message={error} />
|
|
81
|
+
{:else if tasks.length === 0}
|
|
82
|
+
<EmptyState emoji="📭" title="No tasks yet" message="Enjoy the quiet! Tasks will appear when pollers find them." />
|
|
83
|
+
{:else}
|
|
84
|
+
{#each tasks as task (task.id)}
|
|
85
|
+
<TaskCard
|
|
86
|
+
{task}
|
|
87
|
+
selected={selectedTaskId === task.id}
|
|
88
|
+
on:select={(e) => dispatch('selectTask', e.detail)}
|
|
89
|
+
on:generatePlan={(e) => dispatch('generatePlan', e.detail)}
|
|
90
|
+
/>
|
|
91
|
+
{/each}
|
|
92
|
+
{/if}
|
|
93
|
+
</div>
|
|
94
|
+
</div>
|
|
95
|
+
|
|
96
|
+
<style>
|
|
97
|
+
.inbox-feed {
|
|
98
|
+
display: flex;
|
|
99
|
+
flex-direction: column;
|
|
100
|
+
gap: 0.5rem;
|
|
101
|
+
height: 100%;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
.source-tabs {
|
|
105
|
+
display: flex;
|
|
106
|
+
gap: 0.35rem;
|
|
107
|
+
flex-wrap: wrap;
|
|
108
|
+
padding-bottom: 0.25rem;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
.task-count {
|
|
112
|
+
font-size: 0.68rem;
|
|
113
|
+
font-weight: 700;
|
|
114
|
+
color: var(--text-muted);
|
|
115
|
+
letter-spacing: 0.04em;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
.task-list {
|
|
119
|
+
display: flex;
|
|
120
|
+
flex-direction: column;
|
|
121
|
+
gap: 0.5rem;
|
|
122
|
+
flex: 1;
|
|
123
|
+
overflow-y: auto;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
.loading-placeholder {
|
|
127
|
+
display: flex;
|
|
128
|
+
flex-direction: column;
|
|
129
|
+
gap: 0.5rem;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
.skeleton-card {
|
|
133
|
+
height: 80px;
|
|
134
|
+
background: var(--bg-secondary);
|
|
135
|
+
border-radius: 12px;
|
|
136
|
+
animation: pulse 1.5s ease-in-out infinite;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
@keyframes pulse {
|
|
140
|
+
0%, 100% { opacity: 0.4; }
|
|
141
|
+
50% { opacity: 0.7; }
|
|
142
|
+
}
|
|
143
|
+
</style>
|