superacli 1.1.20 → 1.1.21
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/cli/adapter-schema.js +1 -6
- package/cli/config.js +95 -0
- package/cli/executor.js +90 -11
- package/cli/server-command.js +395 -0
- package/cli/supercli.js +82 -1
- package/docs/adapters.md +159 -0
- package/docs/changelog-2026-04.html +15 -0
- package/docs/index.html +314 -98
- package/docs/meta-plugins.json +24424 -1247
- package/docs/plugins-examples.md +8 -8
- package/docs/plugins-how-to.md +0 -13
- package/docs/plugins.html +165 -61
- package/docs/server.md +50 -0
- package/graphify-out/GRAPH_REPORT.md +141 -136
- package/graphify-out/cache/1821936911d6eb3130499b6b45b8e6d760d6c05328131e115ac9825a5e3061cd.json +1 -0
- package/graphify-out/cache/20b0e1261b41286d639e7b039c8143682001981f607e26eb2a492a06f12c233a.json +1 -0
- package/graphify-out/cache/21dc011c08a0813ac33c0520c9cf131d8922e6007df7d9b51df740974fee5fb3.json +1 -0
- package/graphify-out/cache/22d3d43c24fdff310ab8445bd442fbcf4714872c6950f15b90777e5a8358dcd9.json +1 -0
- package/graphify-out/cache/2b8a3fbc23afe7bbc1a5ecb60d4074f39afdf6cb42120922176c280f4cc4ab4b.json +1 -0
- package/graphify-out/cache/37418b8b152d9c436d75361969c39bc74a76260ab7209ceabf455f80452f1654.json +1 -0
- package/graphify-out/cache/3b9aae57e4326f5862f8153c13b7d809a34c422996993e7b491c5ea3a66b5e04.json +1 -0
- package/graphify-out/cache/448f8e0c5e77cfb15fa625ed40ee0dc1622d9814d24905fe4c12c4d175131dc9.json +1 -0
- package/graphify-out/cache/47dd42009ceac28b7a7fec5586e565e7bc850c80be96b8329e1ed7c49efbe34f.json +1 -0
- package/graphify-out/cache/492712949d38efbe312c303fe717e5c93e1ecc8888e1ceb855750afc48caffdb.json +1 -0
- package/graphify-out/cache/494fd6af80b6a1bb7a2a5641c6ad11e2fbe2a67b5a2b3ff57fd04bb49ccad1c4.json +1 -0
- package/graphify-out/cache/4b31b0de656deff083119780df64d17205c45e72233987e1c495f2121c4923fd.json +1 -0
- package/graphify-out/cache/5a16fa8cdd687a39661321adb39186ce8cf924a79ef936e42b365380b5e3543e.json +1 -0
- package/graphify-out/cache/644fcb129e595cfdb4e3a9757ed9658230347f5ed0652be45a894528a2b6585b.json +1 -0
- package/graphify-out/cache/6b8762db2c5f51408cf49d915f3e44bcc25c7ebb32abc635307fdf2a672b370c.json +1 -0
- package/graphify-out/cache/6db88050060880e56dd26ec58f29e3b56a409ace48e31bc75f54cab7dc74e409.json +1 -0
- package/graphify-out/cache/7932d1b6a9751b2cb2f4dc6b4e5f9fd7dcc2cee82d7866ec3d655482010533c0.json +1 -0
- package/graphify-out/cache/8140a76ff5b70dfdd0d020aeffff27f87af39da5b237c35baa938fcc4b288ba5.json +1 -0
- package/graphify-out/cache/8876638878758733a412bd4ca4cbd22863322689fbba9f037d92abfa877ea3ab.json +1 -0
- package/graphify-out/cache/90445359c448f8135207ddcd378c8391b9274460064cf998f1ea2ad6f2173703.json +1 -0
- package/graphify-out/cache/b01f675bb3942d2e8f359d11c38d52f28487544f54cdef518c60b7d8dfd2b383.json +1 -0
- package/graphify-out/cache/bc353b40b348d54c730d8498af675f57998b14ae796b4f952cdc200147008f13.json +1 -0
- package/graphify-out/cache/c38b2e784b1c441121b422db0c79b4911fb525d2a3cfad10d3fd561a07900bd5.json +1 -0
- package/graphify-out/cache/ce9df9bb2d36ecf310a53fb34526168bdf25773edf0826c1f55accebfa3c012d.json +1 -0
- package/graphify-out/cache/ceaeb0f99851d313b29cb1440240db1cf835de425b9b0c1e58fa3035355c2b2b.json +1 -0
- package/graphify-out/cache/d8f3e78931a41cd0f367fdab1d11b634bbffb2c90280c75a80a1a53a6b2d6bc7.json +1 -0
- package/graphify-out/cache/f574e4ce3bcb1250ca88410519fb4fd26c5b6cd786039410880fbd6e606ab23f.json +1 -0
- package/graphify-out/cache/faa65636d5f779b8b1c790dde11cc6470b48125bf42a12a77651827fd1653fd7.json +1 -0
- package/graphify-out/graph.json +4173 -2115
- package/ideal-plugins.csv +124 -0
- package/package.json +3 -2
- package/plugin-scores.csv +999 -0
- package/plugins/beads/plugin.json +2 -2
- package/plugins/biome/install-guidance.json +11 -0
- package/plugins/biome/meta.json +13 -0
- package/plugins/biome/plugin.json +83 -0
- package/plugins/biome/skills/quickstart/SKILL.md +25 -0
- package/plugins/bore/install-guidance.json +11 -0
- package/plugins/bore/meta.json +11 -0
- package/plugins/bore/plugin.json +65 -0
- package/plugins/bore/skills/quickstart/SKILL.md +25 -0
- package/plugins/ccf-deadlines/install-guidance.json +11 -0
- package/plugins/ccf-deadlines/meta.json +11 -0
- package/plugins/ccf-deadlines/plugin.json +28 -0
- package/plugins/ccf-deadlines/skills/quickstart/SKILL.md +25 -0
- package/plugins/code2prompt/install-guidance.json +11 -0
- package/plugins/code2prompt/meta.json +12 -0
- package/plugins/code2prompt/plugin.json +46 -0
- package/plugins/code2prompt/skills/quickstart/SKILL.md +25 -0
- package/plugins/commiat/install-guidance.json +10 -0
- package/plugins/commiat/plugin.json +2 -2
- package/plugins/dua-cli/install-guidance.json +11 -0
- package/plugins/dua-cli/meta.json +11 -0
- package/plugins/dua-cli/plugin.json +57 -0
- package/plugins/dua-cli/skills/quickstart/SKILL.md +25 -0
- package/plugins/forever/install-guidance.json +11 -0
- package/plugins/forever/meta.json +11 -0
- package/plugins/forever/plugin.json +75 -0
- package/plugins/forever/skills/quickstart/SKILL.md +25 -0
- package/plugins/gitmoji-cli/install-guidance.json +11 -0
- package/plugins/gitmoji-cli/meta.json +11 -0
- package/plugins/gitmoji-cli/plugin.json +49 -0
- package/plugins/gitmoji-cli/skills/quickstart/SKILL.md +25 -0
- package/plugins/google-maps-scraper/plugin.json +1 -1
- package/plugins/gwc/install-guidance.json +10 -0
- package/plugins/gwc/plugin.json +2 -2
- package/plugins/quarto/install-guidance.json +11 -0
- package/plugins/quarto/meta.json +11 -0
- package/plugins/quarto/plugin.json +61 -0
- package/plugins/quarto/skills/quickstart/SKILL.md +25 -0
- package/plugins/readme-md-generator/install-guidance.json +11 -0
- package/plugins/readme-md-generator/meta.json +11 -0
- package/plugins/readme-md-generator/plugin.json +28 -0
- package/plugins/readme-md-generator/skills/quickstart/SKILL.md +25 -0
- package/plugins/twarc/install-guidance.json +11 -0
- package/plugins/twarc/meta.json +13 -0
- package/plugins/twarc/plugin.json +165 -0
- package/plugins/twarc/skills/quickstart/SKILL.md +61 -0
- package/plugins/vale/install-guidance.json +11 -0
- package/plugins/vale/meta.json +12 -0
- package/plugins/vale/plugin.json +50 -0
- package/plugins/vale/skills/quickstart/SKILL.md +25 -0
- package/plugins/volta/install-guidance.json +11 -0
- package/plugins/volta/meta.json +12 -0
- package/plugins/volta/plugin.json +75 -0
- package/plugins/volta/skills/quickstart/SKILL.md +25 -0
- package/plugins/wgcf/install-guidance.json +11 -0
- package/plugins/wgcf/meta.json +12 -0
- package/plugins/wgcf/plugin.json +45 -0
- package/plugins/wgcf/skills/quickstart/SKILL.md +25 -0
- package/scripts/analyze-plugins.js +130 -0
- package/scripts/enrich-meta-plugins.js +67 -0
- package/server/app.js +23 -1
- package/server/routes/adapters.js +356 -0
- package/server/routes/dashboard.js +113 -0
- package/server/routes/jobs.js +26 -0
- package/server/services/adaptersService.js +284 -0
- package/server/services/pluginsService.js +4 -0
- package/server/views/adapter-edit.ejs +226 -0
- package/server/views/adapter-packages.ejs +191 -0
- package/server/views/adapters.ejs +112 -0
- package/server/views/command-edit.ejs +48 -21
- package/server/views/commands.ejs +25 -22
- package/server/views/dashboard.ejs +196 -0
- package/server/views/layout.ejs +94 -14
- package/server/views/mcp.ejs +38 -35
- package/server/views/partials/head.ejs +88 -12
- package/server/views/plugins.ejs +9 -0
- package/server/views/specs.ejs +33 -30
- package/cli/adapters/builtin.js +0 -43
- package/index.html +0 -384
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
<%- include("partials/head", { title: `${adapter.name} - Packages` }) %>
|
|
2
|
+
|
|
3
|
+
<div class="max-w-2xl">
|
|
4
|
+
<div class="flex justify-between items-center mb-6">
|
|
5
|
+
<div>
|
|
6
|
+
<p class="text-xs uppercase tracking-[0.2em] text-[#787774] mb-1">Adapter Packages</p>
|
|
7
|
+
<h1 class="text-2xl editorial-heading text-[#111111]"><%= adapter.name %></h1>
|
|
8
|
+
</div>
|
|
9
|
+
<div class="flex gap-2">
|
|
10
|
+
<a href="/api/adapters/<%= adapter.name %>/edit" class="btn">Edit Adapter</a>
|
|
11
|
+
<a href="/api/adapters" class="btn">Back</a>
|
|
12
|
+
</div>
|
|
13
|
+
</div>
|
|
14
|
+
|
|
15
|
+
<div id="packages-manager">
|
|
16
|
+
<div v-if="message.text" :class="['alert mb-4', message.type === 'error' ? 'alert-error' : 'alert-success']">
|
|
17
|
+
<span>{{ message.text }}</span>
|
|
18
|
+
</div>
|
|
19
|
+
|
|
20
|
+
<div class="bento-card p-6 mb-6">
|
|
21
|
+
<h3 class="text-sm font-medium text-[#111111] mb-4">Add Package</h3>
|
|
22
|
+
<div class="flex gap-2">
|
|
23
|
+
<input v-model="newPackage" class="input input-bordered input-sm flex-1"
|
|
24
|
+
placeholder="package-name@version (e.g., pg@8.11.0)"
|
|
25
|
+
@keyup.enter="addPackage"
|
|
26
|
+
:disabled="adding">
|
|
27
|
+
<button @click="addPackage" class="btn btn-primary btn-sm" :disabled="!newPackage.trim() || adding">
|
|
28
|
+
<span v-if="adding" class="loading loading-spinner loading-sm"></span>
|
|
29
|
+
<span v-else>Add</span>
|
|
30
|
+
</button>
|
|
31
|
+
</div>
|
|
32
|
+
<p class="text-xs text-[#787774] mt-2">
|
|
33
|
+
Enter package name with optional version. Examples: <code>pg</code>, <code>pg@8.11.0</code>, <code>redis@latest</code>
|
|
34
|
+
</p>
|
|
35
|
+
</div>
|
|
36
|
+
|
|
37
|
+
<div class="bento-card overflow-hidden">
|
|
38
|
+
<div class="px-6 py-4 border-b border-[#EAEAEA] bg-[#F7F6F3] flex justify-between items-center">
|
|
39
|
+
<h3 class="text-sm font-medium text-[#111111]">Installed Packages ({{ packages.length }})</h3>
|
|
40
|
+
<button v-if="packages.length > 0" @click="prunePackages" class="btn btn-xs border-red-200 text-red-600 hover:bg-red-50" :disabled="pruning">
|
|
41
|
+
<span v-if="pruning" class="loading loading-spinner loading-sm"></span>
|
|
42
|
+
<span v-else>Prune All</span>
|
|
43
|
+
</button>
|
|
44
|
+
</div>
|
|
45
|
+
|
|
46
|
+
<table class="w-full">
|
|
47
|
+
<thead>
|
|
48
|
+
<tr class="border-b border-[#EAEAEA] bg-[#F7F6F3]">
|
|
49
|
+
<th class="px-6 py-3 text-left text-xs font-semibold text-[#787774] uppercase tracking-wider">Package</th>
|
|
50
|
+
<th class="px-6 py-3 text-left text-xs font-semibold text-[#787774] uppercase tracking-wider">Version</th>
|
|
51
|
+
<th class="px-6 py-3 text-left text-xs font-semibold text-[#787774] uppercase tracking-wider">Actions</th>
|
|
52
|
+
</tr>
|
|
53
|
+
</thead>
|
|
54
|
+
<tbody>
|
|
55
|
+
<tr v-if="packages.length === 0">
|
|
56
|
+
<td colspan="3" class="px-6 py-12 text-center text-[#787774]">
|
|
57
|
+
No packages installed. Add packages above.
|
|
58
|
+
</td>
|
|
59
|
+
</tr>
|
|
60
|
+
<tr v-for="pkg in packages" :key="pkg" class="border-b border-[#EAEAEA] hover:bg-[#F7F6F3] transition-colors">
|
|
61
|
+
<td class="px-6 py-4 font-medium text-[#111111]">{{ parsePackageName(pkg).name }}</td>
|
|
62
|
+
<td class="px-6 py-4 text-sm text-[#2F3437]">{{ parsePackageName(pkg).version }}</td>
|
|
63
|
+
<td class="px-6 py-4">
|
|
64
|
+
<button @click="removePackage(pkg)" class="btn text-xs border-red-200 text-red-600 hover:bg-red-50 hover:border-red-300" :disabled="removing === pkg">
|
|
65
|
+
<span v-if="removing === pkg" class="loading loading-spinner loading-sm"></span>
|
|
66
|
+
<span v-else>Remove</span>
|
|
67
|
+
</button>
|
|
68
|
+
</td>
|
|
69
|
+
</tr>
|
|
70
|
+
</tbody>
|
|
71
|
+
</table>
|
|
72
|
+
</div>
|
|
73
|
+
|
|
74
|
+
<div class="mt-6 p-4 bg-blue-50 border border-blue-200 rounded">
|
|
75
|
+
<h4 class="text-sm font-medium text-blue-800 mb-2">Auto-Install</h4>
|
|
76
|
+
<p class="text-xs text-blue-700">
|
|
77
|
+
Packages are automatically installed when the adapter runs.
|
|
78
|
+
For CLI-context adapters, packages are installed on the user's machine when they sync.
|
|
79
|
+
</p>
|
|
80
|
+
</div>
|
|
81
|
+
</div>
|
|
82
|
+
</div>
|
|
83
|
+
|
|
84
|
+
<script>
|
|
85
|
+
const adapterName = '<%= adapter.name %>'
|
|
86
|
+
const initialPackages = <%- JSON.stringify(packages) %>
|
|
87
|
+
|
|
88
|
+
Vue.createApp({
|
|
89
|
+
data() {
|
|
90
|
+
return {
|
|
91
|
+
packages: initialPackages,
|
|
92
|
+
newPackage: '',
|
|
93
|
+
message: { text: '', type: '' },
|
|
94
|
+
adding: false,
|
|
95
|
+
removing: null,
|
|
96
|
+
pruning: false
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
methods: {
|
|
100
|
+
parsePackageName(pkg) {
|
|
101
|
+
if (pkg.includes('@') && !pkg.startsWith('@')) {
|
|
102
|
+
const parts = pkg.split('@')
|
|
103
|
+
return { name: parts[0], version: parts[1] }
|
|
104
|
+
}
|
|
105
|
+
return { name: pkg, version: 'latest' }
|
|
106
|
+
},
|
|
107
|
+
|
|
108
|
+
async addPackage() {
|
|
109
|
+
const pkg = this.newPackage.trim()
|
|
110
|
+
if (!pkg) return
|
|
111
|
+
|
|
112
|
+
this.message.text = ''
|
|
113
|
+
this.adding = true
|
|
114
|
+
|
|
115
|
+
try {
|
|
116
|
+
const res = await fetch(`/api/adapters/${adapterName}/packages`, {
|
|
117
|
+
method: 'POST',
|
|
118
|
+
headers: { 'Content-Type': 'application/json' },
|
|
119
|
+
body: JSON.stringify({ package: pkg })
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
const data = await res.json()
|
|
123
|
+
|
|
124
|
+
if (!res.ok) {
|
|
125
|
+
throw new Error(data.error || 'Failed to add package')
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
this.packages = data.packages
|
|
129
|
+
this.newPackage = ''
|
|
130
|
+
this.message = { text: `Package '${pkg}' added`, type: 'success' }
|
|
131
|
+
} catch (err) {
|
|
132
|
+
this.message = { text: err.message, type: 'error' }
|
|
133
|
+
} finally {
|
|
134
|
+
this.adding = false
|
|
135
|
+
}
|
|
136
|
+
},
|
|
137
|
+
|
|
138
|
+
async removePackage(pkg) {
|
|
139
|
+
if (!confirm(`Remove package '${this.parsePackageName(pkg).name}'?`)) return
|
|
140
|
+
|
|
141
|
+
this.message.text = ''
|
|
142
|
+
this.removing = pkg
|
|
143
|
+
|
|
144
|
+
try {
|
|
145
|
+
const name = this.parsePackageName(pkg).name
|
|
146
|
+
const res = await fetch(`/api/adapters/${adapterName}/packages/${name}`, {
|
|
147
|
+
method: 'DELETE'
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
const data = await res.json()
|
|
151
|
+
|
|
152
|
+
if (!res.ok) {
|
|
153
|
+
throw new Error(data.error || 'Failed to remove package')
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
this.packages = data.packages
|
|
157
|
+
this.message = { text: `Package '${name}' removed`, type: 'success' }
|
|
158
|
+
} catch (err) {
|
|
159
|
+
this.message = { text: err.message, type: 'error' }
|
|
160
|
+
} finally {
|
|
161
|
+
this.removing = null
|
|
162
|
+
}
|
|
163
|
+
},
|
|
164
|
+
|
|
165
|
+
async prunePackages() {
|
|
166
|
+
if (!confirm('Remove all packages and uninstall them from disk?')) return
|
|
167
|
+
|
|
168
|
+
this.message.text = ''
|
|
169
|
+
this.pruning = true
|
|
170
|
+
|
|
171
|
+
try {
|
|
172
|
+
const res = await fetch(`/api/adapters/${adapterName}/packages`, {
|
|
173
|
+
method: 'POST',
|
|
174
|
+
headers: { 'Content-Type': 'application/json' },
|
|
175
|
+
body: JSON.stringify({ packages: [] })
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
const data = await res.json()
|
|
179
|
+
this.packages = []
|
|
180
|
+
this.message = { text: 'All packages removed', type: 'success' }
|
|
181
|
+
} catch (err) {
|
|
182
|
+
this.message = { text: err.message, type: 'error' }
|
|
183
|
+
} finally {
|
|
184
|
+
this.pruning = false
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}).mount('#packages-manager')
|
|
189
|
+
</script>
|
|
190
|
+
|
|
191
|
+
<%- include("partials/foot") %>
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
<%- include("partials/head", { title: "Adapters" }) %>
|
|
2
|
+
|
|
3
|
+
<div class="flex justify-between items-center mb-8">
|
|
4
|
+
<div>
|
|
5
|
+
<p class="text-xs uppercase tracking-[0.2em] text-[#787774] mb-1">Management</p>
|
|
6
|
+
<h1 class="text-2xl editorial-heading text-[#111111]">Custom Adapters</h1>
|
|
7
|
+
<p class="text-sm text-[#787774] mt-1">Create and manage JavaScript adapters for custom integrations</p>
|
|
8
|
+
</div>
|
|
9
|
+
<a href="/api/adapters/new" class="btn btn-primary">+ New Adapter</a>
|
|
10
|
+
</div>
|
|
11
|
+
|
|
12
|
+
<div class="bento-card overflow-hidden">
|
|
13
|
+
<table class="w-full">
|
|
14
|
+
<thead>
|
|
15
|
+
<tr class="border-b border-[#EAEAEA] bg-[#F7F6F3]">
|
|
16
|
+
<th class="px-6 py-3 text-left text-xs font-semibold text-[#787774] uppercase tracking-wider">Name</th>
|
|
17
|
+
<th class="px-6 py-3 text-left text-xs font-semibold text-[#787774] uppercase tracking-wider">Context</th>
|
|
18
|
+
<th class="px-6 py-3 text-left text-xs font-semibold text-[#787774] uppercase tracking-wider">Packages</th>
|
|
19
|
+
<th class="px-6 py-3 text-left text-xs font-semibold text-[#787774] uppercase tracking-wider">Updated</th>
|
|
20
|
+
<th class="px-6 py-3 text-left text-xs font-semibold text-[#787774] uppercase tracking-wider">Actions</th>
|
|
21
|
+
</tr>
|
|
22
|
+
</thead>
|
|
23
|
+
<tbody>
|
|
24
|
+
<% if (adapters.length === 0) { %>
|
|
25
|
+
<tr><td colspan="5" class="px-6 py-12 text-center text-[#787774]">No adapters yet. Create one to get started.</td></tr>
|
|
26
|
+
<% } %>
|
|
27
|
+
<% adapters.forEach(a => { %>
|
|
28
|
+
<tr class="border-b border-[#EAEAEA] hover:bg-[#F7F6F3] transition-colors">
|
|
29
|
+
<td class="px-6 py-4">
|
|
30
|
+
<div class="flex flex-col">
|
|
31
|
+
<span class="font-medium text-[#111111]"><%= a.name %></span>
|
|
32
|
+
<% if (a.description) { %>
|
|
33
|
+
<span class="text-xs text-[#787774] mt-1"><%= a.description %></span>
|
|
34
|
+
<% } %>
|
|
35
|
+
</div>
|
|
36
|
+
</td>
|
|
37
|
+
<td class="px-6 py-4">
|
|
38
|
+
<span class="badge <%= a.execution_context === 'cli' ? 'badge-warning' : 'badge-success' %>">
|
|
39
|
+
<%= a.execution_context %>
|
|
40
|
+
</span>
|
|
41
|
+
</td>
|
|
42
|
+
<td class="px-6 py-4">
|
|
43
|
+
<span class="mono-text text-sm text-[#2F3437]"><%= a.dependencies.length %> deps</span>
|
|
44
|
+
</td>
|
|
45
|
+
<td class="px-6 py-4 text-sm text-[#787774]">
|
|
46
|
+
<%= new Date(a.updated_at).toLocaleDateString() %>
|
|
47
|
+
</td>
|
|
48
|
+
<td class="px-6 py-4 flex gap-2">
|
|
49
|
+
<a href="/api/adapters/<%= a.name %>/edit" class="btn text-xs">Edit</a>
|
|
50
|
+
<a href="/api/adapters/<%= a.name %>/packages" class="btn text-xs">Packages</a>
|
|
51
|
+
<% if (a.execution_context === 'server') { %>
|
|
52
|
+
<button onclick="testAdapter('<%= a.name %>')" class="btn text-xs">Test</button>
|
|
53
|
+
<% } %>
|
|
54
|
+
<button onclick="deleteAdapter('<%= a.name %>')" class="btn text-xs border-red-200 text-red-600 hover:bg-red-50 hover:border-red-300">Delete</button>
|
|
55
|
+
</td>
|
|
56
|
+
</tr>
|
|
57
|
+
<% }) %>
|
|
58
|
+
</tbody>
|
|
59
|
+
</table>
|
|
60
|
+
</div>
|
|
61
|
+
|
|
62
|
+
<div id="test-modal" class="hidden fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center">
|
|
63
|
+
<div class="bg-white rounded-lg p-6 max-w-lg w-full mx-4 shadow-xl">
|
|
64
|
+
<h3 class="text-lg font-medium text-[#111111] mb-4">Test Adapter</h3>
|
|
65
|
+
<div id="test-result" class="mb-4"></div>
|
|
66
|
+
<button onclick="closeTestModal()" class="btn btn-primary">Close</button>
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
|
|
70
|
+
<script>
|
|
71
|
+
async function deleteAdapter(name) {
|
|
72
|
+
if (!confirm(`Delete adapter '${name}'?`)) return
|
|
73
|
+
await fetch('/api/adapters/' + name, { method: 'DELETE' })
|
|
74
|
+
location.reload()
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function testAdapter(name) {
|
|
78
|
+
const modal = document.getElementById('test-modal')
|
|
79
|
+
const result = document.getElementById('test-result')
|
|
80
|
+
modal.classList.remove('hidden')
|
|
81
|
+
result.innerHTML = '<p class="text-[#787774]">Running test...</p>'
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
const res = await fetch(`/api/adapters/${name}/test`, { method: 'POST' })
|
|
85
|
+
const data = await res.json()
|
|
86
|
+
|
|
87
|
+
if (data.ok) {
|
|
88
|
+
result.innerHTML = `
|
|
89
|
+
<div class="space-y-2">
|
|
90
|
+
<p class="text-green-600 font-medium">✓ Test passed (${data.duration_ms}ms)</p>
|
|
91
|
+
<pre class="bg-[#F7F6F3] p-3 rounded text-xs overflow-auto">${JSON.stringify(data.result, null, 2)}</pre>
|
|
92
|
+
</div>
|
|
93
|
+
`
|
|
94
|
+
} else {
|
|
95
|
+
result.innerHTML = `
|
|
96
|
+
<div class="space-y-2">
|
|
97
|
+
<p class="text-red-600 font-medium">✗ Test failed (${data.duration_ms}ms)</p>
|
|
98
|
+
<p class="text-sm text-red-500">${data.error}</p>
|
|
99
|
+
</div>
|
|
100
|
+
`
|
|
101
|
+
}
|
|
102
|
+
} catch (err) {
|
|
103
|
+
result.innerHTML = `<p class="text-red-600">Error: ${err.message}</p>`
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function closeTestModal() {
|
|
108
|
+
document.getElementById('test-modal').classList.add('hidden')
|
|
109
|
+
}
|
|
110
|
+
</script>
|
|
111
|
+
|
|
112
|
+
<%- include("partials/foot") %>
|
|
@@ -37,14 +37,22 @@
|
|
|
37
37
|
<div class="form-control">
|
|
38
38
|
<label class="label"><span class="label-text">Adapter</span></label>
|
|
39
39
|
<select v-model="form.adapter" @change="onAdapterChange" class="select select-bordered select-sm">
|
|
40
|
-
<
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
40
|
+
<optgroup label="Built-in">
|
|
41
|
+
<option value="http">http</option>
|
|
42
|
+
<option value="openapi">openapi</option>
|
|
43
|
+
<option value="mcp">mcp</option>
|
|
44
|
+
<option value="shell">shell</option>
|
|
45
|
+
<option value="process">process</option>
|
|
46
|
+
</optgroup>
|
|
47
|
+
<optgroup v-if="customAdapters.length > 0" label="Custom">
|
|
48
|
+
<option v-for="adapter in customAdapters" :key="adapter.name" :value="adapter.name">
|
|
49
|
+
{{ adapter.name }} ({{ adapter.execution_context }})
|
|
50
|
+
</option>
|
|
51
|
+
</optgroup>
|
|
47
52
|
</select>
|
|
53
|
+
<span v-if="customAdapters.length === 0" class="text-xs text-[#787774] mt-1">
|
|
54
|
+
<a href="/api/adapters" class="text-blue-600 hover:underline">Create custom adapters</a> to see them here
|
|
55
|
+
</span>
|
|
48
56
|
</div>
|
|
49
57
|
|
|
50
58
|
<div class="rounded border border-base-300 p-3 bg-base-100">
|
|
@@ -148,16 +156,9 @@
|
|
|
148
156
|
timeout_ms: 5000
|
|
149
157
|
}
|
|
150
158
|
},
|
|
151
|
-
builtin: {
|
|
152
|
-
required: "adapterConfig.builtin",
|
|
153
|
-
help: "Runs internal built-in behavior.",
|
|
154
|
-
template: {
|
|
155
|
-
builtin: "noop"
|
|
156
|
-
}
|
|
157
|
-
},
|
|
158
159
|
custom: {
|
|
159
160
|
required: "Depends on your custom adapter",
|
|
160
|
-
help: "
|
|
161
|
+
help: "Custom adapters are loaded dynamically from the server. Create them in the Adapters section.",
|
|
161
162
|
template: {}
|
|
162
163
|
}
|
|
163
164
|
};
|
|
@@ -177,19 +178,45 @@
|
|
|
177
178
|
adapterConfigStr: editData && editData.adapterConfig ? JSON.stringify(editData.adapterConfig, null, 2) : '{}',
|
|
178
179
|
message: { type: '', text: '' },
|
|
179
180
|
validationResult: null,
|
|
181
|
+
customAdapters: [],
|
|
180
182
|
}
|
|
181
183
|
},
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
return ADAPTER_GUIDES[this.form.adapter] || ADAPTER_GUIDES.http
|
|
185
|
-
}
|
|
186
|
-
},
|
|
187
|
-
mounted() {
|
|
184
|
+
async mounted() {
|
|
185
|
+
await this.loadCustomAdapters()
|
|
188
186
|
if (!this.editId && this.adapterConfigStr.trim() === '{}') {
|
|
189
187
|
this.loadTemplate(false)
|
|
190
188
|
}
|
|
191
189
|
},
|
|
190
|
+
computed: {
|
|
191
|
+
adapterGuide() {
|
|
192
|
+
// Check if it's a built-in adapter
|
|
193
|
+
if (ADAPTER_GUIDES[this.form.adapter]) {
|
|
194
|
+
return ADAPTER_GUIDES[this.form.adapter]
|
|
195
|
+
}
|
|
196
|
+
// Check if it's a custom adapter
|
|
197
|
+
const customAdapter = this.customAdapters.find(a => a.name === this.form.adapter)
|
|
198
|
+
if (customAdapter) {
|
|
199
|
+
return {
|
|
200
|
+
required: "adapterConfig (custom per adapter)",
|
|
201
|
+
help: `Custom adapter: ${customAdapter.description || 'No description'} (${customAdapter.execution_context} context)`,
|
|
202
|
+
template: {}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return ADAPTER_GUIDES.http
|
|
206
|
+
}
|
|
207
|
+
},
|
|
192
208
|
methods: {
|
|
209
|
+
async loadCustomAdapters() {
|
|
210
|
+
try {
|
|
211
|
+
const res = await fetch('/api/adapters', { headers: { 'Accept': 'application/json' } })
|
|
212
|
+
if (res.ok) {
|
|
213
|
+
const data = await res.json()
|
|
214
|
+
this.customAdapters = data.adapters || []
|
|
215
|
+
}
|
|
216
|
+
} catch (err) {
|
|
217
|
+
console.error('Failed to load custom adapters:', err)
|
|
218
|
+
}
|
|
219
|
+
},
|
|
193
220
|
setMessage(type, text) {
|
|
194
221
|
this.message = { type, text }
|
|
195
222
|
},
|
|
@@ -1,36 +1,39 @@
|
|
|
1
1
|
<%- include("partials/head", { title: "Commands" }) %>
|
|
2
2
|
|
|
3
|
-
<div class="flex justify-between items-center mb-
|
|
4
|
-
<
|
|
5
|
-
|
|
3
|
+
<div class="flex justify-between items-center mb-8">
|
|
4
|
+
<div>
|
|
5
|
+
<p class="text-xs uppercase tracking-[0.2em] text-[#787774] mb-1">Management</p>
|
|
6
|
+
<h1 class="text-2xl editorial-heading text-[#111111]">Commands</h1>
|
|
7
|
+
</div>
|
|
8
|
+
<a href="/api/commands/new" class="btn btn-primary">+ Create Command</a>
|
|
6
9
|
</div>
|
|
7
10
|
|
|
8
|
-
<div class="overflow-
|
|
9
|
-
<table class="
|
|
11
|
+
<div class="bento-card overflow-hidden">
|
|
12
|
+
<table class="w-full">
|
|
10
13
|
<thead>
|
|
11
|
-
<tr>
|
|
12
|
-
<th>Namespace</th>
|
|
13
|
-
<th>Resource</th>
|
|
14
|
-
<th>Action</th>
|
|
15
|
-
<th>Adapter</th>
|
|
16
|
-
<th>Description</th>
|
|
17
|
-
<th>Actions</th>
|
|
14
|
+
<tr class="border-b border-[#EAEAEA] bg-[#F7F6F3]">
|
|
15
|
+
<th class="px-6 py-3 text-left text-xs font-semibold text-[#787774] uppercase tracking-wider">Namespace</th>
|
|
16
|
+
<th class="px-6 py-3 text-left text-xs font-semibold text-[#787774] uppercase tracking-wider">Resource</th>
|
|
17
|
+
<th class="px-6 py-3 text-left text-xs font-semibold text-[#787774] uppercase tracking-wider">Action</th>
|
|
18
|
+
<th class="px-6 py-3 text-left text-xs font-semibold text-[#787774] uppercase tracking-wider">Adapter</th>
|
|
19
|
+
<th class="px-6 py-3 text-left text-xs font-semibold text-[#787774] uppercase tracking-wider">Description</th>
|
|
20
|
+
<th class="px-6 py-3 text-left text-xs font-semibold text-[#787774] uppercase tracking-wider">Actions</th>
|
|
18
21
|
</tr>
|
|
19
22
|
</thead>
|
|
20
23
|
<tbody>
|
|
21
24
|
<% if (commands.length === 0) { %>
|
|
22
|
-
<tr><td colspan="6" class="text-center
|
|
25
|
+
<tr><td colspan="6" class="px-6 py-12 text-center text-[#787774]">No commands yet. Create one to get started.</td></tr>
|
|
23
26
|
<% } %>
|
|
24
27
|
<% commands.forEach(c => { %>
|
|
25
|
-
<tr>
|
|
26
|
-
<td><span class="badge badge-outline"><%= c.namespace %></span></td>
|
|
27
|
-
<td><%= c.resource %></td>
|
|
28
|
-
<td><code><%= c.action %></code></td>
|
|
29
|
-
<td
|
|
30
|
-
<td class="max-w-xs truncate
|
|
31
|
-
<td class="flex gap-
|
|
32
|
-
<a href="/api/commands/<%= c._id %>/edit" class="btn
|
|
33
|
-
<button onclick="deleteCommand('<%= c._id %>')" class="btn
|
|
28
|
+
<tr class="border-b border-[#EAEAEA] hover:bg-[#F7F6F3] transition-colors">
|
|
29
|
+
<td class="px-6 py-4"><span class="badge badge-outline"><%= c.namespace %></span></td>
|
|
30
|
+
<td class="px-6 py-4 text-[#111111]"><%= c.resource %></td>
|
|
31
|
+
<td class="px-6 py-4"><code class="mono-text text-sm text-[#1F6C9F]"><%= c.action %></code></td>
|
|
32
|
+
<td class="px-6 py-4"><span class="badge"><%= c.adapter %></span></td>
|
|
33
|
+
<td class="px-6 py-4 max-w-xs truncate text-[#2F3437]"><%= c.description || '-' %></td>
|
|
34
|
+
<td class="px-6 py-4 flex gap-2">
|
|
35
|
+
<a href="/api/commands/<%= c._id %>/edit" class="btn text-xs">Edit</a>
|
|
36
|
+
<button onclick="deleteCommand('<%= c._id %>')" class="btn text-xs border-red-200 text-red-600 hover:bg-red-50 hover:border-red-300">Delete</button>
|
|
34
37
|
</td>
|
|
35
38
|
</tr>
|
|
36
39
|
<% }) %>
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
<%- include("partials/head", { title: "Dashboard" }) %>
|
|
2
|
+
|
|
3
|
+
<div class="mb-8">
|
|
4
|
+
<p class="text-xs uppercase tracking-[0.2em] text-[#787774] mb-1">Overview</p>
|
|
5
|
+
<h1 class="text-2xl editorial-heading text-[#111111]">Dashboard</h1>
|
|
6
|
+
</div>
|
|
7
|
+
|
|
8
|
+
<!-- Resource Summary Cards -->
|
|
9
|
+
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-8">
|
|
10
|
+
<a href="/api/commands" class="bento-card p-5 block no-underline">
|
|
11
|
+
<p class="text-xs uppercase tracking-widest text-[#787774] mb-2">Commands</p>
|
|
12
|
+
<p class="text-3xl font-semibold text-[#111111]"><%= counts.commands %></p>
|
|
13
|
+
</a>
|
|
14
|
+
<a href="/api/mcp" class="bento-card p-5 block no-underline">
|
|
15
|
+
<p class="text-xs uppercase tracking-widest text-[#787774] mb-2">MCP Servers</p>
|
|
16
|
+
<p class="text-3xl font-semibold text-[#111111]"><%= counts.mcpServers %></p>
|
|
17
|
+
</a>
|
|
18
|
+
<a href="/api/specs" class="bento-card p-5 block no-underline">
|
|
19
|
+
<p class="text-xs uppercase tracking-widest text-[#787774] mb-2">OpenAPI Specs</p>
|
|
20
|
+
<p class="text-3xl font-semibold text-[#111111]"><%= counts.specs %></p>
|
|
21
|
+
</a>
|
|
22
|
+
<a href="/api/plugins" class="bento-card p-5 block no-underline">
|
|
23
|
+
<p class="text-xs uppercase tracking-widest text-[#787774] mb-2">Plugins</p>
|
|
24
|
+
<p class="text-3xl font-semibold text-[#111111]"><%= counts.plugins %></p>
|
|
25
|
+
<p class="text-xs text-[#787774] mt-1"><%= counts.pluginsEnabled %> enabled</p>
|
|
26
|
+
</a>
|
|
27
|
+
</div>
|
|
28
|
+
|
|
29
|
+
<!-- Job Metrics Row -->
|
|
30
|
+
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-8">
|
|
31
|
+
<div class="bento-card p-5">
|
|
32
|
+
<p class="text-xs uppercase tracking-widest text-[#787774] mb-2">Total Jobs</p>
|
|
33
|
+
<p class="text-3xl font-semibold text-[#111111]"><%= jobs.total %></p>
|
|
34
|
+
</div>
|
|
35
|
+
<div class="bento-card p-5">
|
|
36
|
+
<p class="text-xs uppercase tracking-widest text-[#787774] mb-2">Success Rate</p>
|
|
37
|
+
<p class="text-3xl font-semibold text-[#346538]"><%= jobs.successRate %>%</p>
|
|
38
|
+
</div>
|
|
39
|
+
<div class="bento-card p-5">
|
|
40
|
+
<p class="text-xs uppercase tracking-widest text-[#787774] mb-2">Failure Rate</p>
|
|
41
|
+
<p class="text-3xl font-semibold <%= jobs.failed > 0 ? 'text-red-600' : 'text-[#111111]' %>"><%= jobs.failureRate %>%</p>
|
|
42
|
+
</div>
|
|
43
|
+
<div class="bento-card p-5">
|
|
44
|
+
<p class="text-xs uppercase tracking-widest text-[#787774] mb-2">Avg Duration</p>
|
|
45
|
+
<p class="text-3xl font-semibold text-[#111111]"><%= jobs.avgDuration %><span class="text-base text-[#787774] ml-1">ms</span></p>
|
|
46
|
+
</div>
|
|
47
|
+
</div>
|
|
48
|
+
|
|
49
|
+
<!-- Charts Row -->
|
|
50
|
+
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-8">
|
|
51
|
+
<!-- 7-day Trend Chart -->
|
|
52
|
+
<div class="bento-card p-5 md:col-span-2">
|
|
53
|
+
<p class="text-xs uppercase tracking-widest text-[#787774] mb-4">Job Activity (Last 7 Days)</p>
|
|
54
|
+
<canvas id="trendChart" height="120"></canvas>
|
|
55
|
+
</div>
|
|
56
|
+
<!-- Status Pie Chart -->
|
|
57
|
+
<div class="bento-card p-5">
|
|
58
|
+
<p class="text-xs uppercase tracking-widest text-[#787774] mb-4">Job Status</p>
|
|
59
|
+
<% if (jobs.total > 0) { %>
|
|
60
|
+
<canvas id="statusChart" height="160"></canvas>
|
|
61
|
+
<% } else { %>
|
|
62
|
+
<div class="flex items-center justify-center h-40 text-[#787774] text-sm">No jobs yet</div>
|
|
63
|
+
<% } %>
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
66
|
+
|
|
67
|
+
<!-- Top Commands + Recent Jobs -->
|
|
68
|
+
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
69
|
+
<!-- Top Commands -->
|
|
70
|
+
<div class="bento-card overflow-hidden">
|
|
71
|
+
<div class="px-6 py-4 border-b border-[#EAEAEA] bg-[#F7F6F3]">
|
|
72
|
+
<p class="text-xs uppercase tracking-widest text-[#787774]">Top Commands</p>
|
|
73
|
+
</div>
|
|
74
|
+
<% if (topCommands.length === 0) { %>
|
|
75
|
+
<div class="px-6 py-10 text-center text-[#787774] text-sm">No job data yet</div>
|
|
76
|
+
<% } else { %>
|
|
77
|
+
<div class="divide-y divide-[#EAEAEA]">
|
|
78
|
+
<% topCommands.forEach((cmd, i) => { %>
|
|
79
|
+
<div class="px-6 py-3 flex items-center justify-between">
|
|
80
|
+
<div class="flex items-center gap-3">
|
|
81
|
+
<span class="text-xs text-[#787774] w-4"><%= i + 1 %></span>
|
|
82
|
+
<code class="mono-text text-sm text-[#1F6C9F]"><%= cmd.command %></code>
|
|
83
|
+
</div>
|
|
84
|
+
<div class="flex items-center gap-4 text-xs text-[#787774]">
|
|
85
|
+
<span><%= cmd.count %> runs</span>
|
|
86
|
+
<span><%= cmd.avg_ms %>ms avg</span>
|
|
87
|
+
</div>
|
|
88
|
+
</div>
|
|
89
|
+
<% }) %>
|
|
90
|
+
</div>
|
|
91
|
+
<% } %>
|
|
92
|
+
</div>
|
|
93
|
+
|
|
94
|
+
<!-- Recent Jobs -->
|
|
95
|
+
<div class="bento-card overflow-hidden">
|
|
96
|
+
<div class="px-6 py-4 border-b border-[#EAEAEA] bg-[#F7F6F3] flex items-center justify-between">
|
|
97
|
+
<p class="text-xs uppercase tracking-widest text-[#787774]">Recent Jobs</p>
|
|
98
|
+
<a href="/api/jobs" class="text-xs text-[#787774] hover:text-[#111111] transition-colors">View all →</a>
|
|
99
|
+
</div>
|
|
100
|
+
<% if (recentJobs.length === 0) { %>
|
|
101
|
+
<div class="px-6 py-10 text-center text-[#787774] text-sm">No jobs yet</div>
|
|
102
|
+
<% } else { %>
|
|
103
|
+
<div class="divide-y divide-[#EAEAEA]">
|
|
104
|
+
<% recentJobs.forEach(job => { %>
|
|
105
|
+
<div class="px-6 py-3 flex items-center justify-between">
|
|
106
|
+
<div class="flex items-center gap-3">
|
|
107
|
+
<span class="w-2 h-2 rounded-full flex-shrink-0
|
|
108
|
+
<%= job.status === 'success' ? 'bg-[#346538]' : job.status === 'failed' ? 'bg-red-500' : 'bg-[#787774]' %>">
|
|
109
|
+
</span>
|
|
110
|
+
<code class="mono-text text-xs text-[#1F6C9F] truncate max-w-[160px]"><%= job.command || '—' %></code>
|
|
111
|
+
</div>
|
|
112
|
+
<div class="flex items-center gap-3 text-xs text-[#787774] flex-shrink-0">
|
|
113
|
+
<% if (job.duration_ms) { %><span><%= job.duration_ms %>ms</span><% } %>
|
|
114
|
+
<span><%= new Date(job.timestamp).toLocaleDateString('en-US', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' }) %></span>
|
|
115
|
+
</div>
|
|
116
|
+
</div>
|
|
117
|
+
<% }) %>
|
|
118
|
+
</div>
|
|
119
|
+
<% } %>
|
|
120
|
+
</div>
|
|
121
|
+
</div>
|
|
122
|
+
|
|
123
|
+
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.2/dist/chart.umd.min.js"></script>
|
|
124
|
+
<script>
|
|
125
|
+
const trend = <%- JSON.stringify(trend) %>;
|
|
126
|
+
const jobs = <%- JSON.stringify(jobs) %>;
|
|
127
|
+
|
|
128
|
+
Chart.defaults.font.family = "'Geist', system-ui, sans-serif";
|
|
129
|
+
Chart.defaults.color = '#787774';
|
|
130
|
+
|
|
131
|
+
// Trend line chart
|
|
132
|
+
const trendCtx = document.getElementById('trendChart');
|
|
133
|
+
if (trendCtx) {
|
|
134
|
+
new Chart(trendCtx, {
|
|
135
|
+
type: 'bar',
|
|
136
|
+
data: {
|
|
137
|
+
labels: trend.map(d => d.label),
|
|
138
|
+
datasets: [
|
|
139
|
+
{
|
|
140
|
+
label: 'Success',
|
|
141
|
+
data: trend.map(d => d.success),
|
|
142
|
+
backgroundColor: '#EDF3EC',
|
|
143
|
+
borderColor: '#346538',
|
|
144
|
+
borderWidth: 1,
|
|
145
|
+
borderRadius: 4,
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
label: 'Failed',
|
|
149
|
+
data: trend.map(d => d.failed),
|
|
150
|
+
backgroundColor: '#FEE2E2',
|
|
151
|
+
borderColor: '#DC2626',
|
|
152
|
+
borderWidth: 1,
|
|
153
|
+
borderRadius: 4,
|
|
154
|
+
}
|
|
155
|
+
]
|
|
156
|
+
},
|
|
157
|
+
options: {
|
|
158
|
+
responsive: true,
|
|
159
|
+
plugins: {
|
|
160
|
+
legend: { position: 'bottom', labels: { boxWidth: 12, padding: 16, font: { size: 11 } } }
|
|
161
|
+
},
|
|
162
|
+
scales: {
|
|
163
|
+
x: { grid: { display: false }, border: { display: false } },
|
|
164
|
+
y: { grid: { color: '#EAEAEA' }, border: { display: false }, beginAtZero: true, ticks: { stepSize: 1 } }
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Status pie chart
|
|
171
|
+
const statusCtx = document.getElementById('statusChart');
|
|
172
|
+
if (statusCtx && jobs.total > 0) {
|
|
173
|
+
const unknown = jobs.total - jobs.success - jobs.failed;
|
|
174
|
+
new Chart(statusCtx, {
|
|
175
|
+
type: 'doughnut',
|
|
176
|
+
data: {
|
|
177
|
+
labels: ['Success', 'Failed', 'Unknown'],
|
|
178
|
+
datasets: [{
|
|
179
|
+
data: [jobs.success, jobs.failed, unknown],
|
|
180
|
+
backgroundColor: ['#EDF3EC', '#FEE2E2', '#F7F6F3'],
|
|
181
|
+
borderColor: ['#346538', '#DC2626', '#EAEAEA'],
|
|
182
|
+
borderWidth: 1,
|
|
183
|
+
}]
|
|
184
|
+
},
|
|
185
|
+
options: {
|
|
186
|
+
responsive: true,
|
|
187
|
+
cutout: '65%',
|
|
188
|
+
plugins: {
|
|
189
|
+
legend: { position: 'bottom', labels: { boxWidth: 12, padding: 12, font: { size: 11 } } }
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
</script>
|
|
195
|
+
|
|
196
|
+
<%- include("partials/foot") %>
|