clavarium 0.1.0-next.4 → 0.1.0-next.6
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/deploy/install.sh +6 -0
- package/deploy/updater.mjs +7 -3
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/server/api.mjs +1 -1
- package/server/capacity.mjs +99 -35
- package/server/main.mjs +6 -4
- package/server/updates.mjs +1 -1
- package/web/app.js +82 -23
- package/web/index.html +79 -18
- package/web/style.css +22 -16
package/deploy/install.sh
CHANGED
|
@@ -661,6 +661,12 @@ if [ -n "$RELAY" ]; then
|
|
|
661
661
|
echo
|
|
662
662
|
echo "ON THE RELAY ($RELAY), run this one command; the tunnel then comes up by itself:"
|
|
663
663
|
echo " sudo deploy/relay.sh --key \"$TUNNEL_KEY\""
|
|
664
|
+
# A server with a screen and a keyboard only: the key, carried by a phone.
|
|
665
|
+
# It is a PUBLIC key: showing it is safe; it lets the relay know this machine.
|
|
666
|
+
if command -v qrencode >/dev/null 2>&1 || apt-get install -y -qq qrencode >/dev/null 2>&1; then
|
|
667
|
+
echo "The same key, to read with a phone or a tablet:"
|
|
668
|
+
qrencode -t ANSIUTF8 -m 2 -- "$TUNNEL_KEY" || true
|
|
669
|
+
fi
|
|
664
670
|
echo "It will print this, and it must be the same: $RELAY_PRINT"
|
|
665
671
|
echo "The name $DOMAIN must lead to the relay."
|
|
666
672
|
else
|
package/deploy/updater.mjs
CHANGED
|
@@ -366,7 +366,9 @@ export class Updater {
|
|
|
366
366
|
async apply(version) {
|
|
367
367
|
if (!VERSION.test(version ?? '')) throw new Refused(`not a version: ${version}`);
|
|
368
368
|
const from = this.current();
|
|
369
|
-
|
|
369
|
+
/* Asked for the version that runs (a request that waited while it came by itself): done already. */
|
|
370
|
+
if (from && compareVersions(version, from.version) === 0) return;
|
|
371
|
+
if (from && compareVersions(version, from.version) < 0) throw new Refused(`${version} is not newer than ${from.version}`);
|
|
370
372
|
const why = this.busy(); if (why) throw new Refused(why);
|
|
371
373
|
const unlock = this.#lock();
|
|
372
374
|
try {
|
|
@@ -480,7 +482,9 @@ export class Updater {
|
|
|
480
482
|
if ((state.history ?? []).some((h) => h.to === available && h.action === 'update' && h.result !== 'done')) return `${available} failed here before: not tried again by itself`;
|
|
481
483
|
if (this.busy()) return this.busy();
|
|
482
484
|
await this.apply(available);
|
|
483
|
-
|
|
485
|
+
/* What the history says, not what was hoped: a version that did not start was undone. */
|
|
486
|
+
const last = (this.#state().history ?? []).at(-1);
|
|
487
|
+
return last?.to === available && last.result !== 'done' ? `${available} did not work: back to ${last.from ?? 'nothing'} (${last.why ?? last.result})` : `updated to ${available}`;
|
|
484
488
|
}
|
|
485
489
|
|
|
486
490
|
/* What the host program asked (a request file), or the timer's turn. */
|
|
@@ -513,7 +517,7 @@ export class Updater {
|
|
|
513
517
|
let cwd = ''; try { cwd = realpathSync(`/proc/${pid}/cwd`); } catch { /* gone */ }
|
|
514
518
|
if (cwd.startsWith(target)) break;
|
|
515
519
|
said = 'the server that answers is not this release';
|
|
516
|
-
} else if (active.code !== 0) said = '
|
|
520
|
+
} else if (active.code !== 0) said = 'its service stopped';
|
|
517
521
|
if (Date.now() > end) return said;
|
|
518
522
|
await new Promise((r) => setTimeout(r, 1000));
|
|
519
523
|
}
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "clavarium",
|
|
3
|
-
"version": "0.1.0-next.
|
|
3
|
+
"version": "0.1.0-next.6",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "clavarium",
|
|
9
|
-
"version": "0.1.0-next.
|
|
9
|
+
"version": "0.1.0-next.6",
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@xterm/addon-fit": "^0.11.0",
|
package/package.json
CHANGED
package/server/api.mjs
CHANGED
|
@@ -268,7 +268,7 @@ export async function handleApi(request, response, conversations, headers, resou
|
|
|
268
268
|
if (pathname === '/api/updates/settings' && updates) return send(200, await updates.settings({ mode: body.mode, channel: body.channel, window: body.window })), true;
|
|
269
269
|
if (pathname === '/api/updates/apply' && updates) return send(202, await updates.apply(body.version)), true;
|
|
270
270
|
if (pathname === '/api/updates/back' && updates) return send(202, await updates.back(body.version)), true;
|
|
271
|
-
if (pathname === '/api/server/reserve' && capacity) { capacity.setReserve({ diskMb: body.diskMb, memoryMb: body.memoryMb }); return send(200, await capacity.about()), true; }
|
|
271
|
+
if (pathname === '/api/server/reserve' && capacity) { capacity.setReserve({ diskMb: body.diskMb, diskPercent: body.diskPercent, memoryMb: body.memoryMb, memoryPercent: body.memoryPercent }); return send(200, await capacity.about()), true; }
|
|
272
272
|
if (pathname === '/api/git-hosts/forget' && gitAccess) return send(200, gitAccess.forgetHost(body.host)), true;
|
|
273
273
|
const unkeyed = /^\/api\/git-keys\/([a-z0-9-]{1,32})\/remove$/.exec(pathname);
|
|
274
274
|
if (unkeyed && gitAccess) { gitAccess.removeKey(unkeyed[1]); return send(200, { ok: true }), true; }
|
package/server/capacity.mjs
CHANGED
|
@@ -14,22 +14,35 @@
|
|
|
14
14
|
* disks, the machines' disks, the images). Slow (it measures
|
|
15
15
|
* folders): asked again after USAGE_MS at most, in the background.
|
|
16
16
|
*
|
|
17
|
-
* THE RESERVE (Settings, This server):
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* were given
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
17
|
+
* THE RESERVE (Settings, This server, Resources): for each of disk and
|
|
18
|
+
* memory, the larger of a fixed amount and a share of the whole (by default
|
|
19
|
+
* disk 5 GB or 5 %, memory 1 GB or 5 %). It is judged on what the system
|
|
20
|
+
* says is free (df on the machines' disk, MemAvailable), never on sums of
|
|
21
|
+
* what machines were given:
|
|
22
|
+
* - creating, starting, waking or growing a machine is refused, with a
|
|
23
|
+
* sentence that says why and what to do, when what is free less what the
|
|
24
|
+
* machine needs at first would be under the reserve. Memory: its size
|
|
25
|
+
* (or what it grows by). Disk: what a new machine takes, or what a disk
|
|
26
|
+
* grows by. Starting or waking is never refused for disk: a full disk is
|
|
27
|
+
* cleaned from inside the conversations.
|
|
28
|
+
* - all the time (watch): once what is free falls under the reserve, the
|
|
29
|
+
* owner is alerted and the running machine that takes the most is put to
|
|
30
|
+
* sleep, said in its conversation. Once: again only after what is free
|
|
31
|
+
* came back above the reserve.
|
|
32
|
+
* What was PROMISED (the disks' largest sizes, the memory given to running
|
|
33
|
+
* machines) is only said, with a warning when it is more than there is.
|
|
25
34
|
*
|
|
26
35
|
* When the figures cannot be read (a host program from before these verbs),
|
|
27
36
|
* nothing is refused: the reserve is a guard, not a gate that jams.
|
|
28
37
|
*/
|
|
29
|
-
import { Refusal } from './conversations.mjs';
|
|
38
|
+
import { Refusal, DISK_GB } from './conversations.mjs';
|
|
30
39
|
|
|
31
|
-
export const RESERVE_DISK_MB =
|
|
40
|
+
export const RESERVE_DISK_MB = 5 * 1024;
|
|
32
41
|
export const RESERVE_MEMORY_MB = 1024;
|
|
42
|
+
export const RESERVE_DISK_PERCENT = 5;
|
|
43
|
+
export const RESERVE_MEMORY_PERCENT = 5;
|
|
44
|
+
/* How often what is free is looked at, against the reserve. */
|
|
45
|
+
export const WATCH_MS = 60_000;
|
|
33
46
|
/* What may be set, in megabytes. */
|
|
34
47
|
export const RESERVE_DISK_MAX_MB = 1024 * 1024;
|
|
35
48
|
export const RESERVE_MEMORY_MAX_MB = 256 * 1024;
|
|
@@ -74,32 +87,39 @@ export const gb = (mb) => `${(mb / 1024).toFixed(1)} GB`;
|
|
|
74
87
|
const percent = (part, all) => (all > 0 ? Math.round((part / all) * 100) : 0);
|
|
75
88
|
|
|
76
89
|
export class Capacity {
|
|
77
|
-
#run; #store; #conversations; #shelf; #now; #roomMs; #room = null; #roomAt = -Infinity; #usage = null; #usageAt = -Infinity; #going = null; #measuring = null;
|
|
90
|
+
#run; #store; #conversations; #shelf; #now; #roomMs; #sleep; #alert; #say; #short = { disk: false, memory: false }; #room = null; #roomAt = -Infinity; #usage = null; #usageAt = -Infinity; #going = null; #measuring = null;
|
|
78
91
|
|
|
79
92
|
/* `run(argv)`: the host program. `store`: where the reserve is kept.
|
|
80
93
|
`conversations`: what each machine was given. `shelf`: how large a
|
|
81
94
|
ready-made machine is (a new one is a copy of it). */
|
|
82
95
|
/* `roomMs`: how long what is free is taken as known (ROOM_MS). */
|
|
83
|
-
|
|
84
|
-
|
|
96
|
+
/* `sleep(name)`, `alert(payload)`, `say(name, text)`: what watch() does
|
|
97
|
+
when what is free falls under the reserve. */
|
|
98
|
+
constructor({ run, store, conversations, shelf = null, now = () => Date.now(), roomMs = ROOM_MS, sleep = null, alert = null, say = null }) {
|
|
99
|
+
this.#roomMs = roomMs; this.#sleep = sleep; this.#alert = alert; this.#say = say;
|
|
85
100
|
this.#run = run; this.#store = store; this.#conversations = conversations; this.#shelf = shelf; this.#now = now;
|
|
86
101
|
}
|
|
87
102
|
|
|
103
|
+
/* The two settings of each: a fixed amount and a share of the whole. */
|
|
88
104
|
reserve() {
|
|
89
105
|
const r = this.#store.get('reserve') ?? {};
|
|
90
|
-
return { diskMb: r.diskMb ?? RESERVE_DISK_MB, memoryMb: r.memoryMb ?? RESERVE_MEMORY_MB };
|
|
106
|
+
return { diskMb: r.diskMb ?? RESERVE_DISK_MB, diskPercent: r.diskPercent ?? RESERVE_DISK_PERCENT, memoryMb: r.memoryMb ?? RESERVE_MEMORY_MB, memoryPercent: r.memoryPercent ?? RESERVE_MEMORY_PERCENT };
|
|
91
107
|
}
|
|
92
|
-
setReserve({ diskMb, memoryMb } = {}) {
|
|
108
|
+
setReserve({ diskMb, diskPercent, memoryMb, memoryPercent } = {}) {
|
|
93
109
|
const now = this.reserve();
|
|
94
110
|
const take = (value, max, what) => {
|
|
95
111
|
if (value === undefined || value === null || value === '') return undefined;
|
|
96
112
|
const n = Number(value);
|
|
97
|
-
if (!Number.isInteger(n) || n < 0 || n > max) throw new Refusal(400, `The ${what} reserve is a whole number of megabytes, from 0 to ${max}.`);
|
|
113
|
+
if (!Number.isInteger(n) || n < 0 || n > max) throw new Refusal(400, max === 100 ? `The ${what} reserve share is a whole number of percent, from 0 to 100.` : `The ${what} reserve is a whole number of megabytes, from 0 to ${max}.`);
|
|
98
114
|
return n;
|
|
99
115
|
};
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
|
|
116
|
+
const kept = {
|
|
117
|
+
diskMb: take(diskMb, RESERVE_DISK_MAX_MB, 'disk') ?? now.diskMb,
|
|
118
|
+
diskPercent: take(diskPercent, 100, 'disk') ?? now.diskPercent,
|
|
119
|
+
memoryMb: take(memoryMb, RESERVE_MEMORY_MAX_MB, 'memory') ?? now.memoryMb,
|
|
120
|
+
memoryPercent: take(memoryPercent, 100, 'memory') ?? now.memoryPercent,
|
|
121
|
+
};
|
|
122
|
+
this.#store.put({ name: 'reserve', ...kept });
|
|
103
123
|
return this.reserve();
|
|
104
124
|
}
|
|
105
125
|
|
|
@@ -131,21 +151,29 @@ export class Capacity {
|
|
|
131
151
|
disk but the first one, and that one not long. */
|
|
132
152
|
#figures(room) {
|
|
133
153
|
if (!room) return null;
|
|
134
|
-
const
|
|
135
|
-
const
|
|
154
|
+
const set = this.reserve();
|
|
155
|
+
const all = this.#conversations.list();
|
|
156
|
+
const records = new Map(all.map((c) => [`clv-${c.name}`, c]));
|
|
136
157
|
const machines = room.machines.map((m) => {
|
|
137
158
|
const c = records.get(m.vm);
|
|
138
159
|
return { name: c ? c.name : null, shelf: m.vm.startsWith('clvshelf-'), rssMb: m.rssMb, sizeMb: c?.memoryMb ?? null };
|
|
139
160
|
});
|
|
140
|
-
/*
|
|
141
|
-
const
|
|
142
|
-
const memory = room.memory ? {
|
|
143
|
-
totalMb: room.memory.totalMb, freeMb: room.memory.availableMb, promisedMb,
|
|
144
|
-
spareMb: Math.max(0, room.memory.availableMb - promisedMb),
|
|
145
|
-
/* Counted as taken: what running machines were given and will take. */
|
|
146
|
-
usedPercent: percent(room.memory.totalMb - Math.max(0, room.memory.availableMb - promisedMb), room.memory.totalMb),
|
|
147
|
-
} : null;
|
|
161
|
+
/* What the system says is available: the machines' own use is in it already. */
|
|
162
|
+
const memory = room.memory ? { totalMb: room.memory.totalMb, freeMb: room.memory.availableMb, usedPercent: percent(room.memory.totalMb - room.memory.availableMb, room.memory.totalMb) } : null;
|
|
148
163
|
const where = room.places.find((p) => p.what === 'machines') ?? room.places[0];
|
|
164
|
+
/* The reserve kept, in megabytes: the larger of the fixed amount and the share of the whole. */
|
|
165
|
+
const share = (totalMb, pct) => Math.round((totalMb * pct) / 100);
|
|
166
|
+
const reserve = {
|
|
167
|
+
diskMb: Math.max(set.diskMb, where ? share(where.sizeMb, set.diskPercent) : 0),
|
|
168
|
+
memoryMb: Math.max(set.memoryMb, memory ? share(memory.totalMb, set.memoryPercent) : 0),
|
|
169
|
+
};
|
|
170
|
+
/* Promised, only said: every disk at its largest, the memory given to running machines. */
|
|
171
|
+
const diskPromisedMb = all.reduce((sum, c) => sum + (c.diskGb ?? DISK_GB) * 1024, 0);
|
|
172
|
+
const memoryPromisedMb = machines.reduce((sum, m) => sum + (m.sizeMb ?? 0), 0);
|
|
173
|
+
const promised = {
|
|
174
|
+
disk: where ? { mb: diskPromisedMb, totalMb: where.sizeMb, over: diskPromisedMb > where.sizeMb } : null,
|
|
175
|
+
memory: memory ? { mb: memoryPromisedMb, totalMb: memory.totalMb, over: memoryPromisedMb > memory.totalMb } : null,
|
|
176
|
+
};
|
|
149
177
|
/* The same disk named once, with all it holds. */
|
|
150
178
|
const disks = [];
|
|
151
179
|
for (const p of room.places) {
|
|
@@ -155,9 +183,9 @@ export class Capacity {
|
|
|
155
183
|
const disk = where ? { sizeMb: where.sizeMb, freeMb: where.freeMb, usedPercent: percent(where.sizeMb - where.freeMb, where.sizeMb), disks: disks.map(({ device, ...d }) => d) } : null;
|
|
156
184
|
const high = {
|
|
157
185
|
disk: Boolean(disk) && disks.some((d) => d.usedPercent > HIGH_PERCENT || d.freeMb < HIGH_FACTOR * reserve.diskMb),
|
|
158
|
-
memory: Boolean(memory) && (memory.usedPercent > HIGH_PERCENT || memory.
|
|
186
|
+
memory: Boolean(memory) && (memory.usedPercent > HIGH_PERCENT || memory.freeMb < HIGH_FACTOR * reserve.memoryMb),
|
|
159
187
|
};
|
|
160
|
-
return { reserve, disk, memory, machines, high };
|
|
188
|
+
return { reserve, settings: set, disk, memory, machines, promised, high };
|
|
161
189
|
}
|
|
162
190
|
|
|
163
191
|
/* For Settings, This server: everything known. */
|
|
@@ -175,7 +203,7 @@ export class Capacity {
|
|
|
175
203
|
machines: { disksMb: Object.values(u.machines).reduce((a, b) => a + b, 0), each: Object.entries(u.machines).map(([name, mb]) => ({ name, mb })).sort((a, b) => b.mb - a.mb), shelfMb: u.shelf ?? null, imagesMb: u.images ?? null, othersMb: u.others ?? null },
|
|
176
204
|
at: new Date(this.#usageAt).toISOString(),
|
|
177
205
|
} : null;
|
|
178
|
-
return { known: Boolean(figures), reserve: this.reserve(), ...(figures ?? {}), usage, measuring: Boolean(this.#measuring) };
|
|
206
|
+
return { known: Boolean(figures), reserve: { diskMb: this.reserve().diskMb, memoryMb: this.reserve().memoryMb }, settings: this.reserve(), ...(figures ?? {}), usage, measuring: Boolean(this.#measuring) };
|
|
179
207
|
}
|
|
180
208
|
|
|
181
209
|
/* For the list: only whether it is high, and how much. Cheap: what is
|
|
@@ -191,7 +219,7 @@ export class Capacity {
|
|
|
191
219
|
return {
|
|
192
220
|
known: true, high: f.high.disk || f.high.memory,
|
|
193
221
|
disk: disk ? { usedPercent: disk.usedPercent, freeMb: disk.freeMb, aboveMb: disk.freeMb - r.diskMb, high: f.high.disk, level: level(f.high.disk, disk.usedPercent, disk.freeMb - r.diskMb, r.diskMb) } : null,
|
|
194
|
-
memory: f.memory ? { usedPercent: f.memory.usedPercent, freeMb: f.memory.
|
|
222
|
+
memory: f.memory ? { usedPercent: f.memory.usedPercent, freeMb: f.memory.freeMb, aboveMb: f.memory.freeMb - r.memoryMb, high: f.high.memory, level: level(f.high.memory, f.memory.usedPercent, f.memory.freeMb - r.memoryMb, r.memoryMb) } : null,
|
|
195
223
|
};
|
|
196
224
|
}
|
|
197
225
|
|
|
@@ -206,10 +234,10 @@ export class Capacity {
|
|
|
206
234
|
const r = f.reserve;
|
|
207
235
|
const verb = kind === 'create' ? 'Not created' : kind === 'start' ? 'Not started' : 'Not changed';
|
|
208
236
|
const who = name ? `${name}` : 'a new machine';
|
|
209
|
-
if (f.memory && memoryMb > 0 && f.memory.
|
|
237
|
+
if (f.memory && memoryMb > 0 && f.memory.freeMb - memoryMb < r.memoryMb) {
|
|
210
238
|
const asks = kind === 'grow' ? `${gb(memoryMb)} more memory for ${who}` : `${gb(memoryMb)} of memory for ${who}`;
|
|
211
239
|
const what = kind === 'create' ? 'Put a conversation you are not using to sleep, or choose less memory' : kind === 'grow' ? 'Put a conversation you are not using to sleep, or give it less' : 'Put a conversation you are not using to sleep';
|
|
212
|
-
throw new Refusal(409, `${verb}: this server cannot spare ${asks}. It has ${gb(f.memory.
|
|
240
|
+
throw new Refusal(409, `${verb}: this server cannot spare ${asks}. It has ${gb(f.memory.freeMb)} of memory free and keeps ${gb(r.memoryMb)} in reserve. ${what}, or lower the reserve in Settings, This server.`);
|
|
213
241
|
}
|
|
214
242
|
const takes = kind === 'create' ? (this.#shelf?.about?.().current?.sizeMb ?? NEW_MACHINE_MB) : diskMb;
|
|
215
243
|
if (f.disk && kind !== 'start' && takes > 0 && f.disk.freeMb - takes < r.diskMb) {
|
|
@@ -217,4 +245,40 @@ export class Capacity {
|
|
|
217
245
|
throw new Refusal(409, `${verb}: this server cannot spare ${asks}. It has ${gb(f.disk.freeMb)} of disk free and keeps ${gb(r.diskMb)} in reserve. Archive conversations you no longer need, or lower the reserve in Settings, This server.`);
|
|
218
246
|
}
|
|
219
247
|
}
|
|
248
|
+
|
|
249
|
+
/* WATCHED: what is free against the reserve, all the time. Under it, once
|
|
250
|
+
(again only after it came back above): the owner alerted, and the
|
|
251
|
+
running machine that takes the most put to sleep and told so in its
|
|
252
|
+
conversation. Disk: the one whose disk is largest (by the last
|
|
253
|
+
measure; memory held when none is known). Resolves with what was done. */
|
|
254
|
+
async watch() {
|
|
255
|
+
const f = this.#figures(await this.room({ fresh: true }).catch(() => null));
|
|
256
|
+
if (!f) return [];
|
|
257
|
+
const done = [];
|
|
258
|
+
const awake = new Set(this.#conversations.awake?.() ?? []);
|
|
259
|
+
for (const what of ['disk', 'memory']) {
|
|
260
|
+
const free = f[what]?.freeMb;
|
|
261
|
+
const short = free !== undefined && free < f.reserve[`${what}Mb`];
|
|
262
|
+
const was = this.#short[what];
|
|
263
|
+
this.#short[what] = short;
|
|
264
|
+
if (!short || was) continue;
|
|
265
|
+
const running = f.machines.filter((m) => m.name && awake.has(m.name));
|
|
266
|
+
const takes = (m) => (what === 'disk' ? this.#usage?.machines?.[m.name] ?? -1 : m.rssMb);
|
|
267
|
+
const most = running.sort((a, b) => takes(b) - takes(a) || b.rssMb - a.rssMb)[0] ?? null;
|
|
268
|
+
let slept = null;
|
|
269
|
+
if (most && this.#sleep) slept = await Promise.resolve(this.#sleep(most.name)).then(() => most.name, () => null);
|
|
270
|
+
const figures = `${gb(free)} free, ${gb(f.reserve[`${what}Mb`])} kept in reserve`;
|
|
271
|
+
this.#alert?.({ title: `This server is short of ${what}`, body: `${figures}.${slept ? ` ${slept} was put to sleep.` : ''}`, url: '/?open=settings&at=resources' });
|
|
272
|
+
if (slept) this.#say?.(slept, `This server ran short of ${what} (${figures}). This machine took the most, so it was put to sleep. Wake it once there is room again.`);
|
|
273
|
+
done.push({ what, slept });
|
|
274
|
+
}
|
|
275
|
+
return done;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/* Watches every `everyMs`; returns what stops it. */
|
|
279
|
+
start(everyMs = WATCH_MS) {
|
|
280
|
+
const timer = setInterval(() => { void this.watch().catch(() => {}); }, everyMs);
|
|
281
|
+
timer.unref?.();
|
|
282
|
+
return () => clearInterval(timer);
|
|
283
|
+
}
|
|
220
284
|
}
|
package/server/main.mjs
CHANGED
|
@@ -338,10 +338,14 @@ export async function startHome({ port, host, dataDir, mntDir = dataDir, gitRese
|
|
|
338
338
|
const stopUpdates = updates.watch();
|
|
339
339
|
const setup = new Setup({ store: new Store(`${dataDir}/flows.json`), version: platform.version, conversations: all, resources: folders, secrets, devices, shortcuts, wakeups, answerer, talk, gitKeeper, gitAccess, mailboxes, signin, network, guide, ...(setupPollMs ? { pollMs: setupPollMs } : {}) });
|
|
340
340
|
/* This server's disk and memory, and the reserve no machine may eat into: see capacity.mjs. */
|
|
341
|
-
const capacity = new Capacity({ run: asHost, store: new Store(`${dataDir}/server.json`), conversations: all, shelf, ...(serverRoomMs === undefined ? {} : { roomMs: serverRoomMs })
|
|
341
|
+
const capacity = new Capacity({ run: asHost, store: new Store(`${dataDir}/server.json`), conversations: all, shelf, ...(serverRoomMs === undefined ? {} : { roomMs: serverRoomMs }),
|
|
342
|
+
/* Under the reserve: the machine that takes the most put to sleep, the owner told, and its chat. */
|
|
343
|
+
sleep: (name) => all.sleep(name), alert: (payload) => { void alerts.tell(payload); },
|
|
344
|
+
say: (name, text) => { try { talk.event(name, { kind: 'machine-problem', text, tone: 'warn', topic: 'Machine' }); } catch { /* its chat may be off */ } } });
|
|
342
345
|
all.room = (ask) => capacity.check(ask);
|
|
346
|
+
const stopWatch = capacity.start();
|
|
343
347
|
const { server, listening } = serve(port, all, folders, { gate, originOk, shortcuts, voice, sharing, door, secrets, network, signin, mailboxes, gitReview, platform, answerer, wakeups, alerts, talk, gitAccess, gitKeeper, shelf, health, setup, guide, capacity, updates, repair: (name) => repairTerminal(name) });
|
|
344
|
-
server.on('close', () => { closing = true; stopShelf(); stopHealth(); stopUpdates(); clearInterval(mailTimer); clearInterval(answerTimer); clearInterval(wakeTimer); clearInterval(gitTimer); talk.stop(); });
|
|
348
|
+
server.on('close', () => { closing = true; stopShelf(); stopHealth(); stopUpdates(); stopWatch(); clearInterval(mailTimer); clearInterval(answerTimer); clearInterval(wakeTimer); clearInterval(gitTimer); talk.stop(); });
|
|
345
349
|
let closing = false;
|
|
346
350
|
bridge = attachBridge(server, {
|
|
347
351
|
onRejoin: (name) => all.rejoin(name),
|
|
@@ -397,8 +401,6 @@ function hostSetting(key) {
|
|
|
397
401
|
}
|
|
398
402
|
|
|
399
403
|
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
400
|
-
/* DELIBERATELY BROKEN, this version only: it proves an update that does not start is undone by itself. */
|
|
401
|
-
console.error('0.1.0-next.4 refuses to start, on purpose: the updater must go back by itself.'); process.exit(70);
|
|
402
404
|
const argv = process.argv.slice(2);
|
|
403
405
|
const port = Number(option(argv, 'port', '7717'));
|
|
404
406
|
const host = option(argv, 'host', '');
|
package/server/updates.mjs
CHANGED
|
@@ -107,7 +107,7 @@ export class Updates {
|
|
|
107
107
|
if (this.#store.get('told')?.key === key) return;
|
|
108
108
|
this.#store.put({ name: 'told', key });
|
|
109
109
|
const title = last.result === 'rolled-back' ? `The update to ${last.to} did not work: back on ${last.from}` : `${last.action === 'back' ? 'Going back to' : 'The update to'} ${last.to} failed`;
|
|
110
|
-
void this.#alerts?.tell({ title, body: String(last.why ?? '').slice(0, 180), url: '/?open=settings' })?.catch?.(() => {});
|
|
110
|
+
void this.#alerts?.tell({ title, body: String(last.why ?? '').slice(0, 180), url: '/?open=settings&at=updates' })?.catch?.(() => {});
|
|
111
111
|
};
|
|
112
112
|
look();
|
|
113
113
|
this.#timer = setInterval(look, everyMs); this.#timer.unref?.();
|
package/web/app.js
CHANGED
|
@@ -1193,9 +1193,9 @@
|
|
|
1193
1193
|
for (const b of $('network').children) b.setAttribute('aria-pressed', String((b.dataset.online === 'true') !== offline));
|
|
1194
1194
|
await paintSignIn();
|
|
1195
1195
|
}
|
|
1196
|
-
/* ----------
|
|
1197
|
-
/*
|
|
1198
|
-
const OPEN_AT_FIRST = {
|
|
1196
|
+
/* ---------- chapters (a conversation's settings, and Settings): folded or not, kept on this device ---------- */
|
|
1197
|
+
/* All folded at first, as a project's settings: what the person opens is kept. */
|
|
1198
|
+
const OPEN_AT_FIRST = {};
|
|
1199
1199
|
let chapters = {};
|
|
1200
1200
|
try { chapters = JSON.parse(localStorage.getItem('clavarium-chapters') ?? '{}') ?? {}; } catch { /* a private window */ }
|
|
1201
1201
|
for (const chapter of document.querySelectorAll('details.chapter')) {
|
|
@@ -1206,7 +1206,19 @@
|
|
|
1206
1206
|
try { localStorage.setItem('clavarium-chapters', JSON.stringify(chapters)); } catch { /* as above */ }
|
|
1207
1207
|
});
|
|
1208
1208
|
}
|
|
1209
|
-
|
|
1209
|
+
/* Reached by a link (or a button that leads to it): opened, and said for a moment. */
|
|
1210
|
+
let reachedTimer;
|
|
1211
|
+
const openChapter = (key, { show = true } = {}) => {
|
|
1212
|
+
const chapter = document.querySelector(`details.chapter[data-chapter="${key}"]`);
|
|
1213
|
+
if (!chapter) return chapter;
|
|
1214
|
+
chapter.open = true;
|
|
1215
|
+
if (show) {
|
|
1216
|
+
for (const other of document.querySelectorAll('details.chapter.reached')) other.classList.remove('reached');
|
|
1217
|
+
chapter.classList.add('reached');
|
|
1218
|
+
clearTimeout(reachedTimer); reachedTimer = setTimeout(() => chapter.classList.remove('reached'), 2000);
|
|
1219
|
+
}
|
|
1220
|
+
return chapter;
|
|
1221
|
+
};
|
|
1210
1222
|
|
|
1211
1223
|
/* ---------- the machine: its health, its size, a restart ---------- */
|
|
1212
1224
|
const sizeWords = (mb) => (mb >= 1024 ? `${Number.isInteger(mb / 1024) ? mb / 1024 : (mb / 1024).toFixed(1)} GB` : `${mb} MB`);
|
|
@@ -2371,7 +2383,13 @@
|
|
|
2371
2383
|
const wanted = q.get('c') ?? '';
|
|
2372
2384
|
if (home && wanted !== current && conversations.some((c) => c.name === wanted)) select(wanted);
|
|
2373
2385
|
const open = q.get('open') ?? '';
|
|
2374
|
-
if (open === 'settings') {
|
|
2386
|
+
if (open === 'settings') {
|
|
2387
|
+
await openSettings({ push: false });
|
|
2388
|
+
/* A link to one chapter of Settings (?open=settings&at=resources): that one opened, and shown. */
|
|
2389
|
+
const at = q.get('at') ?? '';
|
|
2390
|
+
if (/^[a-z]{1,20}$/.test(at)) openChapter(`s-${at}`)?.scrollIntoView({ block: 'start' });
|
|
2391
|
+
return;
|
|
2392
|
+
}
|
|
2375
2393
|
const former = FORMER[open === 'access' ? (q.get('tab') ?? 'map') : open];
|
|
2376
2394
|
if (former) { await openAccessPage(former[0], { mode: former[1], of: q.get('for') ?? '', push: false }); return; }
|
|
2377
2395
|
leavePlace({ fromHistory: true });
|
|
@@ -2436,11 +2454,13 @@
|
|
|
2436
2454
|
};
|
|
2437
2455
|
const when = (iso) => new Date(iso).toLocaleString([], { day: 'numeric', month: 'short', hour: '2-digit', minute: '2-digit' });
|
|
2438
2456
|
function updatesError(text) { $('updates-error').textContent = text ?? ''; $('updates-error').hidden = !text; }
|
|
2457
|
+
let lastUpdates = null;
|
|
2439
2458
|
async function paintUpdates() {
|
|
2440
2459
|
clearTimeout(updatesTimer);
|
|
2441
2460
|
const u = await fetch(`${API}/updates`).then((r) => (r.ok ? r.json() : null)).catch(() => null);
|
|
2442
2461
|
/* The server restarts during an update: asked again until it answers. */
|
|
2443
2462
|
if (!u) { if (updatesBusy && !$('settings').hidden) updatesTimer = setTimeout(() => { void paintUpdates(); }, 3000); return; }
|
|
2463
|
+
lastUpdates = u;
|
|
2444
2464
|
$('updates-version').textContent = `Clavarium ${u.version ?? ''}`;
|
|
2445
2465
|
for (const id of ['updates-check', 'updates-settings', 'updates-back']) $(id).hidden = !u.installed;
|
|
2446
2466
|
if (!u.installed) {
|
|
@@ -2456,12 +2476,22 @@
|
|
|
2456
2476
|
: u.checkedAt ? `Up to date · checked ${sinceWords(u.checkedAt)}` : 'Up to date';
|
|
2457
2477
|
$('updates-check').disabled = Boolean(running);
|
|
2458
2478
|
$('updates-offer').hidden = !u.available || Boolean(running);
|
|
2459
|
-
|
|
2460
|
-
|
|
2479
|
+
/* A version that already failed here is not offered as usual: tried again only on purpose, warned. */
|
|
2480
|
+
const failedHere = Boolean(u.available) && u.history.some((h) => h.action === 'update' && h.to === u.available && h.result !== 'done');
|
|
2481
|
+
const apply = $('updates-apply');
|
|
2482
|
+
apply.dataset.version = u.available ?? '';
|
|
2483
|
+
apply.dataset.retry = String(failedHere);
|
|
2484
|
+
apply.className = failedHere ? '' : 'primary';
|
|
2485
|
+
apply.textContent = failedHere ? `Try ${u.available} again\u2026` : `Update to ${u.available ?? ''}`;
|
|
2486
|
+
$('updates-retry-note').textContent = failedHere ? `It did not start here before. If it fails again, ${u.version} is put back.` : '';
|
|
2487
|
+
$('updates-retry-note').hidden = !failedHere;
|
|
2488
|
+
$('updates-retry-yes').hidden = true; $('updates-retry-no').hidden = true; apply.hidden = false;
|
|
2489
|
+
/* The last thing that went wrong, said once at the top, in words: a version put
|
|
2490
|
+
back runs normally -- only the new one did not start, here. */
|
|
2461
2491
|
const last = u.history[0];
|
|
2462
|
-
const
|
|
2463
|
-
const news = !running && last && last.result !== 'done' ? (last.result === 'rolled-back' ?
|
|
2464
|
-
: !running && u.refused && (!last || u.refused.at > last.at) ? `Not done: ${u.refused.why}` : '';
|
|
2492
|
+
const backFailed = /did not come back either/.test(last?.why ?? '');
|
|
2493
|
+
const news = !running && last && last.result !== 'done' ? (last.result === 'rolled-back' ? (backFailed ? `${last.to} did not start on this server, and putting ${last.from} back did not work either: ${last.why}` : `${last.to} did not start on this server, so ${last.from} was put back. ${last.from} runs normally.`) : `${last.action === 'back' ? 'Going back to' : 'The update to'} ${last.to} failed: ${last.why ?? ''}`)
|
|
2494
|
+
: !running && u.refused && (!last || u.refused.at > last.at) && !/is not newer than/.test(u.refused.why ?? '') ? `Not done: ${u.refused.why}` : '';
|
|
2465
2495
|
$('updates-news').textContent = news.trim(); $('updates-news').hidden = !news;
|
|
2466
2496
|
if (![$('updates-mode'), $('updates-from'), $('updates-to'), $('updates-channel')].includes(document.activeElement)) {
|
|
2467
2497
|
$('updates-mode').value = u.mode; $('updates-channel').value = u.channel;
|
|
@@ -2500,17 +2530,30 @@
|
|
|
2500
2530
|
if (running && !$('settings').hidden) updatesTimer = setTimeout(() => { void paintUpdates(); }, 3000);
|
|
2501
2531
|
}
|
|
2502
2532
|
$('updates-check').addEventListener('click', async () => {
|
|
2503
|
-
updatesError(); $('updates-check').disabled = true; $('updates-state').textContent = 'Checking...';
|
|
2504
|
-
|
|
2505
|
-
|
|
2533
|
+
updatesError(); $('updates-check').disabled = true; $('updates-check').textContent = 'Checking...'; $('updates-state').textContent = 'Checking...';
|
|
2534
|
+
let failed = false;
|
|
2535
|
+
try { await post(`${API}/updates/check`); } catch (error) { failed = true; updatesError(`Could not ask npm: ${error.message}. Try again later.`); }
|
|
2536
|
+
$('updates-check').disabled = false; $('updates-check').textContent = 'Check now';
|
|
2506
2537
|
await paintUpdates();
|
|
2538
|
+
/* The answer to the click, said next to it. */
|
|
2539
|
+
const u = lastUpdates;
|
|
2540
|
+
if (!failed && u && !u.checkError) { $('updates-news').textContent = u.available ? `${u.available} is out.` : `No new version: ${u.version} is the latest.`; $('updates-news').hidden = false; }
|
|
2507
2541
|
});
|
|
2508
|
-
|
|
2542
|
+
async function applyUpdate() {
|
|
2509
2543
|
updatesError();
|
|
2510
|
-
const version = $('updates-apply').
|
|
2544
|
+
const version = $('updates-apply').dataset.version;
|
|
2511
2545
|
try { await post(`${API}/updates/apply`, { version }); updatesBusy = true; } catch (error) { updatesError(error.message); }
|
|
2512
2546
|
await paintUpdates();
|
|
2547
|
+
}
|
|
2548
|
+
$('updates-apply').addEventListener('click', () => {
|
|
2549
|
+
if ($('updates-apply').dataset.retry !== 'true') { void applyUpdate(); return; }
|
|
2550
|
+
/* Tried again: asked once more, in place. */
|
|
2551
|
+
$('updates-apply').hidden = true;
|
|
2552
|
+
$('updates-retry-note').textContent = `The server restarts to try ${$('updates-apply').dataset.version} again.`;
|
|
2553
|
+
$('updates-retry-yes').hidden = false; $('updates-retry-no').hidden = false; $('updates-retry-yes').focus();
|
|
2513
2554
|
});
|
|
2555
|
+
$('updates-retry-yes').addEventListener('click', () => { void applyUpdate(); });
|
|
2556
|
+
$('updates-retry-no').addEventListener('click', () => { void paintUpdates(); });
|
|
2514
2557
|
async function saveUpdates(change) {
|
|
2515
2558
|
updatesError(); $('updates-saved').textContent = '';
|
|
2516
2559
|
try { await post(`${API}/updates/settings`, change); $('updates-saved').textContent = 'Saved.'; } catch (error) { updatesError(error.message); }
|
|
@@ -2600,22 +2643,35 @@
|
|
|
2600
2643
|
const r = await fetch(`${API}/server`).then((x) => (x.ok ? x.json() : null)).catch(() => null);
|
|
2601
2644
|
$('settings-server-top').hidden = !r; $('settings-server').hidden = !r;
|
|
2602
2645
|
if (!r) return;
|
|
2603
|
-
|
|
2604
|
-
|
|
2646
|
+
/* The two settings of each: a fixed amount and a share; the larger is kept free. */
|
|
2647
|
+
const set = r.settings ?? { ...r.reserve, diskPercent: 0, memoryPercent: 0 };
|
|
2648
|
+
const editing = ['reserve-disk', 'reserve-disk-percent', 'reserve-memory', 'reserve-memory-percent'].some((id) => $(id) === document.activeElement);
|
|
2649
|
+
if (!editing) {
|
|
2650
|
+
$('reserve-disk').value = String(Math.round((set.diskMb / 1024) * 100) / 100); $('reserve-memory').value = String(Math.round((set.memoryMb / 1024) * 100) / 100);
|
|
2651
|
+
$('reserve-disk-percent').value = String(set.diskPercent); $('reserve-memory-percent').value = String(set.memoryPercent);
|
|
2652
|
+
}
|
|
2653
|
+
$('reserve-now').textContent = r.known ? `Kept free now: ${GB(r.reserve.diskMb)} of disk, ${GB(r.reserve.memoryMb)} of memory.` : '';
|
|
2654
|
+
/* What was promised, only said: never refused for it. */
|
|
2655
|
+
const promised = (line, p, words) => {
|
|
2656
|
+
line.hidden = !p; if (!p) return;
|
|
2657
|
+
line.dataset.over = String(p.over);
|
|
2658
|
+
line.textContent = `${words(GB(p.mb), GB(p.totalMb))}${p.over ? ' That is more than there is: fine while they do not all fill up.' : ''}`;
|
|
2659
|
+
};
|
|
2660
|
+
promised($('promised-disk'), r.promised?.disk, (mb, total) => `Disks promised to machines, at their largest: ${mb} of ${total}.`);
|
|
2661
|
+
promised($('promised-memory'), r.promised?.memory, (mb, total) => `Memory given to running machines: ${mb} of ${total}.`);
|
|
2605
2662
|
$('server-unknown').hidden = r.known;
|
|
2606
2663
|
$('server-disks').hidden = !r.disk; $('server-memory').hidden = !r.memory;
|
|
2607
2664
|
if (r.disk) paintDisks(r);
|
|
2608
2665
|
if (r.memory) {
|
|
2609
2666
|
const mem = r.memory;
|
|
2610
|
-
$('server-memory-figures').textContent = `${GB(mem.
|
|
2611
|
-
meter($('server-memory-meter'), mem.usedPercent, r.reserve.memoryMb, mem.totalMb, levelOf(r.high.memory, mem.usedPercent, mem.
|
|
2667
|
+
$('server-memory-figures').textContent = `${GB(mem.freeMb)} free of ${GB(mem.totalMb)}`;
|
|
2668
|
+
meter($('server-memory-meter'), mem.usedPercent, r.reserve.memoryMb, mem.totalMb, levelOf(r.high.memory, mem.usedPercent, mem.freeMb - r.reserve.memoryMb, r.reserve.memoryMb));
|
|
2612
2669
|
$('server-memory-state').textContent = 'Short'; $('server-memory-state').hidden = !r.high.memory;
|
|
2613
|
-
$('server-memory-note').textContent = aboveWords(mem.
|
|
2670
|
+
$('server-memory-note').textContent = aboveWords(mem.freeMb - r.reserve.memoryMb, r.reserve.memoryMb);
|
|
2614
2671
|
const held = r.machines.reduce((a, x) => a + x.rssMb, 0);
|
|
2615
2672
|
parts($('server-memory-parts'), [
|
|
2616
2673
|
[`Running machines (${r.machines.length})`, held],
|
|
2617
2674
|
...r.machines.map((x) => [x.shelf ? 'ready-made machine (being made)' : (x.name ?? 'another machine'), x.sizeMb ? `uses ${GB(x.rssMb)} · given ${GB(x.sizeMb)}` : `uses ${GB(x.rssMb)}`, true]),
|
|
2618
|
-
...(mem.promisedMb > 0 ? [['given to them, not used yet', mem.promisedMb, true]] : []),
|
|
2619
2675
|
['The system and other programs', Math.max(0, mem.totalMb - mem.freeMb - held)],
|
|
2620
2676
|
]);
|
|
2621
2677
|
}
|
|
@@ -2625,9 +2681,12 @@
|
|
|
2625
2681
|
$('reserve-save').addEventListener('click', async () => {
|
|
2626
2682
|
$('reserve-error').hidden = true; $('reserve-saved').textContent = '';
|
|
2627
2683
|
const mb = (id) => { const v = Number.parseFloat($(id).value.replace(',', '.')); return Number.isFinite(v) ? Math.round(v * 1024) : NaN; };
|
|
2684
|
+
const share = (id) => { const v = Number($(id).value.trim()); return Number.isInteger(v) && v >= 0 && v <= 100 ? v : NaN; };
|
|
2628
2685
|
const diskMb = mb('reserve-disk'); const memoryMb = mb('reserve-memory');
|
|
2686
|
+
const diskPercent = share('reserve-disk-percent'); const memoryPercent = share('reserve-memory-percent');
|
|
2629
2687
|
if (!(diskMb >= 0) || !(memoryMb >= 0)) { $('reserve-error').textContent = 'Write a number of gigabytes, 0 or more.'; $('reserve-error').hidden = false; return; }
|
|
2630
|
-
|
|
2688
|
+
if (Number.isNaN(diskPercent) || Number.isNaN(memoryPercent)) { $('reserve-error').textContent = 'Write a whole percent, from 0 to 100.'; $('reserve-error').hidden = false; return; }
|
|
2689
|
+
try { await post(`${API}/server/reserve`, { diskMb, diskPercent, memoryMb, memoryPercent }); $('reserve-saved').textContent = 'Saved.'; } catch (error) { $('reserve-error').textContent = error.message; $('reserve-error').hidden = false; return; }
|
|
2631
2690
|
await paintServer(); void paintGauge();
|
|
2632
2691
|
});
|
|
2633
2692
|
/* THE SMALL GAUGE at the bottom of the list: only when disk or memory is
|
|
@@ -2657,7 +2716,7 @@
|
|
|
2657
2716
|
}
|
|
2658
2717
|
$('server-gauge').addEventListener('click', async () => {
|
|
2659
2718
|
if (place !== 'settings') await openSettings(); else await paintServer();
|
|
2660
|
-
const top =
|
|
2719
|
+
const top = openChapter('s-resources');
|
|
2661
2720
|
top.scrollIntoView({ block: 'start' });
|
|
2662
2721
|
$('settings-server').focus({ preventScroll: true });
|
|
2663
2722
|
});
|
package/web/index.html
CHANGED
|
@@ -505,17 +505,33 @@
|
|
|
505
505
|
<!-- SETTINGS: the platform, and this device. -->
|
|
506
506
|
<div id="settings" class="sheet" hidden>
|
|
507
507
|
<div class="panel-top"><b>Settings</b><button type="button" id="settings-close">Close</button></div>
|
|
508
|
-
<
|
|
508
|
+
<h3 class="chapter-group">This device</h3>
|
|
509
|
+
<details class="chapter" id="settings-ch-screen" data-chapter="s-screen">
|
|
510
|
+
<summary><span class="chapter-title">Screen</span><span class="chapter-about">Light or dark, text size, the app.</span></summary>
|
|
511
|
+
<div class="chapter-body">
|
|
509
512
|
<div class="row"><span>Screen</span><button type="button" id="theme" aria-label="Light or dark screen">Light</button><button type="button" id="install" class="primary" hidden>Install the app</button></div>
|
|
510
513
|
<div class="row"><span>Text size</span><span class="text-size" id="settings-text" role="group" aria-label="Text size"><button type="button" data-text="smaller" aria-label="Smaller text">A−</button><button type="button" data-text="normal" aria-label="Normal text size" class="text-now">100%</button><button type="button" data-text="larger" aria-label="Larger text">A+</button></span></div>
|
|
511
514
|
<p class="hint">All text, the terminal's too, on this device only. Also: two fingers pinched, Ctrl and the wheel, Ctrl + and Ctrl -.</p>
|
|
512
|
-
|
|
515
|
+
</div>
|
|
516
|
+
</details>
|
|
517
|
+
<details class="chapter" id="settings-ch-alerts" data-chapter="s-alerts">
|
|
518
|
+
<summary><span class="chapter-title">Alerts</span><span class="chapter-about">Be told here when a tool waits for you.</span></summary>
|
|
519
|
+
<div class="chapter-body">
|
|
513
520
|
<p class="hint" id="alerts-state">When a tool stops and waits for you, this device can be told.</p>
|
|
514
521
|
<div class="row"><button type="button" id="alerts-on" class="primary">Alert me on this device</button><button type="button" id="alerts-off" hidden>Stop alerting this device</button><button type="button" id="alerts-test" hidden>Send a test</button></div>
|
|
515
|
-
|
|
522
|
+
</div>
|
|
523
|
+
</details>
|
|
524
|
+
<h3 class="chapter-group">This server</h3>
|
|
525
|
+
<details class="chapter" id="settings-ch-shortcuts" data-chapter="s-shortcuts">
|
|
526
|
+
<summary><span class="chapter-title">Shortcuts</span><span class="chapter-about">The ready-made answers under the terminal.</span></summary>
|
|
527
|
+
<div class="chapter-body">
|
|
516
528
|
<p class="hint">The ready-made answers under the terminal. They are added there ("+ Shortcut"); they are removed here.</p>
|
|
517
529
|
<div id="settings-shortcuts"></div>
|
|
518
|
-
|
|
530
|
+
</div>
|
|
531
|
+
</details>
|
|
532
|
+
<details class="chapter" id="settings-ch-words" data-chapter="s-words">
|
|
533
|
+
<summary><span class="chapter-title">Chat screen</span><span class="chapter-about">Its language, and your own words.</span></summary>
|
|
534
|
+
<div class="chapter-body">
|
|
519
535
|
<div class="row"><label for="words-lang">Language</label><select id="words-lang"><option value="device">The language of each device</option></select></div>
|
|
520
536
|
<details id="words-box">
|
|
521
537
|
<summary>Your own words</summary>
|
|
@@ -527,18 +543,34 @@
|
|
|
527
543
|
</details>
|
|
528
544
|
<!-- WHAT THE THREAD SHOWS of what this server says there: each kind of
|
|
529
545
|
event at a level (server/talk.mjs EVENT_LEVELS); the owner may move one. -->
|
|
530
|
-
|
|
546
|
+
</div>
|
|
547
|
+
</details>
|
|
548
|
+
<details class="chapter" id="settings-ch-thread" data-chapter="s-thread">
|
|
549
|
+
<summary><span class="chapter-title">Chat thread</span><span class="chapter-about">What the chat says of machines and repositories.</span></summary>
|
|
550
|
+
<div class="chapter-body">
|
|
531
551
|
<p class="hint">Besides the messages, the chat says what happened to its machine and its repositories. <b>Needs you</b>: in both views. <b>News</b>: in the simple view as one short line, several in a row grouped. <b>Trace</b>: in the detailed view only. Never counted as unread.</p>
|
|
532
552
|
<div id="thread-levels" class="thread-levels"></div>
|
|
533
553
|
<p class="hint" id="thread-saved" role="status"></p>
|
|
534
554
|
<p id="thread-error" class="error" role="alert" hidden></p>
|
|
535
|
-
|
|
555
|
+
</div>
|
|
556
|
+
</details>
|
|
557
|
+
<details class="chapter" id="settings-ch-dictation" data-chapter="s-dictation">
|
|
558
|
+
<summary><span class="chapter-title">Dictation</span><span class="chapter-about">Speech to text, on this server.</span></summary>
|
|
559
|
+
<div class="chapter-body">
|
|
536
560
|
<p id="settings-dictation" class="hint"></p>
|
|
537
|
-
|
|
561
|
+
</div>
|
|
562
|
+
</details>
|
|
563
|
+
<details class="chapter" id="settings-ch-people" data-chapter="s-people">
|
|
564
|
+
<summary><span class="chapter-title">People & address</span><span class="chapter-about">Who may sign in, and where.</span></summary>
|
|
565
|
+
<div class="chapter-body">
|
|
538
566
|
<p id="settings-people" class="hint"></p>
|
|
539
567
|
<!-- THIS SERVER: its disk and memory, what takes them, and the reserve
|
|
540
568
|
no machine may eat into (server/capacity.mjs). Nothing here wakes a machine. -->
|
|
541
|
-
|
|
569
|
+
</div>
|
|
570
|
+
</details>
|
|
571
|
+
<details class="chapter" id="settings-server-top" data-chapter="s-resources">
|
|
572
|
+
<summary><span class="chapter-title">Resources</span><span class="chapter-about">Disk and memory, what takes them, the reserve.</span></summary>
|
|
573
|
+
<div class="chapter-body">
|
|
542
574
|
<div id="settings-server" tabindex="-1">
|
|
543
575
|
<p id="server-unknown" class="hint" hidden>This server's disk and memory cannot be read just now. Nothing is refused for lack of room while they cannot.</p>
|
|
544
576
|
<div id="server-disks"></div>
|
|
@@ -549,24 +581,41 @@
|
|
|
549
581
|
<dl id="server-memory-parts" class="server-parts"></dl>
|
|
550
582
|
</div>
|
|
551
583
|
<div class="server-block">
|
|
552
|
-
<div class="server-head"><b>Reserve</b><span class="server-figures">always left free</span></div>
|
|
584
|
+
<div class="server-head"><b>Reserve</b><span class="server-figures">always left free, the larger of the two</span></div>
|
|
585
|
+
<div class="reserve-row">
|
|
586
|
+
<span class="reserve-what">Disk</span>
|
|
587
|
+
<label><input type="number" id="reserve-disk" min="0" max="1024" step="0.5" inputmode="decimal" aria-label="Disk reserve, in GB"> GB</label>
|
|
588
|
+
<span aria-hidden="true">or</span>
|
|
589
|
+
<label><input type="number" id="reserve-disk-percent" min="0" max="100" step="1" inputmode="numeric" aria-label="Disk reserve, in percent of the disk"> %</label>
|
|
590
|
+
</div>
|
|
591
|
+
<div class="reserve-row">
|
|
592
|
+
<span class="reserve-what">Memory</span>
|
|
593
|
+
<label><input type="number" id="reserve-memory" min="0" max="256" step="0.25" inputmode="decimal" aria-label="Memory reserve, in GB"> GB</label>
|
|
594
|
+
<span aria-hidden="true">or</span>
|
|
595
|
+
<label><input type="number" id="reserve-memory-percent" min="0" max="100" step="1" inputmode="numeric" aria-label="Memory reserve, in percent of the memory"> %</label>
|
|
596
|
+
</div>
|
|
553
597
|
<div class="reserve-row">
|
|
554
|
-
<label>Disk <input type="number" id="reserve-disk" min="0" max="1024" step="0.5" inputmode="decimal"> GB</label>
|
|
555
|
-
<label>Memory <input type="number" id="reserve-memory" min="0" max="256" step="0.25" inputmode="decimal"> GB</label>
|
|
556
598
|
<button type="button" id="reserve-save" class="primary">Save</button>
|
|
557
599
|
<span class="hint" id="reserve-saved" role="status"></span>
|
|
558
600
|
</div>
|
|
559
|
-
<p class="
|
|
601
|
+
<p class="server-note" id="reserve-now"></p>
|
|
602
|
+
<p class="hint">Judged on what is really free. A machine is not created, started or given more if that would leave less. Below it, you are alerted and the running machine that takes the most is put to sleep. Waking is never refused for lack of disk, so you can get in to clean up.</p>
|
|
603
|
+
<p class="server-note promised" id="promised-disk" hidden></p>
|
|
604
|
+
<p class="server-note promised" id="promised-memory" hidden></p>
|
|
560
605
|
<p id="reserve-error" class="error" role="alert" hidden></p>
|
|
561
606
|
</div>
|
|
562
607
|
</div>
|
|
563
608
|
<!-- UPDATES: what runs, what is out, how this server updates itself
|
|
564
609
|
(server/updates.mjs; the updater, as root: deploy/updater.mjs). -->
|
|
565
|
-
|
|
610
|
+
</div>
|
|
611
|
+
</details>
|
|
612
|
+
<details class="chapter" id="settings-updates-top" data-chapter="s-updates">
|
|
613
|
+
<summary><span class="chapter-title">Updates</span><span class="chapter-about">The version that runs, and how it updates.</span></summary>
|
|
614
|
+
<div class="chapter-body">
|
|
566
615
|
<div id="updates" tabindex="-1">
|
|
567
616
|
<div class="updates-head"><b id="updates-version"></b><span id="updates-state" role="status"></span><button type="button" id="updates-check">Check now</button></div>
|
|
568
617
|
<p id="updates-news" class="updates-news" hidden></p>
|
|
569
|
-
<div class="row" id="updates-offer" hidden><button type="button" id="updates-apply" class="primary"></button></div>
|
|
618
|
+
<div class="row" id="updates-offer" hidden><button type="button" id="updates-apply" class="primary"></button><button type="button" id="updates-retry-yes" class="primary" hidden>Try again</button><button type="button" id="updates-retry-no" hidden>Cancel</button><span class="hint" id="updates-retry-note" hidden></span></div>
|
|
570
619
|
<div id="updates-settings" class="updates-grid">
|
|
571
620
|
<label for="updates-mode">Mode</label>
|
|
572
621
|
<select id="updates-mode"><option value="auto">Automatic, in a window</option><option value="asap">As soon as out</option><option value="ask">Ask me</option><option value="off">Off</option></select>
|
|
@@ -580,10 +629,17 @@
|
|
|
580
629
|
<details id="updates-history-box" hidden><summary>History</summary><ul id="updates-history" class="updates-history"></ul></details>
|
|
581
630
|
<p id="updates-error" class="error" role="alert" hidden></p>
|
|
582
631
|
</div>
|
|
583
|
-
|
|
632
|
+
</div>
|
|
633
|
+
</details>
|
|
634
|
+
<details class="chapter" id="settings-ch-backup" data-chapter="s-backup">
|
|
635
|
+
<summary><span class="chapter-title">Storage & backup</span><span class="chapter-about">Disk sizes, and how to back up.</span></summary>
|
|
636
|
+
<div class="chapter-body">
|
|
584
637
|
<p class="hint">Each conversation works in a disk of a fixed size (10 GB unless the installation says otherwise). A backup is made on the machine itself, as two files -- the archive, and the key of the vault apart: <code>sudo deploy/backup.sh --to /a/folder</code>.</p>
|
|
585
|
-
|
|
586
|
-
|
|
638
|
+
</div>
|
|
639
|
+
</details>
|
|
640
|
+
<details class="chapter" id="settings-ch-advanced" data-chapter="s-advanced">
|
|
641
|
+
<summary><span class="chapter-title">Advanced</span><span class="chapter-about">Ready-made machine, git server keys.</span></summary>
|
|
642
|
+
<div class="chapter-body" id="settings-advanced">
|
|
587
643
|
<div id="shelf">
|
|
588
644
|
<b>Ready-made machine</b>
|
|
589
645
|
<p class="hint">New conversations start from a copy of a machine prepared in advance, with the tools already installed: a minute or two instead of several. It is rebuilt each month in the background; running conversations are not touched.</p>
|
|
@@ -598,9 +654,14 @@
|
|
|
598
654
|
<div id="git-hosts"></div>
|
|
599
655
|
<p id="git-hosts-error" class="error" role="alert" hidden></p>
|
|
600
656
|
</div>
|
|
657
|
+
</div>
|
|
601
658
|
</details>
|
|
602
|
-
<
|
|
659
|
+
<details class="chapter" id="settings-ch-about" data-chapter="s-about">
|
|
660
|
+
<summary><span class="chapter-title">About</span><span class="chapter-about">The version that runs here.</span></summary>
|
|
661
|
+
<div class="chapter-body">
|
|
603
662
|
<p id="settings-about" class="hint"></p>
|
|
663
|
+
</div>
|
|
664
|
+
</details>
|
|
604
665
|
</div>
|
|
605
666
|
<div id="copy" class="sheet" hidden>
|
|
606
667
|
<div class="panel-top"><b>Copy from the screen</b><button type="button" id="copy-close">Close</button></div>
|
package/web/style.css
CHANGED
|
@@ -228,9 +228,9 @@ footer button.careful { border-style: dashed; color: var(--ink2); }
|
|
|
228
228
|
#sharing-what { display: flex; flex-direction: column; }
|
|
229
229
|
|
|
230
230
|
/* What can be unfolded looks like it can. */
|
|
231
|
-
#panel details > summary { list-style: none; cursor: pointer; padding: 8px 12px; border: 1px solid var(--line); border-radius: 8px; background: var(--button, #222c38); font-weight: 600; }
|
|
232
|
-
#panel details > summary::before { content: '+ '; }
|
|
233
|
-
#panel details[open] > summary::before { content: '- '; }
|
|
231
|
+
#panel details:not(.chapter) > summary { list-style: none; cursor: pointer; padding: 8px 12px; border: 1px solid var(--line); border-radius: 8px; background: var(--button, #222c38); font-weight: 600; }
|
|
232
|
+
#panel details:not(.chapter) > summary::before { content: '+ '; }
|
|
233
|
+
#panel details[open]:not(.chapter) > summary::before { content: '- '; }
|
|
234
234
|
#sharing-label { min-height: 40px; font: .875rem var(--sans); color: var(--ink); background: var(--bg); border: 1px solid var(--line); border-radius: 7px; padding: 7px 9px; }
|
|
235
235
|
#sharing label { font-size: .8125rem; }
|
|
236
236
|
|
|
@@ -308,10 +308,10 @@ footer button.careful { border-style: dashed; color: var(--ink2); }
|
|
|
308
308
|
@media (prefers-reduced-motion: reduce) { .asset-head b::before { transition: none; } }
|
|
309
309
|
.asset-inside { flex-basis: 100%; padding: 0 0 8px 12px; display: flex; flex-direction: column; gap: 4px; }
|
|
310
310
|
@media (max-width: 700px) { .asset-head { grid-template-columns: 1fr; gap: 2px; } }
|
|
311
|
-
#assets details > summary, #access-advanced > summary
|
|
312
|
-
#assets details > summary::before, #access-advanced > summary::before
|
|
313
|
-
#assets details[open] > summary::before, #access-advanced[open] > summary::before
|
|
314
|
-
#assets details form, #access-advanced > div
|
|
311
|
+
#assets details > summary, #access-advanced > summary { cursor: pointer; padding: 8px 12px; border: 1px solid var(--line); border-radius: 8px; background: var(--button, #222c38); font-weight: 600; list-style: none; align-self: flex-start; }
|
|
312
|
+
#assets details > summary::before, #access-advanced > summary::before { content: '+ '; }
|
|
313
|
+
#assets details[open] > summary::before, #access-advanced[open] > summary::before { content: '- '; }
|
|
314
|
+
#assets details form, #access-advanced > div { margin-top: 8px; }
|
|
315
315
|
#attach-picker { border: 1px solid var(--brass); border-radius: 10px; padding: 10px 12px; display: flex; flex-direction: column; gap: 6px; }
|
|
316
316
|
#chips { cursor: pointer; }
|
|
317
317
|
#settings .row { align-items: center; gap: 10px; }
|
|
@@ -398,17 +398,21 @@ textarea.public-key { flex: 1 1 260px; min-width: 0; font: .75rem ui-monospace,
|
|
|
398
398
|
#gitview #gitview-guards ul { margin: 4px 0 8px; padding-left: 20px; }
|
|
399
399
|
#gitview #gitview-guards li { overflow-wrap: anywhere; }
|
|
400
400
|
|
|
401
|
-
/* ----------
|
|
402
|
-
#panel { gap: 12px; }
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
401
|
+
/* ---------- chapters that fold, as a project's settings (a conversation's, and Settings) ---------- */
|
|
402
|
+
#panel, #settings { gap: 12px; }
|
|
403
|
+
details.chapter { flex-shrink: 0; border: 1px solid var(--line); border-radius: 10px; background: var(--bar); }
|
|
404
|
+
details.chapter > summary { list-style: none; cursor: pointer; display: flex; flex-wrap: wrap; align-items: baseline; gap: 2px 12px; padding: 12px 14px; border: 0; background: none; border-radius: 10px; min-height: 48px; }
|
|
405
|
+
details.chapter > summary::-webkit-details-marker { display: none; }
|
|
406
|
+
details.chapter > summary::before { content: none; }
|
|
407
|
+
details.chapter > summary::after { content: "Expand"; margin-left: auto; font-size: .75rem; font-weight: 600; color: var(--ink2); border: 1px solid var(--line); border-radius: 7px; padding: 3px 9px; }
|
|
408
|
+
details.chapter[open] > summary::after { content: "Collapse"; }
|
|
409
|
+
details.chapter[open] > summary { border-bottom: 1px solid var(--line); border-radius: 10px 10px 0 0; }
|
|
410
410
|
.chapter-title { font-size: 1.0625rem; font-weight: 700; color: var(--ink); }
|
|
411
411
|
.chapter-about { font-size: .8125rem; font-weight: 400; color: var(--ink2); flex: 1 1 14rem; }
|
|
412
|
+
.chapter-group { margin: 10px 0 -4px; font-size: .8125rem; font-weight: 700; letter-spacing: .04em; text-transform: uppercase; color: var(--ink2); }
|
|
413
|
+
/* Reached by a link: said for a moment. */
|
|
414
|
+
details.chapter.reached { outline: 2px solid var(--brass); outline-offset: 2px; }
|
|
415
|
+
@media (prefers-reduced-motion: no-preference) { details.chapter { transition: outline-color .6s; } }
|
|
412
416
|
.chapter-body { display: flex; flex-direction: column; gap: 10px; padding: 12px 14px 14px; background: var(--panel); border-radius: 0 0 10px 10px; }
|
|
413
417
|
.facts { display: grid; grid-template-columns: max-content 1fr; gap: 4px 14px; margin: 0; }
|
|
414
418
|
.facts dt { color: var(--ink2); }
|
|
@@ -973,6 +977,8 @@ button.inline:hover { color: var(--brass); }
|
|
|
973
977
|
.server-parts .sub { font-weight: normal; color: var(--ink2); }
|
|
974
978
|
.server-parts dt.sub { padding-left: 14px; }
|
|
975
979
|
.reserve-row { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 14px; }
|
|
980
|
+
.reserve-what { min-width: 4.5em; font-weight: 600; }
|
|
981
|
+
.server-note.promised[data-over="true"] { color: var(--warn, #b7791f); font-weight: 600; }
|
|
976
982
|
.reserve-row label { display: inline-flex; align-items: center; gap: 6px; }
|
|
977
983
|
.reserve-row input { width: 5.5em; min-height: 40px; font: .875rem var(--sans); color: var(--ink); background: var(--bg); border: 1px solid var(--line); border-radius: 7px; padding: 7px 9px; }
|
|
978
984
|
|