@intranefr/superbackend 1.5.0 → 1.5.2
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/.env.example +15 -0
- package/README.md +11 -0
- package/analysis-only.skill +0 -0
- package/index.js +23 -0
- package/package.json +8 -2
- package/src/admin/endpointRegistry.js +120 -0
- package/src/controllers/admin.controller.js +90 -6
- package/src/controllers/adminBlockDefinitions.controller.js +127 -0
- package/src/controllers/adminBlockDefinitionsAi.controller.js +54 -0
- package/src/controllers/adminCache.controller.js +342 -0
- package/src/controllers/adminContextBlockDefinitions.controller.js +141 -0
- package/src/controllers/adminCrons.controller.js +388 -0
- package/src/controllers/adminDbBrowser.controller.js +124 -0
- package/src/controllers/adminEjsVirtual.controller.js +13 -3
- package/src/controllers/adminExperiments.controller.js +200 -0
- package/src/controllers/adminHeadless.controller.js +9 -2
- package/src/controllers/adminHealthChecks.controller.js +570 -0
- package/src/controllers/adminI18n.controller.js +51 -29
- package/src/controllers/adminLlm.controller.js +126 -2
- package/src/controllers/adminPages.controller.js +720 -0
- package/src/controllers/adminPagesContextBlocksAi.controller.js +54 -0
- package/src/controllers/adminProxy.controller.js +113 -0
- package/src/controllers/adminRateLimits.controller.js +138 -0
- package/src/controllers/adminRbac.controller.js +803 -0
- package/src/controllers/adminScripts.controller.js +126 -4
- package/src/controllers/adminSeoConfig.controller.js +71 -48
- package/src/controllers/blogAdmin.controller.js +279 -0
- package/src/controllers/blogAiAdmin.controller.js +224 -0
- package/src/controllers/blogAutomationAdmin.controller.js +141 -0
- package/src/controllers/blogInternal.controller.js +26 -0
- package/src/controllers/blogPublic.controller.js +89 -0
- package/src/controllers/experiments.controller.js +85 -0
- package/src/controllers/fileManager.controller.js +190 -0
- package/src/controllers/fileManagerStoragePolicy.controller.js +23 -0
- package/src/controllers/healthChecksPublic.controller.js +196 -0
- package/src/controllers/internalExperiments.controller.js +17 -0
- package/src/controllers/metrics.controller.js +64 -4
- package/src/controllers/orgAdmin.controller.js +80 -0
- package/src/helpers/mongooseHelper.js +258 -0
- package/src/helpers/scriptBase.js +230 -0
- package/src/helpers/scriptRunner.js +335 -0
- package/src/middleware/rbac.js +62 -0
- package/src/middleware.js +810 -48
- package/src/models/BlockDefinition.js +27 -0
- package/src/models/BlogAutomationLock.js +14 -0
- package/src/models/BlogAutomationRun.js +39 -0
- package/src/models/BlogPost.js +42 -0
- package/src/models/CacheEntry.js +26 -0
- package/src/models/ConsoleEntry.js +32 -0
- package/src/models/ConsoleLog.js +23 -0
- package/src/models/ContextBlockDefinition.js +33 -0
- package/src/models/CronExecution.js +47 -0
- package/src/models/CronJob.js +70 -0
- package/src/models/Experiment.js +75 -0
- package/src/models/ExperimentAssignment.js +23 -0
- package/src/models/ExperimentEvent.js +26 -0
- package/src/models/ExperimentMetricBucket.js +30 -0
- package/src/models/ExternalDbConnection.js +49 -0
- package/src/models/FileEntry.js +22 -0
- package/src/models/GlobalSetting.js +1 -2
- package/src/models/HealthAutoHealAttempt.js +57 -0
- package/src/models/HealthCheck.js +132 -0
- package/src/models/HealthCheckRun.js +51 -0
- package/src/models/HealthIncident.js +49 -0
- package/src/models/Page.js +95 -0
- package/src/models/PageCollection.js +42 -0
- package/src/models/ProxyEntry.js +66 -0
- package/src/models/RateLimitCounter.js +19 -0
- package/src/models/RateLimitMetricBucket.js +20 -0
- package/src/models/RbacGrant.js +25 -0
- package/src/models/RbacGroup.js +16 -0
- package/src/models/RbacGroupMember.js +13 -0
- package/src/models/RbacGroupRole.js +13 -0
- package/src/models/RbacRole.js +25 -0
- package/src/models/RbacUserRole.js +13 -0
- package/src/models/ScriptDefinition.js +1 -0
- package/src/models/Webhook.js +2 -0
- package/src/routes/admin.routes.js +2 -0
- package/src/routes/adminBlog.routes.js +21 -0
- package/src/routes/adminBlogAi.routes.js +16 -0
- package/src/routes/adminBlogAutomation.routes.js +27 -0
- package/src/routes/adminCache.routes.js +20 -0
- package/src/routes/adminConsoleManager.routes.js +302 -0
- package/src/routes/adminCrons.routes.js +25 -0
- package/src/routes/adminDbBrowser.routes.js +65 -0
- package/src/routes/adminEjsVirtual.routes.js +2 -1
- package/src/routes/adminExperiments.routes.js +29 -0
- package/src/routes/adminHeadless.routes.js +2 -1
- package/src/routes/adminHealthChecks.routes.js +28 -0
- package/src/routes/adminI18n.routes.js +4 -3
- package/src/routes/adminLlm.routes.js +4 -2
- package/src/routes/adminPages.routes.js +55 -0
- package/src/routes/adminProxy.routes.js +15 -0
- package/src/routes/adminRateLimits.routes.js +17 -0
- package/src/routes/adminRbac.routes.js +38 -0
- package/src/routes/adminSeoConfig.routes.js +5 -4
- package/src/routes/adminUiComponents.routes.js +2 -1
- package/src/routes/blogInternal.routes.js +14 -0
- package/src/routes/blogPublic.routes.js +9 -0
- package/src/routes/experiments.routes.js +30 -0
- package/src/routes/fileManager.routes.js +62 -0
- package/src/routes/fileManagerStoragePolicy.routes.js +9 -0
- package/src/routes/healthChecksPublic.routes.js +9 -0
- package/src/routes/internalExperiments.routes.js +15 -0
- package/src/routes/log.routes.js +43 -60
- package/src/routes/metrics.routes.js +4 -2
- package/src/routes/orgAdmin.routes.js +1 -0
- package/src/routes/pages.routes.js +123 -0
- package/src/routes/proxy.routes.js +46 -0
- package/src/routes/rbac.routes.js +47 -0
- package/src/routes/webhook.routes.js +2 -1
- package/src/routes/workflows.routes.js +4 -0
- package/src/services/blockDefinitionsAi.service.js +247 -0
- package/src/services/blog.service.js +99 -0
- package/src/services/blogAutomation.service.js +978 -0
- package/src/services/blogCronsBootstrap.service.js +185 -0
- package/src/services/blogPublishing.service.js +58 -0
- package/src/services/cacheLayer.service.js +696 -0
- package/src/services/consoleManager.service.js +738 -0
- package/src/services/consoleOverride.service.js +7 -1
- package/src/services/cronScheduler.service.js +350 -0
- package/src/services/dbBrowser.service.js +536 -0
- package/src/services/ejsVirtual.service.js +102 -32
- package/src/services/experiments.service.js +273 -0
- package/src/services/experimentsAggregation.service.js +308 -0
- package/src/services/experimentsCronsBootstrap.service.js +118 -0
- package/src/services/experimentsRetention.service.js +43 -0
- package/src/services/experimentsWs.service.js +134 -0
- package/src/services/fileManager.service.js +475 -0
- package/src/services/fileManagerStoragePolicy.service.js +285 -0
- package/src/services/globalSettings.service.js +15 -0
- package/src/services/healthChecks.service.js +650 -0
- package/src/services/healthChecksBootstrap.service.js +109 -0
- package/src/services/healthChecksScheduler.service.js +106 -0
- package/src/services/jsonConfigs.service.js +2 -2
- package/src/services/llmDefaults.service.js +190 -0
- package/src/services/migrationAssets/s3.js +2 -2
- package/src/services/pages.service.js +602 -0
- package/src/services/pagesContext.service.js +331 -0
- package/src/services/pagesContextBlocksAi.service.js +349 -0
- package/src/services/proxy.service.js +535 -0
- package/src/services/rateLimiter.service.js +623 -0
- package/src/services/rbac.service.js +212 -0
- package/src/services/scriptsRunner.service.js +215 -15
- package/src/services/uiComponentsAi.service.js +6 -19
- package/src/services/workflow.service.js +23 -8
- package/src/utils/orgRoles.js +14 -0
- package/src/utils/rbac/engine.js +60 -0
- package/src/utils/rbac/rightsRegistry.js +33 -0
- package/views/admin-blog-automation.ejs +877 -0
- package/views/admin-blog-edit.ejs +542 -0
- package/views/admin-blog.ejs +399 -0
- package/views/admin-cache.ejs +681 -0
- package/views/admin-console-manager.ejs +680 -0
- package/views/admin-crons.ejs +645 -0
- package/views/admin-dashboard.ejs +28 -8
- package/views/admin-db-browser.ejs +445 -0
- package/views/admin-ejs-virtual.ejs +16 -10
- package/views/admin-experiments.ejs +91 -0
- package/views/admin-file-manager.ejs +942 -0
- package/views/admin-health-checks.ejs +725 -0
- package/views/admin-i18n.ejs +59 -5
- package/views/admin-llm.ejs +99 -1
- package/views/admin-organizations.ejs +163 -1
- package/views/admin-pages.ejs +2424 -0
- package/views/admin-proxy.ejs +491 -0
- package/views/admin-rate-limiter.ejs +625 -0
- package/views/admin-rbac.ejs +1331 -0
- package/views/admin-scripts.ejs +597 -3
- package/views/admin-seo-config.ejs +61 -7
- package/views/admin-ui-components.ejs +57 -25
- package/views/admin-workflows.ejs +7 -7
- package/views/file-manager.ejs +866 -0
- package/views/pages/blocks/contact.ejs +27 -0
- package/views/pages/blocks/cta.ejs +18 -0
- package/views/pages/blocks/faq.ejs +20 -0
- package/views/pages/blocks/features.ejs +19 -0
- package/views/pages/blocks/hero.ejs +13 -0
- package/views/pages/blocks/html.ejs +5 -0
- package/views/pages/blocks/image.ejs +14 -0
- package/views/pages/blocks/testimonials.ejs +26 -0
- package/views/pages/blocks/text.ejs +10 -0
- package/views/pages/layouts/default.ejs +51 -0
- package/views/pages/layouts/minimal.ejs +42 -0
- package/views/pages/layouts/sidebar.ejs +54 -0
- package/views/pages/partials/footer.ejs +13 -0
- package/views/pages/partials/header.ejs +12 -0
- package/views/pages/partials/sidebar.ejs +8 -0
- package/views/pages/runtime/page.ejs +10 -0
- package/views/pages/templates/article.ejs +20 -0
- package/views/pages/templates/default.ejs +12 -0
- package/views/pages/templates/landing.ejs +14 -0
- package/views/pages/templates/listing.ejs +15 -0
- package/views/partials/admin-image-upload-modal.ejs +221 -0
- package/views/partials/dashboard/nav-items.ejs +12 -0
- package/views/partials/dashboard/palette.ejs +5 -3
- package/views/partials/llm-provider-model-picker.ejs +183 -0
- package/src/routes/llmUi.routes.js +0 -26
|
@@ -0,0 +1,725 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>Admin Health Checks</title>
|
|
7
|
+
<script src="https://cdn.tailwindcss.com"></script>
|
|
8
|
+
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
|
|
9
|
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@tabler/icons-webfont@latest/dist/tabler-icons.min.css">
|
|
10
|
+
</head>
|
|
11
|
+
<body class="bg-gray-50">
|
|
12
|
+
<div id="app" class="max-w-7xl mx-auto px-6 py-6">
|
|
13
|
+
<div class="flex items-center justify-between mb-6">
|
|
14
|
+
<div>
|
|
15
|
+
<h1 class="text-2xl font-semibold text-gray-900">Health Checks</h1>
|
|
16
|
+
<div class="text-sm text-gray-500">Endpoint monitoring, incident tracking, and optional auto-healing</div>
|
|
17
|
+
</div>
|
|
18
|
+
<div class="flex items-center gap-2">
|
|
19
|
+
<button @click="showCreateModal = true" class="px-3 py-2 rounded bg-blue-600 text-white text-sm hover:bg-blue-700">
|
|
20
|
+
<i class="ti ti-plus mr-1"></i> New Check
|
|
21
|
+
</button>
|
|
22
|
+
<button @click="refreshAll" class="px-3 py-2 rounded bg-gray-600 text-white text-sm hover:bg-gray-700">
|
|
23
|
+
<i class="ti ti-refresh mr-1"></i> Refresh
|
|
24
|
+
</button>
|
|
25
|
+
</div>
|
|
26
|
+
</div>
|
|
27
|
+
|
|
28
|
+
<!-- Public status toggle -->
|
|
29
|
+
<div class="bg-white border border-gray-200 rounded-lg p-4 mb-6">
|
|
30
|
+
<div class="flex items-start justify-between gap-4">
|
|
31
|
+
<div>
|
|
32
|
+
<div class="text-sm font-medium text-gray-900">Public status summary endpoint</div>
|
|
33
|
+
<div class="text-xs text-gray-500 mt-1">
|
|
34
|
+
When enabled, exposes <code class="px-1 py-0.5 bg-gray-100 rounded">/api/health-checks/status</code> (no auth).
|
|
35
|
+
</div>
|
|
36
|
+
</div>
|
|
37
|
+
<div class="flex items-center gap-3">
|
|
38
|
+
<span class="text-xs text-gray-500" v-if="publicStatusEnabled">Enabled</span>
|
|
39
|
+
<span class="text-xs text-gray-500" v-else>Disabled</span>
|
|
40
|
+
<button @click="togglePublicStatus" class="relative inline-flex h-6 w-11 items-center rounded-full" :class="publicStatusEnabled ? 'bg-emerald-600' : 'bg-gray-300'">
|
|
41
|
+
<span class="inline-block h-5 w-5 transform rounded-full bg-white transition" :class="publicStatusEnabled ? 'translate-x-5' : 'translate-x-1'"></span>
|
|
42
|
+
</button>
|
|
43
|
+
</div>
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
46
|
+
|
|
47
|
+
<!-- Checks List -->
|
|
48
|
+
<div class="bg-white border border-gray-200 rounded-lg">
|
|
49
|
+
<div class="overflow-x-auto">
|
|
50
|
+
<table class="w-full">
|
|
51
|
+
<thead class="bg-gray-50 border-b border-gray-200">
|
|
52
|
+
<tr>
|
|
53
|
+
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Name</th>
|
|
54
|
+
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Type</th>
|
|
55
|
+
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Target</th>
|
|
56
|
+
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Schedule</th>
|
|
57
|
+
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Last run</th>
|
|
58
|
+
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Status</th>
|
|
59
|
+
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</th>
|
|
60
|
+
</tr>
|
|
61
|
+
</thead>
|
|
62
|
+
<tbody class="divide-y divide-gray-200">
|
|
63
|
+
<tr v-for="hc in healthChecks" :key="hc._id" class="hover:bg-gray-50">
|
|
64
|
+
<td class="px-4 py-3">
|
|
65
|
+
<div>
|
|
66
|
+
<div class="text-sm font-medium text-gray-900">{{ hc.name }}</div>
|
|
67
|
+
<div class="text-xs text-gray-500">{{ hc.description || 'No description' }}</div>
|
|
68
|
+
</div>
|
|
69
|
+
</td>
|
|
70
|
+
<td class="px-4 py-3">
|
|
71
|
+
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium"
|
|
72
|
+
:class="hc.checkType === 'http' ? 'bg-green-100 text-green-800' : hc.checkType === 'script' ? 'bg-blue-100 text-blue-800' : 'bg-gray-100 text-gray-800'">
|
|
73
|
+
<i :class="hc.checkType === 'http' ? 'ti ti-world' : hc.checkType === 'script' ? 'ti ti-terminal-2' : 'ti ti-circle-dashed'" class="mr-1"></i>
|
|
74
|
+
{{ hc.checkType }}
|
|
75
|
+
</span>
|
|
76
|
+
</td>
|
|
77
|
+
<td class="px-4 py-3 text-sm text-gray-900">
|
|
78
|
+
<span v-if="hc.checkType === 'http'" class="font-mono text-xs">{{ hc.httpUrl }}</span>
|
|
79
|
+
<span v-else-if="hc.checkType === 'script'" class="text-xs text-gray-700">Script: {{ resolveScriptName(hc.scriptId) }}</span>
|
|
80
|
+
<span v-else class="text-xs text-gray-500">(internal)</span>
|
|
81
|
+
</td>
|
|
82
|
+
<td class="px-4 py-3">
|
|
83
|
+
<div class="text-sm text-gray-900 font-mono">{{ hc.cronExpression }}</div>
|
|
84
|
+
<div class="text-xs text-gray-500">{{ hc.timezone }}</div>
|
|
85
|
+
</td>
|
|
86
|
+
<td class="px-4 py-3 text-sm text-gray-900">
|
|
87
|
+
<div>{{ formatDate(hc.lastRunAt) }}</div>
|
|
88
|
+
<div class="text-xs text-gray-500" v-if="hc.lastLatencyMs">{{ hc.lastLatencyMs }}ms</div>
|
|
89
|
+
</td>
|
|
90
|
+
<td class="px-4 py-3">
|
|
91
|
+
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium"
|
|
92
|
+
:class="getHealthStatusClass(hc.lastStatus, hc.currentIncidentId)">
|
|
93
|
+
{{ getHealthStatusLabel(hc.lastStatus, hc.currentIncidentId) }}
|
|
94
|
+
</span>
|
|
95
|
+
<div class="text-xs text-gray-500 mt-1" v-if="hc.enabled === false">Disabled</div>
|
|
96
|
+
</td>
|
|
97
|
+
<td class="px-4 py-3">
|
|
98
|
+
<div class="flex items-center gap-1">
|
|
99
|
+
<button @click="triggerCheck(hc._id)" class="p-1 text-emerald-600 hover:bg-emerald-50 rounded" title="Run now">
|
|
100
|
+
<i class="ti ti-player-play"></i>
|
|
101
|
+
</button>
|
|
102
|
+
<button @click="toggleCheck(hc)" :class="hc.enabled ? 'text-orange-600 hover:bg-orange-50' : 'text-green-600 hover:bg-green-50'" class="p-1 rounded" :title="hc.enabled ? 'Disable' : 'Enable'">
|
|
103
|
+
<i :class="hc.enabled ? 'ti ti-player-pause' : 'ti ti-player-track-next'"></i>
|
|
104
|
+
</button>
|
|
105
|
+
<button @click="viewRuns(hc)" class="p-1 text-blue-600 hover:bg-blue-50 rounded" title="View run history">
|
|
106
|
+
<i class="ti ti-history"></i>
|
|
107
|
+
</button>
|
|
108
|
+
<button @click="editCheck(hc)" class="p-1 text-gray-600 hover:bg-gray-50 rounded" title="Edit">
|
|
109
|
+
<i class="ti ti-edit"></i>
|
|
110
|
+
</button>
|
|
111
|
+
<button @click="deleteCheck(hc)" class="p-1 text-red-600 hover:bg-red-50 rounded" title="Delete">
|
|
112
|
+
<i class="ti ti-trash"></i>
|
|
113
|
+
</button>
|
|
114
|
+
</div>
|
|
115
|
+
</td>
|
|
116
|
+
</tr>
|
|
117
|
+
</tbody>
|
|
118
|
+
</table>
|
|
119
|
+
<div v-if="healthChecks.length === 0" class="text-center py-8 text-gray-500">
|
|
120
|
+
No health checks configured. Click "New Check" to create one.
|
|
121
|
+
</div>
|
|
122
|
+
</div>
|
|
123
|
+
</div>
|
|
124
|
+
|
|
125
|
+
<!-- Create/Edit Modal -->
|
|
126
|
+
<div v-if="showCreateModal || editingCheck" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
|
127
|
+
<div class="bg-white rounded-lg w-full max-w-3xl max-h-[90vh] overflow-y-auto">
|
|
128
|
+
<div class="p-6">
|
|
129
|
+
<h2 class="text-lg font-semibold mb-4">{{ editingCheck ? 'Edit Health Check' : 'Create Health Check' }}</h2>
|
|
130
|
+
|
|
131
|
+
<div class="space-y-4">
|
|
132
|
+
<div class="grid grid-cols-2 gap-4">
|
|
133
|
+
<div>
|
|
134
|
+
<label class="block text-sm font-medium text-gray-700 mb-1">Name</label>
|
|
135
|
+
<input v-model="form.name" type="text" class="w-full border rounded px-3 py-2" placeholder="API healthz" />
|
|
136
|
+
</div>
|
|
137
|
+
<div>
|
|
138
|
+
<label class="block text-sm font-medium text-gray-700 mb-1">Description</label>
|
|
139
|
+
<input v-model="form.description" type="text" class="w-full border rounded px-3 py-2" placeholder="(optional)" />
|
|
140
|
+
</div>
|
|
141
|
+
</div>
|
|
142
|
+
|
|
143
|
+
<div class="grid grid-cols-2 gap-4">
|
|
144
|
+
<div>
|
|
145
|
+
<label class="block text-sm font-medium text-gray-700 mb-1">Cron Expression</label>
|
|
146
|
+
<input v-model="form.cronExpression" type="text" class="w-full border rounded px-3 py-2 font-mono text-sm" placeholder="*/1 * * * *" />
|
|
147
|
+
</div>
|
|
148
|
+
<div>
|
|
149
|
+
<label class="block text-sm font-medium text-gray-700 mb-1">Timezone</label>
|
|
150
|
+
<input v-model="form.timezone" type="text" class="w-full border rounded px-3 py-2" placeholder="UTC" />
|
|
151
|
+
</div>
|
|
152
|
+
</div>
|
|
153
|
+
|
|
154
|
+
<div class="grid grid-cols-2 gap-4">
|
|
155
|
+
<div>
|
|
156
|
+
<label class="block text-sm font-medium text-gray-700 mb-1">Check Type</label>
|
|
157
|
+
<select v-model="form.checkType" @change="onCheckTypeChange" class="w-full border rounded px-3 py-2">
|
|
158
|
+
<option value="http">HTTP</option>
|
|
159
|
+
<option value="script">Script</option>
|
|
160
|
+
</select>
|
|
161
|
+
</div>
|
|
162
|
+
<div class="flex items-center pt-6">
|
|
163
|
+
<input v-model="form.enabled" type="checkbox" class="h-4 w-4" />
|
|
164
|
+
<label class="ml-2 text-sm text-gray-700">Enabled</label>
|
|
165
|
+
</div>
|
|
166
|
+
</div>
|
|
167
|
+
|
|
168
|
+
<!-- HTTP -->
|
|
169
|
+
<div v-if="form.checkType === 'http'" class="space-y-4 p-4 bg-gray-50 rounded">
|
|
170
|
+
<div class="grid grid-cols-2 gap-4">
|
|
171
|
+
<div>
|
|
172
|
+
<label class="block text-sm font-medium text-gray-700 mb-1">Method</label>
|
|
173
|
+
<select v-model="form.httpMethod" class="w-full border rounded px-3 py-2">
|
|
174
|
+
<option value="GET">GET</option>
|
|
175
|
+
<option value="POST">POST</option>
|
|
176
|
+
<option value="PUT">PUT</option>
|
|
177
|
+
<option value="DELETE">DELETE</option>
|
|
178
|
+
<option value="PATCH">PATCH</option>
|
|
179
|
+
</select>
|
|
180
|
+
</div>
|
|
181
|
+
<div>
|
|
182
|
+
<label class="block text-sm font-medium text-gray-700 mb-1">URL</label>
|
|
183
|
+
<input v-model="form.httpUrl" type="url" class="w-full border rounded px-3 py-2" placeholder="https://api.example.com/healthz" />
|
|
184
|
+
</div>
|
|
185
|
+
</div>
|
|
186
|
+
|
|
187
|
+
<div class="grid grid-cols-3 gap-4">
|
|
188
|
+
<div>
|
|
189
|
+
<label class="block text-sm font-medium text-gray-700 mb-1">Expected status codes</label>
|
|
190
|
+
<input v-model="form.expectedStatusCodesCsv" type="text" class="w-full border rounded px-3 py-2 font-mono text-sm" placeholder="200,204" />
|
|
191
|
+
</div>
|
|
192
|
+
<div>
|
|
193
|
+
<label class="block text-sm font-medium text-gray-700 mb-1">Max latency (ms)</label>
|
|
194
|
+
<input v-model.number="form.maxLatencyMs" type="number" class="w-full border rounded px-3 py-2" placeholder="1000" />
|
|
195
|
+
</div>
|
|
196
|
+
<div>
|
|
197
|
+
<label class="block text-sm font-medium text-gray-700 mb-1">Timeout (ms)</label>
|
|
198
|
+
<input v-model.number="form.timeoutMs" type="number" class="w-full border rounded px-3 py-2" placeholder="30000" />
|
|
199
|
+
</div>
|
|
200
|
+
</div>
|
|
201
|
+
|
|
202
|
+
<div>
|
|
203
|
+
<label class="block text-sm font-medium text-gray-700 mb-1">Auth</label>
|
|
204
|
+
<select v-model="form.httpAuth.type" class="w-full border rounded px-3 py-2">
|
|
205
|
+
<option value="none">None</option>
|
|
206
|
+
<option value="bearer">Bearer (stored encrypted in Global Settings)</option>
|
|
207
|
+
<option value="basic">Basic (password stored encrypted in Global Settings)</option>
|
|
208
|
+
</select>
|
|
209
|
+
|
|
210
|
+
<div v-if="form.httpAuth.type === 'bearer'" class="mt-2">
|
|
211
|
+
<div class="text-xs text-gray-500 mb-1" v-if="editingCheck && editingCheck.httpAuth && editingCheck.httpAuth.tokenSettingKey">
|
|
212
|
+
Token is already stored. Enter a new token only if you want to rotate it.
|
|
213
|
+
</div>
|
|
214
|
+
<input v-model="form.httpAuth.token" type="text" placeholder="Bearer token (optional on edit)" class="w-full border rounded px-3 py-2 font-mono text-sm" />
|
|
215
|
+
</div>
|
|
216
|
+
|
|
217
|
+
<div v-if="form.httpAuth.type === 'basic'" class="mt-2 grid grid-cols-2 gap-2">
|
|
218
|
+
<div>
|
|
219
|
+
<input v-model="form.httpAuth.username" type="text" placeholder="Username" class="w-full border rounded px-3 py-2" />
|
|
220
|
+
</div>
|
|
221
|
+
<div>
|
|
222
|
+
<div class="text-xs text-gray-500 mb-1" v-if="editingCheck && editingCheck.httpAuth && editingCheck.httpAuth.passwordSettingKey">
|
|
223
|
+
Password is already stored. Enter a new password only if you want to rotate it.
|
|
224
|
+
</div>
|
|
225
|
+
<input v-model="form.httpAuth.password" type="password" placeholder="Password (optional on edit)" class="w-full border rounded px-3 py-2" />
|
|
226
|
+
</div>
|
|
227
|
+
</div>
|
|
228
|
+
</div>
|
|
229
|
+
</div>
|
|
230
|
+
|
|
231
|
+
<!-- Script -->
|
|
232
|
+
<div v-if="form.checkType === 'script'" class="space-y-4 p-4 bg-gray-50 rounded">
|
|
233
|
+
<div>
|
|
234
|
+
<label class="block text-sm font-medium text-gray-700 mb-1">Script</label>
|
|
235
|
+
<select v-model="form.scriptId" class="w-full border rounded px-3 py-2">
|
|
236
|
+
<option value="">Select a script</option>
|
|
237
|
+
<option v-for="s in scripts" :key="s._id" :value="s._id">{{ s.name }} ({{ s.type }}, {{ s.runner }})</option>
|
|
238
|
+
</select>
|
|
239
|
+
</div>
|
|
240
|
+
</div>
|
|
241
|
+
|
|
242
|
+
<!-- Policy -->
|
|
243
|
+
<div class="grid grid-cols-3 gap-4">
|
|
244
|
+
<div>
|
|
245
|
+
<label class="block text-sm font-medium text-gray-700 mb-1">Fail threshold</label>
|
|
246
|
+
<input v-model.number="form.consecutiveFailuresToOpen" type="number" class="w-full border rounded px-3 py-2" placeholder="3" />
|
|
247
|
+
</div>
|
|
248
|
+
<div>
|
|
249
|
+
<label class="block text-sm font-medium text-gray-700 mb-1">Resolve threshold</label>
|
|
250
|
+
<input v-model.number="form.consecutiveSuccessesToResolve" type="number" class="w-full border rounded px-3 py-2" placeholder="2" />
|
|
251
|
+
</div>
|
|
252
|
+
<div>
|
|
253
|
+
<label class="block text-sm font-medium text-gray-700 mb-1">Retries</label>
|
|
254
|
+
<input v-model.number="form.retries" type="number" class="w-full border rounded px-3 py-2" placeholder="0" />
|
|
255
|
+
</div>
|
|
256
|
+
</div>
|
|
257
|
+
|
|
258
|
+
<!-- Notifications -->
|
|
259
|
+
<div class="p-4 bg-gray-50 rounded space-y-3">
|
|
260
|
+
<div class="text-sm font-medium text-gray-900">Notifications</div>
|
|
261
|
+
<div class="grid grid-cols-2 gap-4">
|
|
262
|
+
<div>
|
|
263
|
+
<label class="block text-sm font-medium text-gray-700 mb-1">Channel</label>
|
|
264
|
+
<select v-model="form.notificationChannel" class="w-full border rounded px-3 py-2">
|
|
265
|
+
<option value="in_app">In-app</option>
|
|
266
|
+
<option value="email">Email</option>
|
|
267
|
+
<option value="both">Both</option>
|
|
268
|
+
</select>
|
|
269
|
+
</div>
|
|
270
|
+
<div>
|
|
271
|
+
<label class="block text-sm font-medium text-gray-700 mb-1">Notify user IDs (comma-separated)</label>
|
|
272
|
+
<input v-model="form.notifyUserIdsCsv" type="text" class="w-full border rounded px-3 py-2 font-mono text-sm" placeholder="507f1f77bcf86cd799439011, ..." />
|
|
273
|
+
</div>
|
|
274
|
+
</div>
|
|
275
|
+
|
|
276
|
+
<div class="flex flex-wrap items-center gap-4 text-sm">
|
|
277
|
+
<label class="inline-flex items-center gap-2">
|
|
278
|
+
<input type="checkbox" v-model="form.notifyOnOpen" /> Notify on open
|
|
279
|
+
</label>
|
|
280
|
+
<label class="inline-flex items-center gap-2">
|
|
281
|
+
<input type="checkbox" v-model="form.notifyOnResolve" /> Notify on resolve
|
|
282
|
+
</label>
|
|
283
|
+
<label class="inline-flex items-center gap-2">
|
|
284
|
+
<input type="checkbox" v-model="form.notifyOnEscalation" /> Notify on escalation
|
|
285
|
+
</label>
|
|
286
|
+
<label class="inline-flex items-center gap-2">
|
|
287
|
+
<input type="checkbox" v-model="form.suppressNotificationsWhenAcknowledged" /> Suppress when acknowledged
|
|
288
|
+
</label>
|
|
289
|
+
</div>
|
|
290
|
+
</div>
|
|
291
|
+
|
|
292
|
+
<!-- Auto-heal (minimal) -->
|
|
293
|
+
<div class="p-4 bg-gray-50 rounded space-y-3">
|
|
294
|
+
<div class="flex items-center justify-between">
|
|
295
|
+
<div class="text-sm font-medium text-gray-900">Auto-heal</div>
|
|
296
|
+
<label class="inline-flex items-center gap-2 text-sm">
|
|
297
|
+
<input type="checkbox" v-model="form.autoHealEnabled" /> Enabled
|
|
298
|
+
</label>
|
|
299
|
+
</div>
|
|
300
|
+
<div class="text-xs text-gray-500">
|
|
301
|
+
Configure actions via API for now (stored as <code class="px-1 py-0.5 bg-gray-100 rounded">autoHealActions</code>). UI focuses on monitoring + run history.
|
|
302
|
+
</div>
|
|
303
|
+
</div>
|
|
304
|
+
</div>
|
|
305
|
+
|
|
306
|
+
<div class="mt-6 flex justify-end gap-2">
|
|
307
|
+
<button @click="closeModal" class="px-4 py-2 border rounded text-gray-700 hover:bg-gray-50">Cancel</button>
|
|
308
|
+
<button @click="saveCheck" class="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700">
|
|
309
|
+
{{ editingCheck ? 'Update' : 'Create' }}
|
|
310
|
+
</button>
|
|
311
|
+
</div>
|
|
312
|
+
</div>
|
|
313
|
+
</div>
|
|
314
|
+
</div>
|
|
315
|
+
|
|
316
|
+
<!-- Run History Modal -->
|
|
317
|
+
<div v-if="showRunsModal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
|
318
|
+
<div class="bg-white rounded-lg w-full max-w-5xl max-h-[90vh] overflow-y-auto">
|
|
319
|
+
<div class="p-6">
|
|
320
|
+
<div class="flex items-center justify-between mb-4">
|
|
321
|
+
<h2 class="text-lg font-semibold">Run History</h2>
|
|
322
|
+
<button @click="showRunsModal = false" class="text-gray-400 hover:text-gray-600">
|
|
323
|
+
<i class="ti ti-x text-xl"></i>
|
|
324
|
+
</button>
|
|
325
|
+
</div>
|
|
326
|
+
|
|
327
|
+
<div class="mb-4 text-sm text-gray-600">
|
|
328
|
+
Showing last runs for: <strong>{{ selectedCheck?.name }}</strong>
|
|
329
|
+
</div>
|
|
330
|
+
|
|
331
|
+
<div class="overflow-x-auto">
|
|
332
|
+
<table class="w-full text-sm">
|
|
333
|
+
<thead class="bg-gray-50">
|
|
334
|
+
<tr>
|
|
335
|
+
<th class="px-4 py-2 text-left">Started</th>
|
|
336
|
+
<th class="px-4 py-2 text-left">Duration</th>
|
|
337
|
+
<th class="px-4 py-2 text-left">Latency</th>
|
|
338
|
+
<th class="px-4 py-2 text-left">Status</th>
|
|
339
|
+
<th class="px-4 py-2 text-left">HTTP</th>
|
|
340
|
+
<th class="px-4 py-2 text-left">Reason</th>
|
|
341
|
+
</tr>
|
|
342
|
+
</thead>
|
|
343
|
+
<tbody class="divide-y">
|
|
344
|
+
<tr v-for="r in runs" :key="r._id">
|
|
345
|
+
<td class="px-4 py-2">{{ formatDate(r.startedAt) }}</td>
|
|
346
|
+
<td class="px-4 py-2">{{ r.durationMs ? `${r.durationMs}ms` : '-' }}</td>
|
|
347
|
+
<td class="px-4 py-2">{{ r.latencyMs ? `${r.latencyMs}ms` : '-' }}</td>
|
|
348
|
+
<td class="px-4 py-2">
|
|
349
|
+
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium" :class="getRunStatusClass(r.status)">
|
|
350
|
+
{{ r.status }}
|
|
351
|
+
</span>
|
|
352
|
+
</td>
|
|
353
|
+
<td class="px-4 py-2">{{ r.httpStatusCode || '-' }}</td>
|
|
354
|
+
<td class="px-4 py-2 text-gray-700">{{ r.reason || r.errorMessage || '-' }}</td>
|
|
355
|
+
</tr>
|
|
356
|
+
</tbody>
|
|
357
|
+
</table>
|
|
358
|
+
<div v-if="runs.length === 0" class="text-center py-4 text-gray-500">No runs found</div>
|
|
359
|
+
</div>
|
|
360
|
+
</div>
|
|
361
|
+
</div>
|
|
362
|
+
</div>
|
|
363
|
+
</div>
|
|
364
|
+
|
|
365
|
+
<script>
|
|
366
|
+
const { createApp } = Vue;
|
|
367
|
+
|
|
368
|
+
createApp({
|
|
369
|
+
data() {
|
|
370
|
+
return {
|
|
371
|
+
healthChecks: [],
|
|
372
|
+
scripts: [],
|
|
373
|
+
|
|
374
|
+
publicStatusEnabled: false,
|
|
375
|
+
|
|
376
|
+
showCreateModal: false,
|
|
377
|
+
editingCheck: null,
|
|
378
|
+
|
|
379
|
+
showRunsModal: false,
|
|
380
|
+
selectedCheck: null,
|
|
381
|
+
runs: [],
|
|
382
|
+
|
|
383
|
+
form: {
|
|
384
|
+
name: '',
|
|
385
|
+
description: '',
|
|
386
|
+
enabled: true,
|
|
387
|
+
cronExpression: '*/1 * * * *',
|
|
388
|
+
timezone: 'UTC',
|
|
389
|
+
checkType: 'http',
|
|
390
|
+
|
|
391
|
+
httpMethod: 'GET',
|
|
392
|
+
httpUrl: '',
|
|
393
|
+
httpAuth: { type: 'none', username: '', token: '', password: '' },
|
|
394
|
+
|
|
395
|
+
scriptId: '',
|
|
396
|
+
|
|
397
|
+
expectedStatusCodesCsv: '200',
|
|
398
|
+
maxLatencyMs: null,
|
|
399
|
+
timeoutMs: 30000,
|
|
400
|
+
|
|
401
|
+
consecutiveFailuresToOpen: 3,
|
|
402
|
+
consecutiveSuccessesToResolve: 2,
|
|
403
|
+
retries: 0,
|
|
404
|
+
|
|
405
|
+
notificationChannel: 'in_app',
|
|
406
|
+
notifyUserIdsCsv: '',
|
|
407
|
+
notifyOnOpen: true,
|
|
408
|
+
notifyOnResolve: true,
|
|
409
|
+
notifyOnEscalation: false,
|
|
410
|
+
suppressNotificationsWhenAcknowledged: true,
|
|
411
|
+
|
|
412
|
+
autoHealEnabled: false,
|
|
413
|
+
},
|
|
414
|
+
};
|
|
415
|
+
},
|
|
416
|
+
|
|
417
|
+
async mounted() {
|
|
418
|
+
await this.refreshAll();
|
|
419
|
+
},
|
|
420
|
+
|
|
421
|
+
methods: {
|
|
422
|
+
async refreshAll() {
|
|
423
|
+
await Promise.all([this.loadChecks(), this.loadScripts()]);
|
|
424
|
+
},
|
|
425
|
+
|
|
426
|
+
async loadChecks() {
|
|
427
|
+
try {
|
|
428
|
+
const res = await fetch('/api/admin/health-checks');
|
|
429
|
+
const data = await res.json();
|
|
430
|
+
this.healthChecks = data.items || [];
|
|
431
|
+
this.publicStatusEnabled = Boolean(data.publicStatusEnabled);
|
|
432
|
+
} catch (e) {
|
|
433
|
+
console.error('Failed to load health checks:', e);
|
|
434
|
+
}
|
|
435
|
+
},
|
|
436
|
+
|
|
437
|
+
async loadScripts() {
|
|
438
|
+
try {
|
|
439
|
+
const res = await fetch('/api/admin/scripts');
|
|
440
|
+
const data = await res.json();
|
|
441
|
+
this.scripts = data.items || [];
|
|
442
|
+
} catch (e) {
|
|
443
|
+
console.error('Failed to load scripts:', e);
|
|
444
|
+
}
|
|
445
|
+
},
|
|
446
|
+
|
|
447
|
+
resolveScriptName(scriptId) {
|
|
448
|
+
if (!scriptId) return '(none)';
|
|
449
|
+
const s = this.scripts.find((x) => String(x._id) === String(scriptId));
|
|
450
|
+
return s ? s.name : String(scriptId);
|
|
451
|
+
},
|
|
452
|
+
|
|
453
|
+
getHealthStatusLabel(lastStatus, incidentId) {
|
|
454
|
+
if (incidentId) return 'Incident';
|
|
455
|
+
if (!lastStatus) return 'Unknown';
|
|
456
|
+
return lastStatus === 'healthy' ? 'Healthy' : lastStatus === 'unhealthy' ? 'Unhealthy' : 'Unknown';
|
|
457
|
+
},
|
|
458
|
+
|
|
459
|
+
getHealthStatusClass(lastStatus, incidentId) {
|
|
460
|
+
if (incidentId) return 'bg-red-100 text-red-800';
|
|
461
|
+
if (lastStatus === 'healthy') return 'bg-green-100 text-green-800';
|
|
462
|
+
if (lastStatus === 'unhealthy') return 'bg-red-100 text-red-800';
|
|
463
|
+
return 'bg-gray-100 text-gray-800';
|
|
464
|
+
},
|
|
465
|
+
|
|
466
|
+
getRunStatusClass(status) {
|
|
467
|
+
switch (status) {
|
|
468
|
+
case 'healthy': return 'bg-green-100 text-green-800';
|
|
469
|
+
case 'unhealthy': return 'bg-red-100 text-red-800';
|
|
470
|
+
case 'timed_out': return 'bg-yellow-100 text-yellow-800';
|
|
471
|
+
case 'error': return 'bg-red-100 text-red-800';
|
|
472
|
+
case 'running': return 'bg-blue-100 text-blue-800';
|
|
473
|
+
default: return 'bg-gray-100 text-gray-800';
|
|
474
|
+
}
|
|
475
|
+
},
|
|
476
|
+
|
|
477
|
+
formatDate(date) {
|
|
478
|
+
if (!date) return '-';
|
|
479
|
+
return new Date(date).toLocaleString();
|
|
480
|
+
},
|
|
481
|
+
|
|
482
|
+
onCheckTypeChange() {
|
|
483
|
+
if (this.form.checkType === 'http') {
|
|
484
|
+
this.form.scriptId = '';
|
|
485
|
+
} else {
|
|
486
|
+
this.form.httpUrl = '';
|
|
487
|
+
this.form.httpMethod = 'GET';
|
|
488
|
+
this.form.httpAuth = { type: 'none', username: '', token: '', password: '' };
|
|
489
|
+
}
|
|
490
|
+
},
|
|
491
|
+
|
|
492
|
+
closeModal() {
|
|
493
|
+
this.showCreateModal = false;
|
|
494
|
+
this.editingCheck = null;
|
|
495
|
+
this.resetForm();
|
|
496
|
+
},
|
|
497
|
+
|
|
498
|
+
resetForm() {
|
|
499
|
+
this.form = {
|
|
500
|
+
name: '',
|
|
501
|
+
description: '',
|
|
502
|
+
enabled: true,
|
|
503
|
+
cronExpression: '*/1 * * * *',
|
|
504
|
+
timezone: 'UTC',
|
|
505
|
+
checkType: 'http',
|
|
506
|
+
|
|
507
|
+
httpMethod: 'GET',
|
|
508
|
+
httpUrl: '',
|
|
509
|
+
httpAuth: { type: 'none', username: '', token: '', password: '' },
|
|
510
|
+
|
|
511
|
+
scriptId: '',
|
|
512
|
+
|
|
513
|
+
expectedStatusCodesCsv: '200',
|
|
514
|
+
maxLatencyMs: null,
|
|
515
|
+
timeoutMs: 30000,
|
|
516
|
+
|
|
517
|
+
consecutiveFailuresToOpen: 3,
|
|
518
|
+
consecutiveSuccessesToResolve: 2,
|
|
519
|
+
retries: 0,
|
|
520
|
+
|
|
521
|
+
notificationChannel: 'in_app',
|
|
522
|
+
notifyUserIdsCsv: '',
|
|
523
|
+
notifyOnOpen: true,
|
|
524
|
+
notifyOnResolve: true,
|
|
525
|
+
notifyOnEscalation: false,
|
|
526
|
+
suppressNotificationsWhenAcknowledged: true,
|
|
527
|
+
|
|
528
|
+
autoHealEnabled: false,
|
|
529
|
+
};
|
|
530
|
+
},
|
|
531
|
+
|
|
532
|
+
editCheck(hc) {
|
|
533
|
+
this.editingCheck = hc;
|
|
534
|
+
this.form = {
|
|
535
|
+
name: hc.name,
|
|
536
|
+
description: hc.description,
|
|
537
|
+
enabled: Boolean(hc.enabled),
|
|
538
|
+
cronExpression: hc.cronExpression,
|
|
539
|
+
timezone: hc.timezone,
|
|
540
|
+
checkType: hc.checkType,
|
|
541
|
+
|
|
542
|
+
httpMethod: hc.httpMethod || 'GET',
|
|
543
|
+
httpUrl: hc.httpUrl || '',
|
|
544
|
+
httpAuth: { type: (hc.httpAuth && hc.httpAuth.type) || 'none', username: (hc.httpAuth && hc.httpAuth.username) || '', token: '', password: '' },
|
|
545
|
+
|
|
546
|
+
scriptId: hc.scriptId || '',
|
|
547
|
+
|
|
548
|
+
expectedStatusCodesCsv: (hc.expectedStatusCodes || [200]).join(','),
|
|
549
|
+
maxLatencyMs: hc.maxLatencyMs || null,
|
|
550
|
+
timeoutMs: hc.timeoutMs || 30000,
|
|
551
|
+
|
|
552
|
+
consecutiveFailuresToOpen: hc.consecutiveFailuresToOpen || 3,
|
|
553
|
+
consecutiveSuccessesToResolve: hc.consecutiveSuccessesToResolve || 2,
|
|
554
|
+
retries: hc.retries || 0,
|
|
555
|
+
|
|
556
|
+
notificationChannel: hc.notificationChannel || 'in_app',
|
|
557
|
+
notifyUserIdsCsv: Array.isArray(hc.notifyUserIds) ? hc.notifyUserIds.join(',') : '',
|
|
558
|
+
notifyOnOpen: hc.notifyOnOpen !== false,
|
|
559
|
+
notifyOnResolve: hc.notifyOnResolve !== false,
|
|
560
|
+
notifyOnEscalation: Boolean(hc.notifyOnEscalation),
|
|
561
|
+
suppressNotificationsWhenAcknowledged: hc.suppressNotificationsWhenAcknowledged !== false,
|
|
562
|
+
|
|
563
|
+
autoHealEnabled: Boolean(hc.autoHealEnabled),
|
|
564
|
+
};
|
|
565
|
+
},
|
|
566
|
+
|
|
567
|
+
parseExpectedCodes(csv) {
|
|
568
|
+
const parts = String(csv || '').split(',').map((s) => s.trim()).filter(Boolean);
|
|
569
|
+
const nums = parts.map((p) => Number(p)).filter((n) => Number.isFinite(n));
|
|
570
|
+
return nums.length ? nums : [200];
|
|
571
|
+
},
|
|
572
|
+
|
|
573
|
+
parseNotifyUserIds(csv) {
|
|
574
|
+
return String(csv || '')
|
|
575
|
+
.split(',')
|
|
576
|
+
.map((s) => s.trim())
|
|
577
|
+
.filter(Boolean);
|
|
578
|
+
},
|
|
579
|
+
|
|
580
|
+
async saveCheck() {
|
|
581
|
+
try {
|
|
582
|
+
const payload = {
|
|
583
|
+
name: this.form.name,
|
|
584
|
+
description: this.form.description,
|
|
585
|
+
enabled: Boolean(this.form.enabled),
|
|
586
|
+
cronExpression: this.form.cronExpression,
|
|
587
|
+
timezone: this.form.timezone,
|
|
588
|
+
checkType: this.form.checkType,
|
|
589
|
+
timeoutMs: Number(this.form.timeoutMs || 0),
|
|
590
|
+
|
|
591
|
+
expectedStatusCodes: this.parseExpectedCodes(this.form.expectedStatusCodesCsv),
|
|
592
|
+
maxLatencyMs: this.form.maxLatencyMs === null || this.form.maxLatencyMs === '' ? undefined : Number(this.form.maxLatencyMs),
|
|
593
|
+
|
|
594
|
+
consecutiveFailuresToOpen: Number(this.form.consecutiveFailuresToOpen || 0),
|
|
595
|
+
consecutiveSuccessesToResolve: Number(this.form.consecutiveSuccessesToResolve || 0),
|
|
596
|
+
retries: Number(this.form.retries || 0),
|
|
597
|
+
|
|
598
|
+
notificationChannel: this.form.notificationChannel,
|
|
599
|
+
notifyUserIds: this.parseNotifyUserIds(this.form.notifyUserIdsCsv),
|
|
600
|
+
notifyOnOpen: Boolean(this.form.notifyOnOpen),
|
|
601
|
+
notifyOnResolve: Boolean(this.form.notifyOnResolve),
|
|
602
|
+
notifyOnEscalation: Boolean(this.form.notifyOnEscalation),
|
|
603
|
+
suppressNotificationsWhenAcknowledged: Boolean(this.form.suppressNotificationsWhenAcknowledged),
|
|
604
|
+
|
|
605
|
+
autoHealEnabled: Boolean(this.form.autoHealEnabled),
|
|
606
|
+
};
|
|
607
|
+
|
|
608
|
+
if (this.form.checkType === 'http') {
|
|
609
|
+
payload.httpMethod = this.form.httpMethod;
|
|
610
|
+
payload.httpUrl = this.form.httpUrl;
|
|
611
|
+
payload.httpAuth = {
|
|
612
|
+
type: this.form.httpAuth.type,
|
|
613
|
+
username: this.form.httpAuth.username,
|
|
614
|
+
token: this.form.httpAuth.token,
|
|
615
|
+
password: this.form.httpAuth.password,
|
|
616
|
+
};
|
|
617
|
+
} else {
|
|
618
|
+
payload.scriptId = this.form.scriptId;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
const url = this.editingCheck ? `/api/admin/health-checks/${this.editingCheck._id}` : '/api/admin/health-checks';
|
|
622
|
+
const method = this.editingCheck ? 'PUT' : 'POST';
|
|
623
|
+
|
|
624
|
+
const res = await fetch(url, {
|
|
625
|
+
method,
|
|
626
|
+
headers: { 'Content-Type': 'application/json' },
|
|
627
|
+
body: JSON.stringify(payload),
|
|
628
|
+
});
|
|
629
|
+
|
|
630
|
+
if (!res.ok) {
|
|
631
|
+
const err = await res.json().catch(() => ({}));
|
|
632
|
+
alert(err.error || 'Failed to save health check');
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
this.closeModal();
|
|
637
|
+
await this.loadChecks();
|
|
638
|
+
} catch (e) {
|
|
639
|
+
console.error('Failed to save health check:', e);
|
|
640
|
+
alert('Failed to save health check');
|
|
641
|
+
}
|
|
642
|
+
},
|
|
643
|
+
|
|
644
|
+
async toggleCheck(hc) {
|
|
645
|
+
try {
|
|
646
|
+
const endpoint = hc.enabled ? 'disable' : 'enable';
|
|
647
|
+
const res = await fetch(`/api/admin/health-checks/${hc._id}/${endpoint}`, { method: 'POST' });
|
|
648
|
+
if (!res.ok) {
|
|
649
|
+
alert('Failed to toggle check');
|
|
650
|
+
return;
|
|
651
|
+
}
|
|
652
|
+
await this.loadChecks();
|
|
653
|
+
} catch (e) {
|
|
654
|
+
console.error('Failed to toggle:', e);
|
|
655
|
+
alert('Failed to toggle check');
|
|
656
|
+
}
|
|
657
|
+
},
|
|
658
|
+
|
|
659
|
+
async triggerCheck(id) {
|
|
660
|
+
try {
|
|
661
|
+
const res = await fetch(`/api/admin/health-checks/${id}/trigger`, { method: 'POST' });
|
|
662
|
+
const data = await res.json().catch(() => ({}));
|
|
663
|
+
if (!res.ok) {
|
|
664
|
+
alert(data.error || 'Failed to trigger');
|
|
665
|
+
return;
|
|
666
|
+
}
|
|
667
|
+
alert(`Triggered. Status: ${data.status}`);
|
|
668
|
+
await this.loadChecks();
|
|
669
|
+
} catch (e) {
|
|
670
|
+
console.error('Failed to trigger:', e);
|
|
671
|
+
alert('Failed to trigger');
|
|
672
|
+
}
|
|
673
|
+
},
|
|
674
|
+
|
|
675
|
+
async deleteCheck(hc) {
|
|
676
|
+
if (!confirm(`Delete health check "${hc.name}"?`)) return;
|
|
677
|
+
try {
|
|
678
|
+
const res = await fetch(`/api/admin/health-checks/${hc._id}`, { method: 'DELETE' });
|
|
679
|
+
if (!res.ok) {
|
|
680
|
+
alert('Failed to delete');
|
|
681
|
+
return;
|
|
682
|
+
}
|
|
683
|
+
await this.loadChecks();
|
|
684
|
+
} catch (e) {
|
|
685
|
+
console.error('Failed to delete:', e);
|
|
686
|
+
alert('Failed to delete');
|
|
687
|
+
}
|
|
688
|
+
},
|
|
689
|
+
|
|
690
|
+
async viewRuns(hc) {
|
|
691
|
+
this.selectedCheck = hc;
|
|
692
|
+
this.showRunsModal = true;
|
|
693
|
+
try {
|
|
694
|
+
const res = await fetch(`/api/admin/health-checks/${hc._id}/runs`);
|
|
695
|
+
const data = await res.json();
|
|
696
|
+
this.runs = data.items || [];
|
|
697
|
+
} catch (e) {
|
|
698
|
+
console.error('Failed to load runs:', e);
|
|
699
|
+
this.runs = [];
|
|
700
|
+
}
|
|
701
|
+
},
|
|
702
|
+
|
|
703
|
+
async togglePublicStatus() {
|
|
704
|
+
try {
|
|
705
|
+
const next = !this.publicStatusEnabled;
|
|
706
|
+
const res = await fetch('/api/admin/health-checks/config', {
|
|
707
|
+
method: 'PUT',
|
|
708
|
+
headers: { 'Content-Type': 'application/json' },
|
|
709
|
+
body: JSON.stringify({ publicStatusEnabled: next }),
|
|
710
|
+
});
|
|
711
|
+
if (!res.ok) {
|
|
712
|
+
alert('Failed to update setting');
|
|
713
|
+
return;
|
|
714
|
+
}
|
|
715
|
+
this.publicStatusEnabled = next;
|
|
716
|
+
} catch (e) {
|
|
717
|
+
console.error('Failed to toggle public status:', e);
|
|
718
|
+
alert('Failed to update setting');
|
|
719
|
+
}
|
|
720
|
+
},
|
|
721
|
+
}
|
|
722
|
+
}).mount('#app');
|
|
723
|
+
</script>
|
|
724
|
+
</body>
|
|
725
|
+
</html>
|