signalk-automation-plugin 0.0.1

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/public/app.js ADDED
@@ -0,0 +1,155 @@
1
+ const PLUGIN = 'signalk-automation-plugin'
2
+ const API_READ = '/signalk/v1/api/' + PLUGIN
3
+ const API_WRITE = '/plugins/' + PLUGIN
4
+
5
+ async function getJson (url) {
6
+ const res = await fetch(url, { credentials: 'same-origin' })
7
+ const text = await res.text()
8
+ let data = {}
9
+ try { data = text ? JSON.parse(text) : {} } catch (_) { data = { error: text } }
10
+ if (!res.ok) throw new Error(data.error || res.status + ' ' + url)
11
+ return data
12
+ }
13
+
14
+ async function sendJson (url, method, body) {
15
+ const res = await fetch(url, {
16
+ method,
17
+ credentials: 'same-origin',
18
+ headers: { 'Content-Type': 'application/json' },
19
+ body: JSON.stringify(body || {})
20
+ })
21
+ const data = await res.json().catch(() => ({}))
22
+ if (!res.ok) throw new Error(data.error || res.status + ' ' + url)
23
+ return data
24
+ }
25
+
26
+ function el (id) { return document.getElementById(id) }
27
+
28
+ function badge (result) {
29
+ const r = result || '—'
30
+ return '<span class="badge ' + (result || '') + '">' + r + '</span>'
31
+ }
32
+
33
+ function fmtTime (iso) {
34
+ if (!iso) return ''
35
+ try { return new Date(iso).toLocaleString() } catch (_) { return iso }
36
+ }
37
+
38
+ let status = null
39
+
40
+ async function load () {
41
+ status = await getJson(API_READ + '/status')
42
+ render()
43
+ }
44
+
45
+ function render () {
46
+ const sha = status.activeSha ? status.activeSha.slice(0, 8) : 'working tree / empty'
47
+ el('shaLine').textContent =
48
+ (status.started ? 'Running' : 'Not started') +
49
+ ' · live ' + sha +
50
+ (status.automationsDir ? ' · repo ' + status.automationsDir : '')
51
+ if (status.errors && status.errors.length) {
52
+ el('shaLine').innerHTML +=
53
+ '<div class="error">' +
54
+ status.errors.map((e) => e.file + ': ' + e.message).join('<br>') +
55
+ '</div>'
56
+ }
57
+
58
+ el('autos').innerHTML = (status.automations || []).map((a) => {
59
+ const last = a.lastRun || {}
60
+ return (
61
+ '<div class="card">' +
62
+ '<div class="row">' +
63
+ '<div><div class="name">' + escapeHtml(a.alias) + '</div>' +
64
+ '<div class="muted">' + escapeHtml(a.id) + ' · ' + fmtTime(last.ts) + '</div></div>' +
65
+ '<div class="row">' +
66
+ badge(last.result) +
67
+ '<label class="muted"><input type="checkbox" data-toggle="' + escapeHtml(a.id) + '"' +
68
+ (a.enabled ? ' checked' : '') + '> on</label>' +
69
+ '<button data-traces="' + escapeHtml(a.id) + '" data-alias="' + escapeHtml(a.alias) + '">runs</button>' +
70
+ '</div>' +
71
+ '</div>' +
72
+ '</div>'
73
+ )
74
+ }).join('') || '<p class="hint">No automations loaded. Set automationsDir and activate a commit.</p>'
75
+
76
+ el('helpers').innerHTML = (status.helpers || []).map((h) => {
77
+ const mode = h.on_start === 'restore' ? 'survive reboot' : h.on_start === 'default' ? 'default on start' : 'nothing'
78
+ const val = h.set ? JSON.stringify(h.value) : 'unset'
79
+ return (
80
+ '<div class="card"><div class="row">' +
81
+ '<div><div class="name">' + escapeHtml(h.name) + '</div>' +
82
+ '<div class="muted">' + escapeHtml(h.id) + ' · ' + mode + ' · ' + escapeHtml(val) + '</div></div>' +
83
+ '<button data-reset="' + escapeHtml(h.id) + '">reset default</button>' +
84
+ '</div></div>'
85
+ )
86
+ }).join('') || '<p class="hint">No helpers in live YAML.</p>'
87
+
88
+ el('yaml').textContent = status.yaml || '# no live YAML'
89
+ el('commits').innerHTML = (status.commits || []).map((c) => {
90
+ const live = status.activeSha && c.sha === status.activeSha
91
+ return (
92
+ '<div class="card"><div class="row">' +
93
+ '<div><div class="name">' + escapeHtml(c.subject || '') + '</div>' +
94
+ '<div class="muted">' + escapeHtml(c.sha.slice(0, 10)) + ' · ' + escapeHtml(c.date || '') +
95
+ (live ? ' · live' : '') + '</div></div>' +
96
+ (live ? '' : '<button class="primary" data-sha="' + escapeHtml(c.sha) + '">make live</button>') +
97
+ '</div></div>'
98
+ )
99
+ }).join('') || '<p class="hint">automationsDir is not a git repo (or git is missing in the container).</p>'
100
+
101
+ bind()
102
+ }
103
+
104
+ function bind () {
105
+ document.querySelectorAll('[data-toggle]').forEach((box) => {
106
+ box.addEventListener('change', async () => {
107
+ await sendJson(API_WRITE + '/automations/' + box.getAttribute('data-toggle') + '/enabled', 'POST', {
108
+ enabled: box.checked
109
+ })
110
+ await load()
111
+ })
112
+ })
113
+ document.querySelectorAll('[data-traces]').forEach((btn) => {
114
+ btn.addEventListener('click', async () => {
115
+ const id = btn.getAttribute('data-traces')
116
+ const data = await getJson(API_READ + '/automations/' + encodeURIComponent(id) + '/traces')
117
+ el('detail').hidden = false
118
+ el('detailTitle').textContent = 'Runs — ' + btn.getAttribute('data-alias')
119
+ el('traces').innerHTML = (data.traces || []).map((t) => {
120
+ return (
121
+ '<div class="card">' +
122
+ badge(t.result) + ' <span class="muted">' + fmtTime(t.ts) + '</span>' +
123
+ (t.reason ? '<div class="muted">' + escapeHtml(t.reason) + '</div>' : '') +
124
+ '<pre class="trace">' + escapeHtml(JSON.stringify(t, null, 2)) + '</pre>' +
125
+ '</div>'
126
+ )
127
+ }).join('') || '<p class="hint">No runs yet.</p>'
128
+ el('detail').scrollIntoView({ behavior: 'smooth' })
129
+ })
130
+ })
131
+ document.querySelectorAll('[data-sha]').forEach((btn) => {
132
+ btn.addEventListener('click', async () => {
133
+ await sendJson(API_WRITE + '/live', 'POST', { sha: btn.getAttribute('data-sha') })
134
+ await load()
135
+ })
136
+ })
137
+ document.querySelectorAll('[data-reset]').forEach((btn) => {
138
+ btn.addEventListener('click', async () => {
139
+ await sendJson(API_WRITE + '/helpers/' + btn.getAttribute('data-reset') + '/reset', 'POST', {})
140
+ await load()
141
+ })
142
+ })
143
+ }
144
+
145
+ function escapeHtml (s) {
146
+ return String(s == null ? '' : s)
147
+ .replace(/&/g, '&amp;')
148
+ .replace(/</g, '&lt;')
149
+ .replace(/>/g, '&gt;')
150
+ .replace(/"/g, '&quot;')
151
+ }
152
+
153
+ load().catch((err) => {
154
+ el('shaLine').innerHTML = '<span class="error">' + escapeHtml(err.message) + '</span>'
155
+ })
@@ -0,0 +1,39 @@
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" />
6
+ <title>Signal K Automations</title>
7
+ <link rel="stylesheet" href="style.css" />
8
+ </head>
9
+ <body>
10
+ <header>
11
+ <h1>Automations</h1>
12
+ <p class="meta" id="shaLine">Loading…</p>
13
+ </header>
14
+ <main>
15
+ <section>
16
+ <h2>Automations</h2>
17
+ <div id="autos"></div>
18
+ </section>
19
+ <section>
20
+ <h2>Helpers</h2>
21
+ <div id="helpers"></div>
22
+ </section>
23
+ <section>
24
+ <h2>Live YAML</h2>
25
+ <pre id="yaml" class="yaml"></pre>
26
+ </section>
27
+ <section>
28
+ <h2>Git commits</h2>
29
+ <p class="hint">Activate a commit to copy that tree into plugin data and reload. Invalid YAML is rejected; the previous live snapshot stays.</p>
30
+ <div id="commits"></div>
31
+ </section>
32
+ <section id="detail" hidden>
33
+ <h2 id="detailTitle">Runs</h2>
34
+ <div id="traces"></div>
35
+ </section>
36
+ </main>
37
+ <script src="app.js"></script>
38
+ </body>
39
+ </html>
@@ -0,0 +1,70 @@
1
+ :root {
2
+ color-scheme: dark;
3
+ --bg: #11161c;
4
+ --card: #1b222b;
5
+ --border: #2c3642;
6
+ --text: #e8eef4;
7
+ --muted: #93a1b0;
8
+ --ok: #3dba7a;
9
+ --fail: #e15b5b;
10
+ --skip: #c9a227;
11
+ --accent: #4d8fd6;
12
+ }
13
+ * { box-sizing: border-box; }
14
+ body {
15
+ margin: 0;
16
+ font-family: ui-sans-serif, system-ui, sans-serif;
17
+ background: var(--bg);
18
+ color: var(--text);
19
+ }
20
+ header, main { max-width: 960px; margin: 0 auto; padding: 1rem 1.2rem; }
21
+ h1 { margin: 0 0 0.25rem; font-size: 1.4rem; }
22
+ h2 { font-size: 1.05rem; margin: 1.4rem 0 0.6rem; }
23
+ .meta, .hint, .muted { color: var(--muted); font-size: 0.9rem; }
24
+ .card {
25
+ background: var(--card);
26
+ border: 1px solid var(--border);
27
+ border-radius: 8px;
28
+ padding: 0.75rem 0.9rem;
29
+ margin-bottom: 0.5rem;
30
+ }
31
+ .row { display: flex; gap: 0.75rem; align-items: center; justify-content: space-between; flex-wrap: wrap; }
32
+ .name { font-weight: 600; }
33
+ .badge {
34
+ font-size: 0.75rem;
35
+ text-transform: uppercase;
36
+ letter-spacing: 0.04em;
37
+ padding: 0.15rem 0.45rem;
38
+ border-radius: 4px;
39
+ background: #2a3340;
40
+ }
41
+ .badge.ok { background: #1d3b2c; color: var(--ok); }
42
+ .badge.failure { background: #3b1d1d; color: var(--fail); }
43
+ .badge.skipped { background: #3b341d; color: var(--skip); }
44
+ button, .toggle {
45
+ background: #243040;
46
+ color: var(--text);
47
+ border: 1px solid var(--border);
48
+ border-radius: 6px;
49
+ padding: 0.3rem 0.6rem;
50
+ cursor: pointer;
51
+ }
52
+ button.primary { background: var(--accent); border-color: var(--accent); color: #071018; }
53
+ .yaml {
54
+ background: #0c1015;
55
+ border: 1px solid var(--border);
56
+ border-radius: 8px;
57
+ padding: 0.8rem;
58
+ overflow: auto;
59
+ max-height: 28rem;
60
+ font-size: 0.8rem;
61
+ white-space: pre-wrap;
62
+ }
63
+ .trace {
64
+ font-family: ui-monospace, monospace;
65
+ font-size: 0.8rem;
66
+ white-space: pre-wrap;
67
+ margin: 0.4rem 0 0;
68
+ color: #c5d0db;
69
+ }
70
+ .error { color: var(--fail); }