signalk-maintenance-tracker 1.1.0 → 1.2.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/CHANGELOG.md +51 -0
- package/dist/domain/status.js +11 -1
- package/dist/domain/status.js.map +1 -1
- package/dist/signalk/runtime.js +57 -8
- package/dist/signalk/runtime.js.map +1 -1
- package/package.json +1 -1
- package/public/app/components/StatTable.js +27 -0
- package/public/app/components/TaskFormModal.js +26 -1
- package/public/app/lib/format.js +10 -0
- package/public/app/pages/TaskDetailPage.js +212 -119
- package/public/app/types.js +1 -0
- package/public/styles/main.css +60 -12
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,54 @@
|
|
|
1
|
+
# v1.2.0
|
|
2
|
+
|
|
3
|
+
A task detail page that puts the numbers you actually compare side by side,
|
|
4
|
+
plus figures for tasks that have no schedule at all.
|
|
5
|
+
|
|
6
|
+
## Highlights
|
|
7
|
+
|
|
8
|
+
- **Schedule and Runtime are now their own tabular cards.** The old prose
|
|
9
|
+
readout ("Current 1360 h · last done at 1240.5 h · due at 1440.5 h") buried
|
|
10
|
+
the numbers. Each dimension gets its own card with an Interval / Today /
|
|
11
|
+
Last / Elapsed / Next / Remaining table — current reading above the
|
|
12
|
+
last-service one, elapsed as their difference right below — with the
|
|
13
|
+
progress bar under the table and the remaining figure colored by status.
|
|
14
|
+
Rows that don't apply simply fall away, and the runtime card disappears
|
|
15
|
+
entirely for tasks that don't track runtime.
|
|
16
|
+
- **Interval-less tasks now show their figures too.** "Last done" and "hours
|
|
17
|
+
since" only ever needed a logged completion, not an interval, so a purely
|
|
18
|
+
informational task now reports how long (and how many engine hours) it has
|
|
19
|
+
been since it was last serviced. The new `elapsed_time_ms` field is on the
|
|
20
|
+
task API alongside `elapsed_runtime`.
|
|
21
|
+
- **The task edit form explains its read-only fields.** Last maintenance and
|
|
22
|
+
runtime-at-last-maintenance are shown with a note that they come from the
|
|
23
|
+
task's most recent log entry, and that **Mark complete** (or editing the log)
|
|
24
|
+
is how you change them.
|
|
25
|
+
|
|
26
|
+
## Smaller changes and fixes
|
|
27
|
+
|
|
28
|
+
- Runtime subscriptions now throttle with `minPeriod` instead of `period`.
|
|
29
|
+
Pairing `period` with policy `instant` made the SignalK server log a warning
|
|
30
|
+
and ignore the throttle; each path is now limited to one delta per 5s as
|
|
31
|
+
intended.
|
|
32
|
+
- Tags moved from the description card into the task's title bar, and the
|
|
33
|
+
description card is dropped entirely when a task has neither a description
|
|
34
|
+
nor consumables.
|
|
35
|
+
- Log notes are muted wherever the log is rendered, so they read as annotation
|
|
36
|
+
rather than data.
|
|
37
|
+
- Danger and success buttons use the theme's accent text color instead of a
|
|
38
|
+
hardcoded white, which was low-contrast in some MFD themes.
|
|
39
|
+
|
|
40
|
+
# v1.1.1
|
|
41
|
+
|
|
42
|
+
A small release with one important fix for your SD card's lifespan.
|
|
43
|
+
|
|
44
|
+
## Fixes
|
|
45
|
+
|
|
46
|
+
- **Runtime updates no longer hammer the disk.** Every SignalK runtime delta
|
|
47
|
+
was written straight to the database — up to several commits (and disk syncs)
|
|
48
|
+
per second while the engine was running. Runtime values are now kept in
|
|
49
|
+
memory and flushed in a single transaction at most once per minute, with a
|
|
50
|
+
final flush on shutdown so engine hours still survive restarts.
|
|
51
|
+
|
|
1
52
|
# v1.1.0
|
|
2
53
|
|
|
3
54
|
This release teaches Maintenance Tracker to talk to your spares locker, adds
|
package/dist/domain/status.js
CHANGED
|
@@ -54,6 +54,7 @@ function computeTask(task, currentRuntime, now, cfg) {
|
|
|
54
54
|
due_runtime_at: null,
|
|
55
55
|
runtime_fraction: null,
|
|
56
56
|
runtime_status: null,
|
|
57
|
+
elapsed_time_ms: null,
|
|
57
58
|
scheduled_due_date: null,
|
|
58
59
|
scheduled_remaining_ms: null,
|
|
59
60
|
scheduled_fraction: null,
|
|
@@ -68,12 +69,21 @@ function computeTask(task, currentRuntime, now, cfg) {
|
|
|
68
69
|
status_rank: types_1.STATUS_RANK.unknown,
|
|
69
70
|
urgency: -Infinity,
|
|
70
71
|
};
|
|
72
|
+
// Elapsed-since-last-service figures stand on their own: they need only a
|
|
73
|
+
// logged completion (plus, for runtime, a live reading), not an interval. A
|
|
74
|
+
// task with no schedule at all still shows "last done X, N hours since".
|
|
75
|
+
if (task.last_runtime != null && currentRuntime != null) {
|
|
76
|
+
out.elapsed_runtime = currentRuntime - task.last_runtime;
|
|
77
|
+
}
|
|
78
|
+
if (task.last_maintenance != null) {
|
|
79
|
+
out.elapsed_time_ms =
|
|
80
|
+
now.getTime() - new Date(task.last_maintenance).getTime();
|
|
81
|
+
}
|
|
71
82
|
// Runtime dimension: configured when both interval and path are set.
|
|
72
83
|
if (task.runtime_interval != null && task.runtime_path != null) {
|
|
73
84
|
if (task.last_runtime != null && currentRuntime != null) {
|
|
74
85
|
const elapsed = currentRuntime - task.last_runtime;
|
|
75
86
|
const remaining = task.runtime_interval - elapsed;
|
|
76
|
-
out.elapsed_runtime = elapsed;
|
|
77
87
|
out.remaining_runtime = remaining;
|
|
78
88
|
out.due_runtime_at = task.last_runtime + task.runtime_interval;
|
|
79
89
|
out.runtime_fraction = elapsed / task.runtime_interval;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"status.js","sourceRoot":"","sources":["../../src/domain/status.ts"],"names":[],"mappings":";;AAoBA,kCAcC;AAuBD,
|
|
1
|
+
{"version":3,"file":"status.js","sourceRoot":"","sources":["../../src/domain/status.ts"],"names":[],"mappings":"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"}
|
package/dist/signalk/runtime.js
CHANGED
|
@@ -1,25 +1,38 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.RuntimeManager = void 0;
|
|
3
|
+
exports.RuntimeManager = exports.SUBSCRIBE_MIN_PERIOD_MS = exports.FLUSH_INTERVAL_MS = void 0;
|
|
4
|
+
/** Floor between runtime_cache commits — deltas can arrive many times per
|
|
5
|
+
* second, and persisting each one hammers the disk. */
|
|
6
|
+
exports.FLUSH_INTERVAL_MS = 60_000;
|
|
7
|
+
/** Throttle applied to each subscribed path (policy 'instant'): the first
|
|
8
|
+
* delta arrives immediately, then at most one per interval. */
|
|
9
|
+
exports.SUBSCRIBE_MIN_PERIOD_MS = 5000;
|
|
4
10
|
/**
|
|
5
11
|
* Subscribes to the union of all task runtime paths on vessels.self, keeps an
|
|
6
12
|
* in-memory map of the latest value, and persists it to runtime_cache so
|
|
7
13
|
* values survive restarts (§10.2).
|
|
8
14
|
*
|
|
15
|
+
* Deltas only touch the in-memory map; dirty values are flushed to the DB in
|
|
16
|
+
* one transaction at most once per FLUSH_INTERVAL_MS (and on stop()).
|
|
17
|
+
*
|
|
9
18
|
* This is the single seconds→hours conversion boundary: SignalK runtime paths
|
|
10
19
|
* are seconds; everything stored/returned here is hours.
|
|
11
20
|
*/
|
|
12
21
|
class RuntimeManager {
|
|
13
22
|
app;
|
|
14
23
|
db;
|
|
24
|
+
flushIntervalMs;
|
|
15
25
|
values = new Map();
|
|
16
26
|
paths = [];
|
|
17
27
|
unsubscribes = [];
|
|
18
28
|
listeners = [];
|
|
19
29
|
lastUpdate = null;
|
|
20
|
-
|
|
30
|
+
dirty = new Set();
|
|
31
|
+
flushTimer = null;
|
|
32
|
+
constructor(app, db, flushIntervalMs = exports.FLUSH_INTERVAL_MS) {
|
|
21
33
|
this.app = app;
|
|
22
34
|
this.db = db;
|
|
35
|
+
this.flushIntervalMs = flushIntervalMs;
|
|
23
36
|
this.loadCache();
|
|
24
37
|
}
|
|
25
38
|
loadCache() {
|
|
@@ -55,7 +68,11 @@ class RuntimeManager {
|
|
|
55
68
|
context: 'vessels.self',
|
|
56
69
|
subscribe: next.map((path) => ({
|
|
57
70
|
path,
|
|
58
|
-
period
|
|
71
|
+
// minPeriod (not period) is the throttle that goes with policy
|
|
72
|
+
// 'instant' — period implies policy 'fixed' and the server logs a
|
|
73
|
+
// warning if both are given. Runtime hours creep upward slowly, so
|
|
74
|
+
// one delta per SUBSCRIBE_MIN_PERIOD_MS per path is plenty.
|
|
75
|
+
minPeriod: exports.SUBSCRIBE_MIN_PERIOD_MS,
|
|
59
76
|
policy: 'instant',
|
|
60
77
|
})),
|
|
61
78
|
}, this.unsubscribes, (err) => {
|
|
@@ -71,19 +88,51 @@ class RuntimeManager {
|
|
|
71
88
|
const hours = v.value / 3600; // SignalK runtime is seconds (§10.2)
|
|
72
89
|
const timestamp = new Date().toISOString();
|
|
73
90
|
this.values.set(v.path, { value: hours, timestamp });
|
|
74
|
-
this.
|
|
75
|
-
.prepare(`INSERT INTO runtime_cache (path, value, timestamp) VALUES (?, ?, ?)
|
|
76
|
-
ON CONFLICT(path) DO UPDATE SET value = excluded.value, timestamp = excluded.timestamp`)
|
|
77
|
-
.run(v.path, hours, timestamp);
|
|
91
|
+
this.dirty.add(v.path);
|
|
78
92
|
this.lastUpdate = timestamp;
|
|
79
93
|
changed = true;
|
|
80
94
|
}
|
|
81
95
|
}
|
|
82
|
-
if (changed)
|
|
96
|
+
if (changed) {
|
|
97
|
+
this.scheduleFlush();
|
|
83
98
|
for (const fn of this.listeners)
|
|
84
99
|
fn();
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
scheduleFlush() {
|
|
103
|
+
if (this.flushTimer)
|
|
104
|
+
return;
|
|
105
|
+
this.flushTimer = setTimeout(() => this.flush(), this.flushIntervalMs);
|
|
106
|
+
this.flushTimer.unref?.();
|
|
107
|
+
}
|
|
108
|
+
/** Persist all dirty values in a single transaction. */
|
|
109
|
+
flush() {
|
|
110
|
+
if (this.flushTimer) {
|
|
111
|
+
clearTimeout(this.flushTimer);
|
|
112
|
+
this.flushTimer = null;
|
|
113
|
+
}
|
|
114
|
+
if (!this.dirty.size)
|
|
115
|
+
return;
|
|
116
|
+
const upsert = this.db.prepare(`INSERT INTO runtime_cache (path, value, timestamp) VALUES (?, ?, ?)
|
|
117
|
+
ON CONFLICT(path) DO UPDATE SET value = excluded.value, timestamp = excluded.timestamp`);
|
|
118
|
+
this.db.exec('BEGIN');
|
|
119
|
+
try {
|
|
120
|
+
for (const path of this.dirty) {
|
|
121
|
+
const entry = this.values.get(path);
|
|
122
|
+
if (entry)
|
|
123
|
+
upsert.run(path, entry.value, entry.timestamp);
|
|
124
|
+
}
|
|
125
|
+
this.db.exec('COMMIT');
|
|
126
|
+
this.dirty.clear();
|
|
127
|
+
}
|
|
128
|
+
catch (err) {
|
|
129
|
+
this.db.exec('ROLLBACK');
|
|
130
|
+
this.app.error?.(`maintenance-tracker runtime cache flush failed: ${err}`);
|
|
131
|
+
}
|
|
85
132
|
}
|
|
86
133
|
stop() {
|
|
134
|
+
// Flush before the plugin closes the DB so values survive restarts.
|
|
135
|
+
this.flush();
|
|
87
136
|
this.teardown();
|
|
88
137
|
this.paths = [];
|
|
89
138
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"runtime.js","sourceRoot":"","sources":["../../src/signalk/runtime.ts"],"names":[],"mappings":";;;AAEA
|
|
1
|
+
{"version":3,"file":"runtime.js","sourceRoot":"","sources":["../../src/signalk/runtime.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { html } from '../lib/html.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Label/value readout for the task detail page's Schedule and Runtime cards
|
|
5
|
+
* (§7.4). Rows whose value is null are dropped, so a caller can list every
|
|
6
|
+
* field its dimension could have and let the inapplicable ones fall away.
|
|
7
|
+
*
|
|
8
|
+
* @param {{ rows: { label: string, value: any }[] }} props
|
|
9
|
+
*/
|
|
10
|
+
export function StatTable(props) {
|
|
11
|
+
const rows = props.rows.filter((r) => r.value !== null);
|
|
12
|
+
if (!rows.length) return null;
|
|
13
|
+
return html`
|
|
14
|
+
<table class="stat-table">
|
|
15
|
+
<tbody>
|
|
16
|
+
${rows.map(
|
|
17
|
+
(r) => html`
|
|
18
|
+
<tr key=${r.label}>
|
|
19
|
+
<th scope="row">${r.label}</th>
|
|
20
|
+
<td>${r.value}</td>
|
|
21
|
+
</tr>
|
|
22
|
+
`,
|
|
23
|
+
)}
|
|
24
|
+
</tbody>
|
|
25
|
+
</table>
|
|
26
|
+
`;
|
|
27
|
+
}
|
|
@@ -13,6 +13,7 @@ import { ConsumablesPicker } from './ConsumablesPicker.js';
|
|
|
13
13
|
import { PathPicker } from './PathPicker.js';
|
|
14
14
|
import { createTask, updateTask, useTags, useHealth } from '../api/hooks.js';
|
|
15
15
|
import { slugify } from '../lib/slug.js';
|
|
16
|
+
import { formatDate, formatHours } from '../lib/format.js';
|
|
16
17
|
import { toast } from '../lib/toasts.js';
|
|
17
18
|
|
|
18
19
|
/** @typedef {import('../types.js').TaskDTO} TaskDTO */
|
|
@@ -421,7 +422,31 @@ export function TaskFormModal(props) {
|
|
|
421
422
|
</div>
|
|
422
423
|
</div>
|
|
423
424
|
`
|
|
424
|
-
:
|
|
425
|
+
: html`
|
|
426
|
+
<div class="field-row">
|
|
427
|
+
<div class="field">
|
|
428
|
+
<label class="field-label">Last maintenance</label>
|
|
429
|
+
<div class="input input-static">
|
|
430
|
+
${formatDate(task.last_maintenance)}
|
|
431
|
+
</div>
|
|
432
|
+
</div>
|
|
433
|
+
<div class="field">
|
|
434
|
+
<label class="field-label"
|
|
435
|
+
>Runtime at last maintenance</label
|
|
436
|
+
>
|
|
437
|
+
<div class="input input-static">
|
|
438
|
+
${formatHours(task.last_runtime)}
|
|
439
|
+
</div>
|
|
440
|
+
</div>
|
|
441
|
+
</div>
|
|
442
|
+
<div class="field-hint" style="margin-top:-10px">
|
|
443
|
+
<i class="bi bi-info-circle" /> These come from the task's
|
|
444
|
+
most recent log entry, so they aren't editable here. Use${' '}
|
|
445
|
+
<strong>Mark complete</strong>${' '}
|
|
446
|
+
to record work — it accepts a past date and runtime — or edit
|
|
447
|
+
the latest entry in the maintenance log to correct them.
|
|
448
|
+
</div>
|
|
449
|
+
`
|
|
425
450
|
}
|
|
426
451
|
</form>
|
|
427
452
|
<//>
|
package/public/app/lib/format.js
CHANGED
|
@@ -62,6 +62,16 @@ export function formatRemainingTime(ms) {
|
|
|
62
62
|
return humanizeMs(ms);
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
+
/**
|
|
66
|
+
* Elapsed span since something happened: "3 days". Negative spans (a
|
|
67
|
+
* future-dated log entry) read as "in 3 days".
|
|
68
|
+
* @param {number|null|undefined} ms
|
|
69
|
+
*/
|
|
70
|
+
export function formatElapsedTime(ms) {
|
|
71
|
+
if (ms === null || ms === undefined) return '—';
|
|
72
|
+
return ms < 0 ? 'in ' + humanizeMs(-ms) : humanizeMs(ms);
|
|
73
|
+
}
|
|
74
|
+
|
|
65
75
|
/**
|
|
66
76
|
* Value for <input type="date">: today (local) by default, or the UTC date
|
|
67
77
|
* part of a stored ISO string. The YYYY-MM-DD value is sent to the API as-is
|
|
@@ -8,10 +8,12 @@ import { useTask, useTaskLogs, deleteTask, deleteLog } from '../api/hooks.js';
|
|
|
8
8
|
import { useAuth } from '../auth/auth.js';
|
|
9
9
|
import {
|
|
10
10
|
formatDate,
|
|
11
|
+
formatElapsedTime,
|
|
11
12
|
formatHours,
|
|
12
13
|
formatRemainingHours,
|
|
13
14
|
formatRemainingTime,
|
|
14
15
|
formatUser,
|
|
16
|
+
toDateInput,
|
|
15
17
|
} from '../lib/format.js';
|
|
16
18
|
import { navigate } from '../lib/router.js';
|
|
17
19
|
import { toast } from '../lib/toasts.js';
|
|
@@ -19,6 +21,7 @@ import { Table } from '../components/Table.js';
|
|
|
19
21
|
import { StatusBadge } from '../components/StatusBadge.js';
|
|
20
22
|
import { StockBadge } from '../components/StockBadge.js';
|
|
21
23
|
import { ProgressBar } from '../components/ProgressBar.js';
|
|
24
|
+
import { StatTable } from '../components/StatTable.js';
|
|
22
25
|
import { MarkdownView } from '../components/MarkdownView.js';
|
|
23
26
|
import { TaskFormModal } from '../components/TaskFormModal.js';
|
|
24
27
|
import { LogEntryModal } from '../components/LogEntryModal.js';
|
|
@@ -112,26 +115,68 @@ export function TaskDetailPage(props) {
|
|
|
112
115
|
const timeConfigured = task.time_interval !== null;
|
|
113
116
|
const dueDateConfigured = task.due_date !== null;
|
|
114
117
|
|
|
118
|
+
// Each card draws whatever its dimension actually knows. An interval is what
|
|
119
|
+
// makes something due, but a bare log entry or runtime reading still answers
|
|
120
|
+
// "when/at what hours was this last done, and how long since" — so a task
|
|
121
|
+
// with no interval at all is not an empty card.
|
|
122
|
+
const hasSchedule =
|
|
123
|
+
timeConfigured || dueDateConfigured || task.last_maintenance !== null;
|
|
124
|
+
const hasRuntime =
|
|
125
|
+
runtimeConfigured ||
|
|
126
|
+
task.runtime_path !== null ||
|
|
127
|
+
task.current_runtime !== null ||
|
|
128
|
+
task.last_runtime !== null;
|
|
129
|
+
|
|
130
|
+
const consumables = task.consumables || [];
|
|
131
|
+
|
|
132
|
+
// With both a recurring interval and a one-time deadline set, the deadline
|
|
133
|
+
// takes a row of its own; on its own it simply is the next thing due.
|
|
134
|
+
const bothTimeDimensions = timeConfigured && dueDateConfigured;
|
|
135
|
+
|
|
136
|
+
// "Next" is whichever time sub-dimension comes first — the same choice the
|
|
137
|
+
// backend makes for remaining_time_ms, so the two rows always agree.
|
|
138
|
+
const nextDueIso =
|
|
139
|
+
task.scheduled_remaining_ms !== null && task.due_date_remaining_ms !== null
|
|
140
|
+
? task.scheduled_remaining_ms <= task.due_date_remaining_ms
|
|
141
|
+
? task.scheduled_due_date
|
|
142
|
+
: task.due_date
|
|
143
|
+
: (task.scheduled_due_date ?? task.due_date);
|
|
144
|
+
|
|
145
|
+
/** Remaining figure colored by its sub-status, as in the task list. */
|
|
146
|
+
const remaining = (
|
|
147
|
+
/** @type {string} */ text,
|
|
148
|
+
/** @type {import('../types.js').Status|null} */ status,
|
|
149
|
+
) => html`<span class=${'remaining ' + (status || '')}>${text}</span>`;
|
|
150
|
+
|
|
115
151
|
// Per-task "due soon" lead windows, shown only when overriding the plugin
|
|
116
152
|
// default (null = default, 0 = no warning).
|
|
117
|
-
const
|
|
153
|
+
const warnHint = (
|
|
118
154
|
/** @type {number|null} */ value,
|
|
119
155
|
/** @type {string} */ unit,
|
|
120
156
|
) =>
|
|
121
157
|
value === null
|
|
122
|
-
?
|
|
123
|
-
:
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
158
|
+
? null
|
|
159
|
+
: html`<div class="field-hint">
|
|
160
|
+
${
|
|
161
|
+
value === 0
|
|
162
|
+
? 'No due-soon warning.'
|
|
163
|
+
: 'Warns ' + value + unit + ' early.'
|
|
164
|
+
}
|
|
165
|
+
</div>`;
|
|
128
166
|
|
|
129
167
|
return html`
|
|
130
168
|
<div>
|
|
131
169
|
<div class="page-header">
|
|
132
170
|
<h1 class="page-title">
|
|
133
171
|
${task.name} <${StatusBadge} status=${task.status} />
|
|
134
|
-
<${StockBadge} consumables=${
|
|
172
|
+
<${StockBadge} consumables=${consumables} />
|
|
173
|
+
${
|
|
174
|
+
task.tags.length
|
|
175
|
+
? html`<span class="chips">
|
|
176
|
+
${task.tags.map((tag) => html`<span key=${tag} class="tag">${tag}</span>`)}
|
|
177
|
+
</span>`
|
|
178
|
+
: null
|
|
179
|
+
}
|
|
135
180
|
</h1>
|
|
136
181
|
${
|
|
137
182
|
auth.isLoggedIn
|
|
@@ -165,126 +210,174 @@ export function TaskDetailPage(props) {
|
|
|
165
210
|
</div>
|
|
166
211
|
|
|
167
212
|
<div class="detail-grid">
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
>
|
|
197
|
-
</
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
</div>
|
|
213
|
+
${
|
|
214
|
+
// Nothing to say without a description or parts: drop the card.
|
|
215
|
+
task.description || consumables.length
|
|
216
|
+
? html`<div class="card">
|
|
217
|
+
<h3>Description</h3>
|
|
218
|
+
${
|
|
219
|
+
task.description
|
|
220
|
+
? html`<${MarkdownView} markdown=${task.description} />`
|
|
221
|
+
: html`<p class="muted" style="margin:0">No description.</p>`
|
|
222
|
+
}
|
|
223
|
+
${
|
|
224
|
+
consumables.length
|
|
225
|
+
? html`<div style="margin-top:16px">
|
|
226
|
+
<h3>Consumables</h3>
|
|
227
|
+
<ul class="consumables-list">
|
|
228
|
+
${consumables.map(
|
|
229
|
+
(c) => html`<li
|
|
230
|
+
key=${c.item_id}
|
|
231
|
+
class="consumables-row"
|
|
232
|
+
>
|
|
233
|
+
<span class="consumables-name"
|
|
234
|
+
>${c.item_name}${' '}
|
|
235
|
+
<span class="muted"
|
|
236
|
+
>× ${c.qty_per_service}</span
|
|
237
|
+
></span
|
|
238
|
+
>
|
|
239
|
+
</li>`,
|
|
240
|
+
)}
|
|
241
|
+
</ul>
|
|
242
|
+
</div>`
|
|
243
|
+
: null
|
|
244
|
+
}
|
|
245
|
+
</div>`
|
|
246
|
+
: null
|
|
247
|
+
}
|
|
204
248
|
|
|
205
249
|
<div class="card">
|
|
206
250
|
<h3>Schedule</h3>
|
|
207
251
|
${
|
|
208
|
-
!
|
|
252
|
+
!hasSchedule
|
|
209
253
|
? html`<p class="muted" style="margin:0">
|
|
210
|
-
|
|
254
|
+
No interval or due date configured.
|
|
211
255
|
</p>`
|
|
212
|
-
:
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
256
|
+
: html`
|
|
257
|
+
<${StatTable}
|
|
258
|
+
rows=${[
|
|
259
|
+
{
|
|
260
|
+
label: 'Interval',
|
|
261
|
+
value: timeConfigured
|
|
262
|
+
? 'every ' +
|
|
263
|
+
task.time_interval +
|
|
264
|
+
' ' +
|
|
265
|
+
task.time_interval_unit
|
|
266
|
+
: null,
|
|
267
|
+
},
|
|
268
|
+
{
|
|
269
|
+
// Only when a recurring interval owns the Next row —
|
|
270
|
+
// on its own the deadline *is* the next thing due.
|
|
271
|
+
label: 'Deadline',
|
|
272
|
+
value: bothTimeDimensions
|
|
273
|
+
? formatDate(task.due_date)
|
|
274
|
+
: null,
|
|
275
|
+
},
|
|
276
|
+
{ label: 'Today', value: toDateInput() },
|
|
277
|
+
{
|
|
278
|
+
label: 'Last',
|
|
279
|
+
value:
|
|
280
|
+
task.last_maintenance !== null
|
|
281
|
+
? formatDate(task.last_maintenance)
|
|
282
|
+
: null,
|
|
283
|
+
},
|
|
284
|
+
{
|
|
285
|
+
label: 'Elapsed',
|
|
286
|
+
value:
|
|
287
|
+
task.elapsed_time_ms !== null
|
|
288
|
+
? formatElapsedTime(task.elapsed_time_ms)
|
|
289
|
+
: null,
|
|
290
|
+
},
|
|
291
|
+
{
|
|
292
|
+
label: 'Next',
|
|
293
|
+
value:
|
|
294
|
+
nextDueIso !== null ? formatDate(nextDueIso) : null,
|
|
295
|
+
},
|
|
296
|
+
{
|
|
297
|
+
label: 'Remaining',
|
|
298
|
+
value:
|
|
299
|
+
task.remaining_time_ms !== null
|
|
300
|
+
? remaining(
|
|
301
|
+
formatRemainingTime(task.remaining_time_ms),
|
|
302
|
+
task.time_status,
|
|
303
|
+
)
|
|
304
|
+
: null,
|
|
305
|
+
},
|
|
306
|
+
]}
|
|
307
|
+
/>
|
|
308
|
+
${warnHint(task.time_warning_days, 'd')}
|
|
309
|
+
<${ProgressBar}
|
|
310
|
+
fraction=${task.time_fraction}
|
|
311
|
+
status=${task.time_status}
|
|
312
|
+
/>
|
|
234
313
|
`
|
|
235
|
-
: null
|
|
236
|
-
}
|
|
237
|
-
${
|
|
238
|
-
timeConfigured
|
|
239
|
-
? html`
|
|
240
|
-
<div class="stat-row">
|
|
241
|
-
<div class="stat-label">
|
|
242
|
-
<span>Time — every ${task.time_interval} ${task.time_interval_unit}</span>
|
|
243
|
-
<span class="stat-value"
|
|
244
|
-
>${formatRemainingTime(task.scheduled_remaining_ms)}${task.scheduled_remaining_ms !== null && task.scheduled_remaining_ms >= 0 ? ' left' : ''}</span
|
|
245
|
-
>
|
|
246
|
-
</div>
|
|
247
|
-
<${ProgressBar}
|
|
248
|
-
fraction=${task.scheduled_fraction}
|
|
249
|
-
status=${task.scheduled_status}
|
|
250
|
-
/>
|
|
251
|
-
<div class="field-hint">
|
|
252
|
-
Last done ${formatDate(task.last_maintenance)} · next
|
|
253
|
-
due ${formatDate(task.scheduled_due_date)}${timeWarnNote}
|
|
254
|
-
</div>
|
|
255
|
-
</div>
|
|
256
|
-
`
|
|
257
|
-
: null
|
|
258
|
-
}
|
|
259
|
-
${
|
|
260
|
-
dueDateConfigured
|
|
261
|
-
? html`
|
|
262
|
-
<div class="stat-row">
|
|
263
|
-
<div class="stat-label">
|
|
264
|
-
<span>Due date — ${formatDate(task.due_date)}</span>
|
|
265
|
-
<span class="stat-value"
|
|
266
|
-
>${formatRemainingTime(task.due_date_remaining_ms)}${task.due_date_remaining_ms !== null && task.due_date_remaining_ms >= 0 ? ' left' : ''}</span
|
|
267
|
-
>
|
|
268
|
-
</div>
|
|
269
|
-
<${ProgressBar}
|
|
270
|
-
fraction=${task.due_date_fraction}
|
|
271
|
-
status=${task.due_date_status}
|
|
272
|
-
/>
|
|
273
|
-
<div class="field-hint">
|
|
274
|
-
One-time deadline.${timeWarnNote}
|
|
275
|
-
</div>
|
|
276
|
-
</div>
|
|
277
|
-
`
|
|
278
|
-
: null
|
|
279
|
-
}
|
|
280
|
-
${
|
|
281
|
-
task.runtime_path
|
|
282
|
-
? html`<div class="field-hint">
|
|
283
|
-
Runtime source: <code>${task.runtime_path}</code>
|
|
284
|
-
</div>`
|
|
285
|
-
: null
|
|
286
314
|
}
|
|
287
315
|
</div>
|
|
316
|
+
|
|
317
|
+
${
|
|
318
|
+
// Nothing to say about runtime on a task that doesn't track it —
|
|
319
|
+
// the remaining cards spread to fill the row.
|
|
320
|
+
hasRuntime
|
|
321
|
+
? html`
|
|
322
|
+
<div class="card">
|
|
323
|
+
<h3>Runtime</h3>
|
|
324
|
+
<${StatTable}
|
|
325
|
+
rows=${[
|
|
326
|
+
{
|
|
327
|
+
label: 'Interval',
|
|
328
|
+
value: runtimeConfigured
|
|
329
|
+
? 'every ' + formatHours(task.runtime_interval)
|
|
330
|
+
: null,
|
|
331
|
+
},
|
|
332
|
+
{
|
|
333
|
+
label: 'Current',
|
|
334
|
+
value:
|
|
335
|
+
task.current_runtime !== null
|
|
336
|
+
? formatHours(task.current_runtime)
|
|
337
|
+
: null,
|
|
338
|
+
},
|
|
339
|
+
{
|
|
340
|
+
label: 'Last',
|
|
341
|
+
value:
|
|
342
|
+
task.last_runtime !== null
|
|
343
|
+
? formatHours(task.last_runtime)
|
|
344
|
+
: null,
|
|
345
|
+
},
|
|
346
|
+
{
|
|
347
|
+
label: 'Elapsed',
|
|
348
|
+
value:
|
|
349
|
+
task.elapsed_runtime !== null
|
|
350
|
+
? formatHours(task.elapsed_runtime)
|
|
351
|
+
: null,
|
|
352
|
+
},
|
|
353
|
+
{
|
|
354
|
+
label: 'Next',
|
|
355
|
+
value:
|
|
356
|
+
task.due_runtime_at !== null
|
|
357
|
+
? formatHours(task.due_runtime_at)
|
|
358
|
+
: null,
|
|
359
|
+
},
|
|
360
|
+
{
|
|
361
|
+
label: 'Remaining',
|
|
362
|
+
value:
|
|
363
|
+
task.remaining_runtime !== null
|
|
364
|
+
? remaining(
|
|
365
|
+
formatRemainingHours(task.remaining_runtime),
|
|
366
|
+
task.runtime_status,
|
|
367
|
+
)
|
|
368
|
+
: null,
|
|
369
|
+
},
|
|
370
|
+
]}
|
|
371
|
+
/>
|
|
372
|
+
${warnHint(task.runtime_warning_hours, 'h')}
|
|
373
|
+
<${ProgressBar}
|
|
374
|
+
fraction=${task.runtime_fraction}
|
|
375
|
+
status=${task.runtime_status}
|
|
376
|
+
/>
|
|
377
|
+
</div>
|
|
378
|
+
`
|
|
379
|
+
: null
|
|
380
|
+
}
|
|
288
381
|
</div>
|
|
289
382
|
|
|
290
383
|
<div class="page-header" style="margin-top:24px">
|
package/public/app/types.js
CHANGED
|
@@ -36,6 +36,7 @@
|
|
|
36
36
|
* @property {number|null} due_runtime_at
|
|
37
37
|
* @property {number|null} runtime_fraction
|
|
38
38
|
* @property {Status|null} runtime_status
|
|
39
|
+
* @property {number|null} elapsed_time_ms
|
|
39
40
|
* @property {string|null} scheduled_due_date
|
|
40
41
|
* @property {number|null} scheduled_remaining_ms
|
|
41
42
|
* @property {number|null} scheduled_fraction
|
package/public/styles/main.css
CHANGED
|
@@ -155,6 +155,19 @@ pre {
|
|
|
155
155
|
.page-title .badge {
|
|
156
156
|
margin-left: 10px;
|
|
157
157
|
}
|
|
158
|
+
/* tags trail the badges on the same 10px rhythm; their own margins give way to
|
|
159
|
+
a gap so no trailing space knocks the row off center */
|
|
160
|
+
.page-title .chips {
|
|
161
|
+
margin-left: 10px;
|
|
162
|
+
margin-bottom: 0;
|
|
163
|
+
gap: 4px 10px;
|
|
164
|
+
}
|
|
165
|
+
.page-title .chips > * {
|
|
166
|
+
margin: 0;
|
|
167
|
+
}
|
|
168
|
+
.page-title .tag {
|
|
169
|
+
font-weight: 400;
|
|
170
|
+
}
|
|
158
171
|
|
|
159
172
|
/* header action buttons; wraps below the title on narrow screens */
|
|
160
173
|
.page-actions {
|
|
@@ -216,7 +229,7 @@ pre {
|
|
|
216
229
|
.btn-danger {
|
|
217
230
|
background: var(--danger);
|
|
218
231
|
border-color: var(--danger);
|
|
219
|
-
color:
|
|
232
|
+
color: var(--accent-text);
|
|
220
233
|
}
|
|
221
234
|
.btn-danger:hover {
|
|
222
235
|
background: var(--danger-hover);
|
|
@@ -226,7 +239,7 @@ pre {
|
|
|
226
239
|
.btn-success {
|
|
227
240
|
background: var(--ok);
|
|
228
241
|
border-color: var(--ok);
|
|
229
|
-
color:
|
|
242
|
+
color: var(--accent-text);
|
|
230
243
|
}
|
|
231
244
|
.btn-success:hover {
|
|
232
245
|
background: var(--ok-hover);
|
|
@@ -304,6 +317,12 @@ pre {
|
|
|
304
317
|
min-height: 110px;
|
|
305
318
|
resize: vertical;
|
|
306
319
|
}
|
|
320
|
+
/* Read-only value shown in an input's slot, so a derived field keeps the
|
|
321
|
+
layout of the editable one it replaces. */
|
|
322
|
+
.input-static {
|
|
323
|
+
background: var(--bg-inset);
|
|
324
|
+
color: var(--text-muted);
|
|
325
|
+
}
|
|
307
326
|
/* iOS gives date inputs native chrome with its own oversized intrinsic height;
|
|
308
327
|
flatten it so it matches the other inputs (min-height keeps it from collapsing) */
|
|
309
328
|
.input[type='date'] {
|
|
@@ -549,8 +568,11 @@ pre {
|
|
|
549
568
|
background: transparent;
|
|
550
569
|
}
|
|
551
570
|
|
|
571
|
+
/* notes annotate a log entry rather than being its data, so they read one
|
|
572
|
+
step back from the row's figures — in the task log and the master log alike */
|
|
552
573
|
.log-notes {
|
|
553
574
|
display: flex;
|
|
575
|
+
color: var(--text-muted);
|
|
554
576
|
}
|
|
555
577
|
.log-notes-label {
|
|
556
578
|
flex-shrink: 0;
|
|
@@ -751,9 +773,11 @@ pre {
|
|
|
751
773
|
grid-template-columns: 1fr;
|
|
752
774
|
grid-gap: 16px;
|
|
753
775
|
}
|
|
754
|
-
|
|
776
|
+
/* description / schedule / runtime share the row evenly, and the survivors
|
|
777
|
+
spread to fill it when a card is hidden (e.g. no runtime source) */
|
|
778
|
+
@media (min-width: 900px) {
|
|
755
779
|
.detail-grid {
|
|
756
|
-
grid-template-columns:
|
|
780
|
+
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
|
|
757
781
|
}
|
|
758
782
|
}
|
|
759
783
|
|
|
@@ -771,19 +795,43 @@ pre {
|
|
|
771
795
|
color: var(--text-muted);
|
|
772
796
|
}
|
|
773
797
|
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
display: flex;
|
|
779
|
-
justify-content: space-between;
|
|
798
|
+
/* label/value readout in the schedule + runtime cards */
|
|
799
|
+
.stat-table {
|
|
800
|
+
width: 100%;
|
|
801
|
+
border-collapse: collapse;
|
|
780
802
|
font-size: 15px;
|
|
803
|
+
}
|
|
804
|
+
.stat-table th,
|
|
805
|
+
.stat-table td {
|
|
806
|
+
padding: 5px 0;
|
|
807
|
+
vertical-align: baseline;
|
|
808
|
+
border-bottom: 1px solid var(--border);
|
|
809
|
+
}
|
|
810
|
+
.stat-table tr:last-child th,
|
|
811
|
+
.stat-table tr:last-child td {
|
|
812
|
+
border-bottom: 0;
|
|
813
|
+
padding-bottom: 0;
|
|
814
|
+
}
|
|
815
|
+
.stat-table th {
|
|
816
|
+
text-align: left;
|
|
817
|
+
font-weight: 400;
|
|
781
818
|
color: var(--text-muted);
|
|
782
|
-
|
|
819
|
+
white-space: nowrap;
|
|
783
820
|
}
|
|
784
|
-
.stat-
|
|
821
|
+
.stat-table td {
|
|
822
|
+
text-align: right;
|
|
785
823
|
color: var(--text);
|
|
786
824
|
font-weight: 600;
|
|
825
|
+
font-variant-numeric: tabular-nums;
|
|
826
|
+
}
|
|
827
|
+
.stat-table + .field-hint {
|
|
828
|
+
margin-top: 10px;
|
|
829
|
+
}
|
|
830
|
+
/* the bar reads below the numbers it summarises, so the two cards' tables
|
|
831
|
+
start on the same line whether or not a card has a bar to draw */
|
|
832
|
+
.stat-table + .progress,
|
|
833
|
+
.field-hint + .progress {
|
|
834
|
+
margin-top: 12px;
|
|
787
835
|
}
|
|
788
836
|
|
|
789
837
|
.markdown p:first-child {
|