strom-research 1.18.0-beta.5 → 1.18.0-beta.7

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.cs.md CHANGED
@@ -7,7 +7,7 @@ Změny každé verze ve zkratce. Úplné poznámky a instalační řádky jsou n
7
7
 
8
8
  ## 1.18.0
9
9
 
10
- *Připravuje se: bety do 1.18.0-beta.5. Oddíl bude úplný s vydáním.*
10
+ *Připravuje se: bety do 1.18.0-beta.7. Oddíl bude úplný s vydáním.*
11
11
 
12
12
  - **Fotky z telefonu nastojato.** Fotka otočená podle údaje EXIF se čte tak, jak se má dívat, nastojato; místa na ní
13
13
  vyznačená (výřezy, poloviny dvoustrany, uložená část) jsou místa obrázku, jak se ukazuje. Fotka sama zůstává, jak
@@ -30,7 +30,9 @@ Změny každé verze ve zkratce. Úplné poznámky a instalační řádky jsou n
30
30
  rozhodnutí, posledních 24 hodin a poslední změny, obnovené každých 10 sekund, dokud je stránka na očích — jen
31
31
  k podívání: v telefonu se nic neodpovídá, nespouští ani nerozhoduje. Nedostupný počítač stránka řekne i s posledními
32
32
  údaji a jejich časem. Otevře strom, jehož QR kód ji spároval, a jde přidat na plochu jako ikona, která ji otevře
33
- bez nového párování.
33
+ bez nového párování. Co čeká a změny jdou na telefonu přečíst celé: text toho, co čeká, se neořezává, další položky
34
+ nad prvních 20 se donačtou klepnutím a změna s víc řádky se rozbalí celá; Co čeká je poslední částí stránky. Změna
35
+ starší než dnešek má u času i datum (rok jen u loňských a starších).
34
36
  - **Souhlas v jednom jazyce.** Souhlas, který dává jen člověk (rozbalení výzkumu, odebrání stromu, odmítnuté okno),
35
37
  zazní celý v jazyce výzkumu, nic z něj nezůstane anglicky.
36
38
  - **Odmítnutá aktualizace.** Když se `strom update` odmítne, řekne, že se nic nenainstalovalo a která verze zůstává,
package/CHANGELOG.de.md CHANGED
@@ -7,7 +7,7 @@ Die Änderungen jeder Version in Kürze. Die vollständigen Hinweise und die Ins
7
7
 
8
8
  ## 1.18.0
9
9
 
10
- *In Vorbereitung: die Betas bis 1.18.0-beta.5. Der Abschnitt ist mit dem Release vollständig.*
10
+ *In Vorbereitung: die Betas bis 1.18.0-beta.7. Der Abschnitt ist mit dem Release vollständig.*
11
11
 
12
12
  - **Handyfotos aufrecht.** Ein Foto, das nach seiner EXIF-Ausrichtung gedreht ist, wird gelesen, wie es gesehen
13
13
  werden soll, aufrecht; die darauf markierten Stellen (Ausschnitte, die Hälften einer Doppelseite, ein gespeicherter
@@ -34,7 +34,10 @@ Die Änderungen jeder Version in Kürze. Die vollständigen Hinweise und die Ins
34
34
  Sekunden aufgefrischt, solange die Seite angesehen wird — nur zum Ansehen: auf dem Handy wird nichts beantwortet,
35
35
  gestartet oder entschieden. Ein unerreichbarer Computer wird gesagt, mit den letzten Daten und ihrer Zeit. Die Seite
36
36
  öffnet den Stammbaum, dessen QR-Code sie gekoppelt hat, und lässt sich als Symbol auf den Home-Bildschirm legen,
37
- das sie ohne neues Koppeln öffnet.
37
+ das sie ohne neues Koppeln öffnet. Was wartet und die Änderungen lassen sich auf dem Handy ganz lesen: kein Text
38
+ dessen, was wartet, wird gekürzt, mehr als die ersten 20 kommen auf Antippen, eine Änderung mit vielen Zeilen klappt
39
+ ganz auf; Was wartet ist der letzte Teil der Seite. Eine Änderung vor heute hat bei der Uhrzeit auch ihr Datum (das
40
+ Jahr nur, wenn es nicht das laufende ist).
38
41
  - **Eine Zustimmung in einer Sprache.** Eine Zustimmung, die nur ein Mensch gibt (eine Forschung entpacken, einen
39
42
  Stammbaum entfernen, ein abgelehntes Fenster), steht ganz in der Sprache der Forschung, nichts davon bleibt
40
43
  Englisch.
package/CHANGELOG.md CHANGED
@@ -7,7 +7,7 @@ The changes of each version in short. The full notes and the install lines are o
7
7
 
8
8
  ## 1.18.0
9
9
 
10
- *In preparation: the betas up to 1.18.0-beta.5. The section is complete with the release.*
10
+ *In preparation: the betas up to 1.18.0-beta.7. The section is complete with the release.*
11
11
 
12
12
  - **Phone photos upright.** A photo turned by its EXIF orientation is read as it is meant to be seen, upright; the
13
13
  places marked on it (excerpts, the halves of a spread, a cut-out part) are of the picture as it is shown. The
@@ -32,7 +32,10 @@ The changes of each version in short. The full notes and the install lines are o
32
32
  what waits for a decision, the last 24 hours and the latest changes, refreshed every 10 seconds while the page is
33
33
  looked at — only to look at: nothing on the phone answers, starts or decides anything. A computer out of reach is
34
34
  said, with the last data and their time. The page opens the tree whose QR code paired it, and it can be added to
35
- the home screen as an icon that opens it without pairing again.
35
+ the home screen as an icon that opens it without pairing again. What waits and the changes read whole on the phone:
36
+ no text of what waits is cut, more of it than the first 20 comes on a tap, a change of many lines unfolds whole;
37
+ What waits is the last part of the page. A change older than today has its date beside the time (the year only when
38
+ it is not this one).
36
39
  - **A consent in one language.** A consent only a person gives (unpacking a research, removing a tree, a window said
37
40
  no to) is said whole in the research's language, nothing of it left in English.
38
41
  - **An update said no to.** When `strom update` is declined, it says nothing was installed and which version stays,
@@ -1403,7 +1403,9 @@
1403
1403
  "ui.phone.page.waiting.title": "Co čeká",
1404
1404
  "ui.phone.page.waiting.none": "Nic nečeká.",
1405
1405
  "ui.phone.page.waiting.where": "Rozhodnout jde na počítači v Přehledu výzkumu.",
1406
- "ui.phone.page.waiting.more": "{n, plural, one {a # další na počítači} few {a # další na počítači} other {a # dalších na počítači}}",
1406
+ "ui.phone.page.waiting.more": "{n, plural, one {Ukázat další · zbývá #} few {Ukázat další · zbývají #} other {Ukázat další · zbývá #}}",
1407
+ "ui.phone.page.waiting.loading": "Načítá se…",
1408
+ "ui.phone.page.waiting.rest": "{n, plural, one {a # další v Přehledu výzkumu na počítači} few {a # další v Přehledu výzkumu na počítači} other {a # dalších v Přehledu výzkumu na počítači}}",
1407
1409
  "ui.phone.page.day": "Posledních 24 h",
1408
1410
  "ui.phone.page.day.people": "{n, plural, one {osoba} few {osoby} other {osob}}",
1409
1411
  "ui.phone.page.day.sources": "{n, plural, one {pramen} few {prameny} other {pramenů}}",
@@ -1412,6 +1414,7 @@
1412
1414
  "ui.phone.page.log.more": "Starší změny jsou v Přehledu výzkumu na počítači.",
1413
1415
  "ui.phone.page.log.new": "{n, plural, one {# nová změna ↑} few {# nové změny ↑} other {# nových změn ↑}}",
1414
1416
  "ui.phone.page.log.lines": "{n, plural, one {a # další} few {a # další} other {a # dalších}}",
1417
+ "ui.phone.page.log.fold": "Sbalit",
1415
1418
  "ui.phone.page.log.none": "Zatím se nic neuložilo.",
1416
1419
  "ui.settings.phone": "Propojení s telefonem (fotky z telefonu do výzkumu v místní síti): {state}",
1417
1420
  "ui.settings.phone.on": "zapnuto, spárované telefony: {n}",
@@ -1403,7 +1403,9 @@
1403
1403
  "ui.phone.page.waiting.title": "Wartet",
1404
1404
  "ui.phone.page.waiting.none": "Nichts wartet.",
1405
1405
  "ui.phone.page.waiting.where": "Entscheiden lässt sich am Computer in der Forschungsübersicht.",
1406
- "ui.phone.page.waiting.more": "{n, plural, one {und # weiteres am Computer} other {und # weitere am Computer}}",
1406
+ "ui.phone.page.waiting.more": "{n, plural, one {Weitere zeigen · noch #} other {Weitere zeigen · noch #}}",
1407
+ "ui.phone.page.waiting.loading": "Wird geladen…",
1408
+ "ui.phone.page.waiting.rest": "{n, plural, one {und # weiteres in der Forschungsübersicht am Computer} other {und # weitere in der Forschungsübersicht am Computer}}",
1407
1409
  "ui.phone.page.day": "Letzte 24 Std.",
1408
1410
  "ui.phone.page.day.people": "{n, plural, one {Person} other {Personen}}",
1409
1411
  "ui.phone.page.day.sources": "{n, plural, one {Quelle} other {Quellen}}",
@@ -1412,6 +1414,7 @@
1412
1414
  "ui.phone.page.log.more": "Ältere Änderungen stehen in der Forschungsübersicht am Computer.",
1413
1415
  "ui.phone.page.log.new": "{n, plural, one {# neue Änderung ↑} other {# neue Änderungen ↑}}",
1414
1416
  "ui.phone.page.log.lines": "{n, plural, one {und # weitere} other {und # weitere}}",
1417
+ "ui.phone.page.log.fold": "Weniger zeigen",
1415
1418
  "ui.phone.page.log.none": "Noch nichts gespeichert.",
1416
1419
  "ui.settings.phone": "Verbindung mit dem Telefon (Fotos vom Telefon in die Forschung im lokalen Netz): {state}",
1417
1420
  "ui.settings.phone.on": "an, gekoppelte Telefone: {n}",
@@ -58,13 +58,6 @@
58
58
  <ul id="nowLines" class="lines"></ul>
59
59
  <div id="nowIdle" hidden><p class="idle-row"><span class="ring" aria-hidden="true"></span><span id="nowIdleText"></span></p><p id="nowIdleLast" class="quiet small"></p></div>
60
60
  </section>
61
- <section id="waitCard" class="card">
62
- <h2 id="waitTitle" class="cap"></h2>
63
- <p id="waitNone" class="quiet" hidden></p>
64
- <ul id="waitList" class="waits"></ul>
65
- <p id="waitMore" class="quiet small" hidden></p>
66
- <p id="waitWhere" class="quiet small"></p>
67
- </section>
68
61
  <section id="dayCard" class="card" hidden>
69
62
  <h2 id="dayTitle" class="cap"></h2>
70
63
  <div class="day">
@@ -79,6 +72,15 @@
79
72
  <ul id="logList" class="log"></ul>
80
73
  <p id="logMore" class="quiet small" hidden></p>
81
74
  </section>
75
+ <!-- the last: each text whole, more of them asked for on a tap -->
76
+ <section id="waitCard" class="card">
77
+ <h2 id="waitTitle" class="cap"></h2>
78
+ <p id="waitNone" class="quiet" hidden></p>
79
+ <ul id="waitList" class="waits"></ul>
80
+ <p class="morerow"><button id="waitMore" class="linkbtn" type="button" hidden></button></p>
81
+ <p id="waitRest" class="quiet small" hidden></p>
82
+ <p id="waitWhere" class="quiet small"></p>
83
+ </section>
82
84
  </div>
83
85
  <p class="homerow"><button id="homeBtn" class="linkbtn" type="button" hidden></button></p>
84
86
  <section id="homeCard" class="card" hidden>
@@ -74,18 +74,23 @@ button{font:inherit;color:inherit}
74
74
  .waits li{display:grid;grid-template-columns:8px minmax(0,1fr);gap:10px;padding:8px 0;border-top:1px solid var(--divider)}
75
75
  .waits li:first-child{border-top:0;padding-top:0}
76
76
  .waits .wdot{width:8px;height:8px;border-radius:50%;background:var(--waiting-dot);margin-top:7px}
77
- .waits .what{font-size:14.5px;font-weight:500}
78
- .waits .on{font-size:13px;color:var(--text-light);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
77
+ .waits .what{font-size:14.5px;font-weight:500;overflow-wrap:anywhere}
78
+ .waits .on{font-size:13px;color:var(--text-light);overflow-wrap:anywhere;margin-top:2px}
79
79
  #waitCard.none{display:flex;align-items:baseline;gap:12px;flex-wrap:wrap}
80
80
  #waitCard.none .cap,#waitCard.none p{margin:0}
81
- #waitWhere,#waitMore,#logMore{margin:10px 0 0}
81
+ #waitWhere,#waitRest,#logMore{margin:10px 0 0}
82
+ .morerow{margin:6px 0 0}
83
+ .morerow .linkbtn{padding:10px 0;min-height:44px;text-align:left}
84
+ .morerow .linkbtn:disabled{color:var(--text-faint);text-decoration:none;cursor:default}
82
85
  .day{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:8px}
83
86
  .day b{display:block;font-size:20px;font-weight:600;font-variant-numeric:tabular-nums}
84
87
  .day span{font-size:12.5px;color:var(--text-light)}
85
- .log li{display:grid;grid-template-columns:42px minmax(0,1fr);gap:8px;padding:7px 6px;margin:0 -6px;border-radius:8px}
86
- .log time{font-size:13px;color:var(--text-faint);font-variant-numeric:tabular-nums;padding-top:1px}
87
- .log p{margin:0;font-size:14px}
88
+ .log{display:grid;grid-template-columns:fit-content(88px) minmax(0,1fr)}
89
+ .log li{grid-column:1/-1;display:grid;grid-template-columns:fit-content(88px) minmax(0,1fr);grid-template-columns:subgrid;gap:8px;padding:7px 6px;margin:0 -6px;border-radius:8px}
90
+ .log time{min-width:42px;overflow-wrap:normal;font-size:13px;color:var(--text-faint);font-variant-numeric:tabular-nums;padding-top:1px}
91
+ .log p{margin:0;font-size:14px;overflow-wrap:anywhere}
88
92
  .log .more{font-size:13px;color:var(--text-faint)}
93
+ .log .unfold{display:block;background:none;border:0;padding:6px 0;margin:0;min-height:32px;font:inherit;font-size:13px;color:var(--primary);text-align:left;cursor:pointer}
89
94
  .log li.fresh{background:var(--primary-soft)}
90
95
  .newpill{position:fixed;top:calc(72px + env(safe-area-inset-top));left:50%;transform:translateX(-50%);z-index:7;border:0;border-radius:999px;background:var(--primary);color:var(--on-primary);padding:0 16px;min-height:36px;font-size:13px;font-weight:600;cursor:pointer;box-shadow:0 2px 8px rgba(20,16,8,.25)}
91
96
 
@@ -68,7 +68,9 @@
68
68
  const t = (key, v = {}) => fill(T[key] ?? key, v);
69
69
  // a small photo is never "0.0 MB"
70
70
  const mb = (bytes) => new Intl.NumberFormat(LANG, { minimumFractionDigits: 1, maximumFractionDigits: 1 }).format(bytes > 0 ? Math.max(0.1, bytes / 1024 / 1024) : 0);
71
- const hhmm = (ms) => new Intl.DateTimeFormat(LANG, { hour: "2-digit", minute: "2-digit" }).format(new Date(ms));
71
+ // a 12-hour clock without a leading zero ("2:05 PM"), a 24-hour one with it ("09:07")
72
+ const HOUR = /^h1[12]$/u.test(new Intl.DateTimeFormat(LANG, { hour: "numeric" }).resolvedOptions().hourCycle ?? "") ? "numeric" : "2-digit";
73
+ const hhmm = (ms) => new Intl.DateTimeFormat(LANG, { hour: HOUR, minute: "2-digit" }).format(new Date(ms));
72
74
  const fold = (s) => String(s).normalize("NFD").replace(/\p{M}/gu, "").toLowerCase();
73
75
  const rid = () => Array.from(crypto.getRandomValues(new Uint8Array(16)), (b) => b.toString(16).padStart(2, "0")).join("").replace(/^(.{8})(.{4})(.{4})(.{4})/, "$1-$2-$3-$4-");
74
76
  const el = (tag, cls, text) => {
@@ -164,6 +166,8 @@
164
166
  shown: null, // what the tree shown does: now, lastWorked, waiting, day, latest
165
167
  logTree: "", // the tree and its newest change the progress shows
166
168
  logLatest: "",
169
+ waitLoaded: null, // more of what waits, asked for on a tap: {tree, of (what the overview gave then), n, items}
170
+ waitBusy: false,
167
171
  newCount: 0, // changes added above while scrolled down
168
172
  returning: false, // the page looked at again: the first answer after it awaited
169
173
  };
@@ -259,6 +263,7 @@
259
263
  if (asked === S.tree) {
260
264
  S.shown = r.body.shown || null;
261
265
  if (S.shown && S.shown.latest && (S.logTree !== S.tree || S.logLatest !== S.shown.latest)) void loadLog(S.tree, S.shown.latest);
266
+ keepWait();
262
267
  } else {
263
268
  S.shown = null;
264
269
  setTimeout(() => void refresh(), 0);
@@ -287,6 +292,34 @@
287
292
  if (S.shown && S.shown.latest && (S.logTree !== S.tree || S.logLatest !== S.shown.latest)) void loadLog(S.tree, S.shown.latest);
288
293
  })());
289
294
 
295
+ // more of what waits than the overview gives, the first ones of the same list again with them
296
+ const WAIT_PAGE = 50;
297
+ const WAIT_MAX = 200;
298
+ let waitAsking = null;
299
+ const loadWait = (count, quiet = false) =>
300
+ (waitAsking ??= (async () => {
301
+ const tree = S.tree;
302
+ const of = JSON.stringify(S.shown && S.shown.waiting);
303
+ // asked on a tap: the button says so (asked again after an answer: nothing changes till it comes)
304
+ if (!quiet) {
305
+ S.waitBusy = true;
306
+ drawWait();
307
+ }
308
+ const r = await ask(`/phone/waiting?tree=${encodeURIComponent(tree)}&from=0&limit=${Math.min(WAIT_MAX, count)}`);
309
+ waitAsking = null;
310
+ S.waitBusy = false;
311
+ if (tree === S.tree && r.status === 200 && Array.isArray(r.body.items)) S.waitLoaded = { tree, of, n: Number(r.body.n) || 0, items: r.body.items };
312
+ drawWait();
313
+ })());
314
+ // more of what waits asked for, kept over each answer: what waits another, asked again as many (all of it in the
315
+ // overview: none)
316
+ const keepWait = () => {
317
+ const w = S.shown && S.shown.waiting;
318
+ if (!S.waitLoaded || JSON.stringify(w) === S.waitLoaded.of) return;
319
+ if (!w || !Array.isArray(w.items) || w.n <= w.items.length) S.waitLoaded = null;
320
+ else void loadWait(S.waitLoaded.items.length, true);
321
+ };
322
+
290
323
  // ── each photo: its SHA-256, in the research already?, sent ──────────────────────────────────────────────────────
291
324
  let worker = null;
292
325
  const waiting = new Map();
@@ -641,7 +674,16 @@
641
674
  // a time of today, else its day too
642
675
  const atTime = (iso) => {
643
676
  const d = new Date(iso);
644
- return sameDay(d, new Date()) ? hhmm(d) : new Intl.DateTimeFormat(LANG, { day: "numeric", month: "numeric", hour: "2-digit", minute: "2-digit" }).format(d);
677
+ return sameDay(d, new Date()) ? hhmm(d) : new Intl.DateTimeFormat(LANG, { day: "numeric", month: "numeric", hour: HOUR, minute: "2-digit" }).format(d);
678
+ };
679
+ // a change's time: of today the time alone, else its day before it (the year when not this one), broken only
680
+ // between the two
681
+ const logTime = (ms, now = new Date()) => {
682
+ const d = new Date(ms);
683
+ if (sameDay(d, now)) return hhmm(d);
684
+ const day = new Intl.DateTimeFormat(LANG, { day: "numeric", month: "numeric", ...(d.getFullYear() !== now.getFullYear() ? { year: "numeric" } : {}) }).format(d);
685
+ const whole = (text) => text.replace(/\s/gu, "\u00a0");
686
+ return `${whole(day)} ${whole(hhmm(d))}`;
645
687
  };
646
688
  const whenOf = (iso) => {
647
689
  const d = new Date(iso);
@@ -689,26 +731,7 @@
689
731
  $("nowIdleLast").hidden = !o.lastWorked;
690
732
  if (o.lastWorked) $("nowIdleLast").textContent = t("now.idleLast", { when: whenOf(o.lastWorked) });
691
733
  }
692
- const w = o.waiting && Array.isArray(o.waiting.items) ? o.waiting : { n: 0, items: [] };
693
- if (!archive && changed("wait", w)) {
694
- $("waitCard").classList.toggle("none", !w.n);
695
- $("waitTitle").textContent = w.n ? t("waiting", { n: w.n }) : t("waiting.title");
696
- $("waitNone").hidden = w.n > 0;
697
- $("waitList").replaceChildren(
698
- ...w.items.map((x) => {
699
- const li = el("li");
700
- const box = el("div");
701
- box.append(el("div", "what", x.what));
702
- if (x.on) box.append(el("div", "on", x.on));
703
- li.append(el("span", "wdot"), box);
704
- return li;
705
- }),
706
- );
707
- $("waitList").hidden = !w.n;
708
- $("waitMore").hidden = w.n <= w.items.length;
709
- if (w.n > w.items.length) $("waitMore").textContent = t("waiting.more", { n: w.n - w.items.length });
710
- $("waitWhere").hidden = !w.n;
711
- }
734
+ if (!archive) drawWait();
712
735
  $("dayCard").hidden = !o.day;
713
736
  if (o.day && changed("day", o.day)) {
714
737
  const plus = (n) => (n > 0 ? "+" : "") + count(n);
@@ -722,16 +745,68 @@
722
745
  $("logCard").hidden = S.logTree !== S.tree;
723
746
  };
724
747
 
748
+ // what waits, each text whole: the overview's first ones, or more of them asked for (kept till what waits is another)
749
+ const drawWait = () => {
750
+ const tree = treeById(S.tree);
751
+ const o = S.shown;
752
+ if (!tree || !o || tree.archive) return;
753
+ const first = o.waiting && Array.isArray(o.waiting.items) ? o.waiting : { n: 0, items: [] };
754
+ const more = S.waitLoaded && S.waitLoaded.tree === S.tree && S.waitLoaded.items.length > first.items.length ? S.waitLoaded : null;
755
+ const w = more ? { n: more.n, items: more.items } : first;
756
+ if (!changed("wait", [w, S.waitBusy])) return;
757
+ $("waitCard").classList.toggle("none", !w.n);
758
+ $("waitTitle").textContent = w.n ? t("waiting", { n: w.n }) : t("waiting.title");
759
+ $("waitNone").hidden = w.n > 0;
760
+ $("waitList").replaceChildren(
761
+ ...w.items.map((x) => {
762
+ const li = el("li");
763
+ const box = el("div");
764
+ box.append(el("div", "what", x.what));
765
+ if (x.on) box.append(el("div", "on", x.on));
766
+ li.append(el("span", "wdot"), box);
767
+ return li;
768
+ }),
769
+ );
770
+ $("waitList").hidden = !w.n;
771
+ const left = Math.max(0, w.n - w.items.length);
772
+ const can = left > 0 && w.items.length < WAIT_MAX;
773
+ $("waitMore").hidden = !can;
774
+ $("waitMore").disabled = S.waitBusy;
775
+ if (can) $("waitMore").textContent = S.waitBusy ? t("waiting.loading") : t("waiting.more", { n: left });
776
+ $("waitRest").hidden = !left || can;
777
+ if (left && !can) $("waitRest").textContent = t("waiting.rest", { n: left });
778
+ $("waitWhere").hidden = !w.n;
779
+ };
780
+
725
781
  // the progress: the changes drawn kept by their id, a new one put on top (lit for 6 s); scrolled down, what is under
726
782
  // the finger stays where it is and a pill says how many came above
727
783
  const logItems = new Map();
728
784
  const drawChange = (c) => {
729
785
  const li = el("li");
730
- const time = el("time", "", hhmm(Date.parse(c.at)));
786
+ const time = el("time", "", logTime(Date.parse(c.at)));
731
787
  time.dateTime = c.at;
732
788
  const box = el("div");
733
789
  for (const line of c.lines || []) box.append(el("p", "", line));
734
- if (c.more) box.append(el("p", "more", t("log.lines", { n: c.more })));
790
+ // its other lines unfolded on a tap (the change drawn is kept: a new answer folds nothing)
791
+ const rest = Array.isArray(c.rest) ? c.rest : [];
792
+ if (c.more && rest.length) {
793
+ const hid = el("div", "rest");
794
+ hid.hidden = true;
795
+ hid.id = `rest-${c.id}`;
796
+ for (const line of rest) hid.append(el("p", "", line));
797
+ if (c.more > rest.length) hid.append(el("p", "more", t("log.lines", { n: c.more - rest.length })));
798
+ const unfold = el("button", "unfold", t("log.lines", { n: c.more }));
799
+ unfold.type = "button";
800
+ unfold.setAttribute("aria-expanded", "false");
801
+ unfold.setAttribute("aria-controls", hid.id);
802
+ unfold.onclick = () => {
803
+ const open = hid.hidden;
804
+ hid.hidden = !open;
805
+ unfold.setAttribute("aria-expanded", String(open));
806
+ unfold.textContent = open ? t("log.fold") : t("log.lines", { n: c.more });
807
+ };
808
+ box.append(hid, unfold);
809
+ } else if (c.more) box.append(el("p", "more", t("log.lines", { n: c.more })));
735
810
  li.append(time, box);
736
811
  return li;
737
812
  };
@@ -775,6 +850,17 @@
775
850
  }
776
851
  renderPill();
777
852
  };
853
+ // past midnight the changes of the day before get their day (asked every second, drawn again once a day)
854
+ let logDay = "";
855
+ const restampLog = (now = new Date()) => {
856
+ const day = now.toDateString();
857
+ if (day === logDay) return;
858
+ logDay = day;
859
+ for (const li of logItems.values()) {
860
+ const time = li.children[0];
861
+ time.textContent = logTime(Date.parse(time.dateTime), now);
862
+ }
863
+ };
778
864
  const renderPill = () => {
779
865
  if (window.scrollY <= 4) S.newCount = 0;
780
866
  $("newPill").hidden = !S.newCount || S.view !== "main";
@@ -980,6 +1066,7 @@
980
1066
  closeTrees();
981
1067
  if (S.tree !== id) {
982
1068
  S.shown = null;
1069
+ S.waitLoaded = null;
983
1070
  S.logTree = "";
984
1071
  for (const li of logItems.values()) li.remove();
985
1072
  logItems.clear();
@@ -1078,6 +1165,10 @@
1078
1165
  $("personClear").setAttribute("aria-label", t("remove"));
1079
1166
 
1080
1167
  $("plainRetry").onclick = () => start("");
1168
+ $("waitMore").onclick = () => {
1169
+ const w = S.waitLoaded && S.waitLoaded.tree === S.tree ? S.waitLoaded : S.shown && S.shown.waiting;
1170
+ if (w && Array.isArray(w.items)) void loadWait(w.items.length + WAIT_PAGE);
1171
+ };
1081
1172
  $("photoBtn").onclick = $("morePhotoBtn").onclick = () => $("photoInput").click();
1082
1173
  $("galleryBtn").onclick = $("moreGalleryBtn").onclick = () => $("galleryInput").click();
1083
1174
  $("photoInput").onchange = (e) => {
@@ -1210,6 +1301,7 @@
1210
1301
  ticks++;
1211
1302
  if (S.view === "main") {
1212
1303
  renderHeader();
1304
+ restampLog();
1213
1305
  if (ticks % 10 === 0 && document.visibilityState !== "hidden") void refresh();
1214
1306
  } else if (S.view === "unpaired" && ticks % 10 === 0) void whoami().then((w) => w === "paired" && void refresh());
1215
1307
  }, 1000);
package/dist/cli/ui.js CHANGED
@@ -1304,7 +1304,9 @@ export const UI = {
1304
1304
  "ui.phone.page.waiting.title": "Waiting",
1305
1305
  "ui.phone.page.waiting.none": "Nothing is waiting.",
1306
1306
  "ui.phone.page.waiting.where": "Decisions are made on the computer in the Research overview.",
1307
- "ui.phone.page.waiting.more": "{n, plural, one {and # more on the computer} other {and # more on the computer}}",
1307
+ "ui.phone.page.waiting.more": "{n, plural, one {Show more · # left} other {Show more · # left}}",
1308
+ "ui.phone.page.waiting.loading": "Loading…",
1309
+ "ui.phone.page.waiting.rest": "{n, plural, one {and # more in the Research overview on the computer} other {and # more in the Research overview on the computer}}",
1308
1310
  "ui.phone.page.day": "Last 24 h",
1309
1311
  "ui.phone.page.day.people": "{n, plural, one {person} other {people}}",
1310
1312
  "ui.phone.page.day.sources": "{n, plural, one {source} other {sources}}",
@@ -1313,6 +1315,7 @@ export const UI = {
1313
1315
  "ui.phone.page.log.more": "Older changes are in the Research overview on the computer.",
1314
1316
  "ui.phone.page.log.new": "{n, plural, one {# new change ↑} other {# new changes ↑}}",
1315
1317
  "ui.phone.page.log.lines": "{n, plural, one {and # more} other {and # more}}",
1318
+ "ui.phone.page.log.fold": "Show less",
1316
1319
  "ui.phone.page.log.none": "Nothing saved yet.",
1317
1320
  "ui.settings.phone": "Phone connection (photos from a phone into the research on the local network): {state}",
1318
1321
  "ui.settings.phone.on": "on, paired phones: {n}",
@@ -222,9 +222,13 @@ export function statusReads(seen, now = Date.now()) {
222
222
  .map((r) => r.read)
223
223
  .filter((r) => !FINAL_STATES.includes(r.state) || Date.parse(r.since) > now - STATUS_DAYS * DAY_MS);
224
224
  }
225
- /** GET /reads: the last 90 days, and every reading no source of the research is of. */
225
+ /**
226
+ * GET /reads: the last 90 days, and every reading no source of the research is of. inSource: a source of this tree —
227
+ * the only one a reading can be of (its input, state and result are the tree's; the sync takes no other's) — and not
228
+ * retracted (a sync undone, a source merged away): the reading is then to be come back to again.
229
+ */
226
230
  export function listedReads(tree, seen, now = Date.now()) {
227
- const inSources = new Set(tree.list("source").map((s) => s.reading).filter((r) => typeof r === "string"));
231
+ const inSources = new Set(tree.list("source").flatMap((s) => (!s.retracted && typeof s.reading === "string" ? [s.reading] : [])));
228
232
  return seen
229
233
  .all()
230
234
  .map((r) => ({ ...r.read, inSource: inSources.has(r.read.id) }))
@@ -5,8 +5,10 @@
5
5
  // (this computer converts HEIC: else the page asks the phone for JPEG), max (the largest
6
6
  // file, MB); of the tree asked, what it shows (phone-watch.ts): now, lastWorked, waiting,
7
7
  // day, latest (a tree asked that is gone: 404 phone.tree-gone, as /phone/log)
8
- // GET /phone/log?tree=&limit= the last changes the research saved, newest first ({changes: [{id, at, lines, more?}],
9
- // more}), at most 20
8
+ // GET /phone/log?tree=&limit= the last changes the research saved, newest first ({changes: [{id, at, lines, more?,
9
+ // rest?}], more}), at most 20; rest: the lines of a change past the 3 shown
10
+ // GET /phone/waiting?tree=&from=&limit= more of what waits than the state gives ({n, items: [{what, on?}]}), of the
11
+ // same list, at most 200 at once
10
12
  // GET /phone/people?tree=&q= up to 8 people of the tree matching q (2 letters at least; case, accents and NFD
11
13
  // aside): {id, name, birth, death, place}
12
14
  // GET /phone/media/<sha256>?tree= is that content in the research already ({known: false} or {known, kind})
@@ -30,7 +32,7 @@ import { waysHere } from "./readable.js";
30
32
  import { birthEvent, deathEvent, displayName, findPersons } from "./people.js";
31
33
  import { yearLabel } from "./gdate.js";
32
34
  import { foldText } from "./text.js";
33
- import { LOG_PHONE, treeLog, treeMarks, treeOverview } from "./phone-watch.js";
35
+ import { LOG_PHONE, WAITING_ASKED, WAITING_SHOWN, treeLog, treeMarks, treeOverview, treeWaiting } from "./phone-watch.js";
34
36
  /** How often the batches of a phone are looked at, and the parts of uploads cut short. */
35
37
  const INCOMING_EVERY_MS = 60 * 60_000;
36
38
  export function phoneSend(env, note) {
@@ -128,6 +130,22 @@ export function phoneSend(env, note) {
128
130
  const limit = Math.min(LOG_PHONE, Math.max(1, Number.parseInt(url.searchParams.get("limit") ?? "", 10) || LOG_PHONE));
129
131
  reply(200, treeLog(t.root, env, limit));
130
132
  };
133
+ if (req.method === "GET" && p === "/phone/waiting")
134
+ return () => {
135
+ if (foreign())
136
+ return refuse(403, refusal("only the phone's page asks", "phone.origin"));
137
+ req.resume();
138
+ const t = treeOf();
139
+ if (!t)
140
+ return gone();
141
+ const number = (name, or) => {
142
+ const n = Number.parseInt(url.searchParams.get(name) ?? "", 10);
143
+ return Number.isFinite(n) ? n : or;
144
+ };
145
+ const from = Math.max(0, number("from", WAITING_SHOWN));
146
+ const limit = Math.min(WAITING_ASKED, Math.max(1, number("limit", WAITING_ASKED)));
147
+ reply(200, treeWaiting(t.root, env, from, limit));
148
+ };
131
149
  if (req.method === "GET" && p === "/phone/people")
132
150
  return () => {
133
151
  if (foreign())
@@ -21,10 +21,13 @@ import { ALL_PREFIXES } from "./model.js";
21
21
  /** How long the overview of the tree shown is kept (the page asks every 10 s); the marks of the other trees in the switcher. */
22
22
  export const OVERVIEW_MS = 15_000;
23
23
  const MARKS_MS = 60_000;
24
- /** At most so many changes the page shows; so many lines of one change, and of what waits. */
24
+ /** At most so many changes the page shows; so many lines of one change shown, and kept to be unfolded on the phone. */
25
25
  export const LOG_PHONE = 20;
26
26
  const LINES_PER_CHANGE = 3;
27
- const WAITING_SHOWN = 20;
27
+ const LINES_KEPT = 200;
28
+ /** So many of what waits the overview gives; more asked by the page, at most so many at once. */
29
+ export const WAITING_SHOWN = 20;
30
+ export const WAITING_ASKED = 200;
28
31
  // A record's ID standing alone (every prefix of the model, a document read's D + its time + 4 hex); a list of them
29
32
  // (", ", a dash, a short word between two: "H0005, H0010, …", "T0400 und T0401") goes as one.
30
33
  const ALONE = String.raw `(?<![\p{L}\p{M}\p{N}])`;
@@ -125,6 +128,7 @@ function waitingOf(tree, env, all) {
125
128
  });
126
129
  }
127
130
  const marksKept = new Map();
131
+ // what waits is kept whole beside the overview: the page asks for more of the same list the overview began
128
132
  const overviewKept = new Map();
129
133
  const logKept = new Map();
130
134
  /** An agent at work on a tree, and how much waits for the person (none of either in an archive). */
@@ -154,6 +158,7 @@ export function treeOverview(root, env) {
154
158
  return { ...kept.value, ...day, ...latest };
155
159
  const tree = Tree.open(root, env);
156
160
  let value;
161
+ let waits = [];
157
162
  if (actsAsArchive(tree))
158
163
  value = { now: [], waiting: { n: 0, items: [] } };
159
164
  else {
@@ -171,18 +176,32 @@ export function treeOverview(root, env) {
171
176
  });
172
177
  const sessions = tree.list("session");
173
178
  const last = sessions.map((s) => s.ended ?? s.started).sort().at(-1);
174
- const waits = waitingOf(tree, env, all);
175
- // a text of nothing but IDs, paths and links: said in general
176
- const items = waits.slice(0, WAITING_SHOWN).map(({ item }) => {
177
- const on = item.on ? forPhone(humanTask(tree, item.on, tree.lang)) : "";
178
- return { what: forPhone(humanTask(tree, item.what, tree.lang)) || ui(tree.lang, "ui.phone.waiting.item"), ...(on ? { on } : {}) };
179
- });
179
+ waits = waitingOf(tree, env, all);
180
+ const items = waits.slice(0, WAITING_SHOWN).map((w) => waitingLine(tree, w));
180
181
  value = { now, ...(!now.length && last ? { lastWorked: last } : {}), waiting: { n: waits.length, items } };
181
182
  marksKept.set(root, { at: Date.now(), value: { working: now.some((l) => !l.paused), waiting: waits.length } });
182
183
  }
183
- overviewKept.set(root, { at: Date.now(), key, value });
184
+ overviewKept.set(root, { at: Date.now(), key, value, tree, waits });
184
185
  return { ...value, ...day, ...latest };
185
186
  }
187
+ /** One of what waits as the phone shows it, whole; a text of nothing but IDs, paths and links: said in general. */
188
+ function waitingLine(tree, { item }) {
189
+ const on = item.on ? forPhone(humanTask(tree, item.on, tree.lang)) : "";
190
+ return { what: forPhone(humanTask(tree, item.what, tree.lang)) || ui(tree.lang, "ui.phone.waiting.item"), ...(on ? { on } : {}) };
191
+ }
192
+ /**
193
+ * More of what waits than the overview gives: `limit` of them (at most WAITING_ASKED) from `from`, of the same list the
194
+ * overview of the tree began (kept as long as it is), each text as cleaned for the phone; n: how many wait in all.
195
+ */
196
+ export function treeWaiting(root, env, from, limit) {
197
+ treeOverview(root, env);
198
+ const kept = overviewKept.get(root);
199
+ if (!kept?.tree)
200
+ return { n: 0, items: [] };
201
+ const start = Math.max(0, Math.floor(from));
202
+ const tree = kept.tree;
203
+ return { n: kept.waits.length, items: kept.waits.slice(start, start + Math.min(WAITING_ASKED, Math.max(0, Math.floor(limit)))).map((w) => waitingLine(tree, w)) };
204
+ }
186
205
  /** The last changes the research saved, newest first, as the bridge's /log gives them (each commit, its lines). */
187
206
  export function treeLog(root, env, limit = LOG_PHONE) {
188
207
  const h = head(root);
@@ -199,7 +218,7 @@ export function treeLog(root, env, limit = LOG_PHONE) {
199
218
  id: e.head.slice(0, 12),
200
219
  at: e.at,
201
220
  lines: lines.slice(0, LINES_PER_CHANGE),
202
- ...(lines.length > LINES_PER_CHANGE ? { more: lines.length - LINES_PER_CHANGE } : {}),
221
+ ...(lines.length > LINES_PER_CHANGE ? { more: lines.length - LINES_PER_CHANGE, rest: lines.slice(LINES_PER_CHANGE, LINES_PER_CHANGE + LINES_KEPT) } : {}),
203
222
  }));
204
223
  const value = { changes, more: all.length > limit };
205
224
  logKept.set(root, { head: h, limit, value });
package/dist/core/tree.js CHANGED
@@ -27,7 +27,7 @@ import { lastBackup } from "./backup.js";
27
27
  export const TREE_FILE = "strom.json";
28
28
  // between releases the candidate being made (a bridge run from the sources says what it is: the Strom app goes by it);
29
29
  // at a release the release's own (package.json)
30
- export const VERSION = "1.18.0-beta.5";
30
+ export const VERSION = "1.18.0-beta.7";
31
31
  const GITIGNORE = `# Strom Research — local state and large files are not versioned
32
32
  .strom/
33
33
  /media/
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "strom-research",
3
- "version": "1.18.0-beta.5",
3
+ "version": "1.18.0-beta.7",
4
4
  "description": "Genealogical research toolkit for AI coding agents",
5
5
  "type": "module",
6
6
  "bin": {