strom-research 1.18.0-beta.7 → 1.18.0-beta.8
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 +7 -2
- package/CHANGELOG.de.md +8 -3
- package/CHANGELOG.md +6 -3
- package/assets/lang/cs.json +20 -0
- package/assets/lang/de.json +20 -0
- package/assets/phone/index.html +15 -2
- package/assets/phone/phone.css +20 -1
- package/assets/phone/phone.js +184 -1
- package/dist/cli/main.js +17 -0
- package/dist/cli/ui.js +20 -0
- package/dist/core/changelog.js +10 -8
- package/dist/core/live-read.js +2 -2
- package/dist/core/phone-finds.js +273 -0
- package/dist/core/phone-send.js +14 -0
- package/dist/core/phone-watch.js +5 -0
- package/dist/core/tree.js +1 -1
- package/dist/core/userpath.js +57 -0
- package/package.json +1 -1
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.
|
|
10
|
+
*Připravuje se: bety do 1.18.0-beta.8. 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
|
|
@@ -22,6 +22,8 @@ Změny každé verze ve zkratce. Úplné poznámky a instalační řádky jsou n
|
|
|
22
22
|
vypadne — i příjmení, nikdy se nebere od otce ani z jiné rodiny —, co nejde přečíst, je označené; druhý čtenář
|
|
23
23
|
může číst naslepo. Do stromu se z toho nic nezapíše samo. Dokument už přečtený to o sobě řekne, takže se omylem
|
|
24
24
|
nečte podruhé. Dokument čtený znovu se ukazuje pod názvem svého souboru, nikdy pod kódem.
|
|
25
|
+
Dokument poslaný z aplikace Strom najde agenta stejně jako terminál — i nainstalovaného do vlastní složky, přes npm
|
|
26
|
+
nebo Homebrew — místo hlášení, že agent není nastavený.
|
|
25
27
|
- **Fotky z telefonu.** `strom phone on`, které zapíná jen člověk, pustí k počítači telefon v domácí síti (Wi-Fi
|
|
26
28
|
nebo kabel — nikdy mobilní síť ani VPN). Telefon se jednou spáruje přes QR kód; jeho stránka pak fotí nebo vybírá
|
|
27
29
|
z galerie a posílá fotky celé do stromu výzkumu, s poznámkou a osobou, kterou ukazují — stejně jako „Přidat
|
|
@@ -32,7 +34,10 @@ Změny každé verze ve zkratce. Úplné poznámky a instalační řádky jsou n
|
|
|
32
34
|
ú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
35
|
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
36
|
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).
|
|
37
|
+
starší než dnešek má u času i datum (rok jen u loňských a starších). Nálezy pod Teď ukazují, co výzkum našel od
|
|
38
|
+
posledního pohledu z telefonu (aspoň posledních 24 hodin, starší klepnutím) — nové osoby, prameny, nalezené, opravené
|
|
39
|
+
či nově doložené údaje, vyřešené rozpory — nové zvýrazněné; co přibude při otevřené stránce, ukáže jen počet
|
|
40
|
+
v názvu záložky a malý štítek.
|
|
36
41
|
- **Souhlas v jednom jazyce.** Souhlas, který dává jen člověk (rozbalení výzkumu, odebrání stromu, odmítnuté okno),
|
|
37
42
|
zazní celý v jazyce výzkumu, nic z něj nezůstane anglicky.
|
|
38
43
|
- **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.
|
|
10
|
+
*In Vorbereitung: die Betas bis 1.18.0-beta.8. 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
|
|
@@ -23,7 +23,9 @@ Die Änderungen jeder Version in Kürze. Die vollständigen Hinweise und die Ins
|
|
|
23
23
|
weg — auch ein Nachname, nie vom Vater oder aus einer anderen Familie genommen —, Unleserliches wird markiert; ein
|
|
24
24
|
zweiter Leser kann blind lesen. In den Stammbaum gelangt nichts von selbst. Ein schon gelesenes Dokument sagt es,
|
|
25
25
|
so wird es nicht versehentlich ein zweites Mal gelesen. Ein erneut gelesenes Dokument wird unter dem Namen seiner
|
|
26
|
-
Datei gezeigt, nie unter einem Code.
|
|
26
|
+
Datei gezeigt, nie unter einem Code. Ein aus der Strom-App gesendetes Dokument findet den Agenten wie das Terminal —
|
|
27
|
+
auch einen in einem eigenen Ordner, über npm oder Homebrew installierten —, statt zu melden, es sei kein Agent
|
|
28
|
+
eingerichtet.
|
|
27
29
|
- **Fotos vom Handy.** `strom phone on`, das nur ein Mensch einschaltet, lässt ein Handy im Heimnetz (WLAN oder
|
|
28
30
|
Kabel — nie ein Mobilfunknetz oder ein VPN) zu diesem Computer. Das Handy wird einmal per QR-Code gekoppelt; seine
|
|
29
31
|
Seite fotografiert dann oder wählt aus der Galerie und schickt die Fotos ganz in einen Stammbaum der Forschung, mit
|
|
@@ -37,7 +39,10 @@ Die Änderungen jeder Version in Kürze. Die vollständigen Hinweise und die Ins
|
|
|
37
39
|
das sie ohne neues Koppeln öffnet. Was wartet und die Änderungen lassen sich auf dem Handy ganz lesen: kein Text
|
|
38
40
|
dessen, was wartet, wird gekürzt, mehr als die ersten 20 kommen auf Antippen, eine Änderung mit vielen Zeilen klappt
|
|
39
41
|
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).
|
|
42
|
+
Jahr nur, wenn es nicht das laufende ist). Funde unter Jetzt zeigen, was die Forschung seit dem letzten Blick vom
|
|
43
|
+
Handy gefunden hat (mindestens die letzten 24 Stunden, ältere auf Antippen) — neue Personen, Quellen, gefundene,
|
|
44
|
+
korrigierte oder neu belegte Angaben, geklärte Widersprüche —, die neuen hervorgehoben; was bei offener Seite
|
|
45
|
+
hinzukommt, zeigt nur eine Zahl im Titel des Tabs und ein kleines Etikett.
|
|
41
46
|
- **Eine Zustimmung in einer Sprache.** Eine Zustimmung, die nur ein Mensch gibt (eine Forschung entpacken, einen
|
|
42
47
|
Stammbaum entfernen, ein abgelehntes Fenster), steht ganz in der Sprache der Forschung, nichts davon bleibt
|
|
43
48
|
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.
|
|
10
|
+
*In preparation: the betas up to 1.18.0-beta.8. 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
|
|
@@ -22,7 +22,8 @@ The changes of each version in short. The full notes and the install lines are o
|
|
|
22
22
|
a name the document does not have is left out — a surname too, never taken from a father or another family —,
|
|
23
23
|
what cannot be read is marked; a second reader can read it blind. Nothing of it enters the tree by itself. A
|
|
24
24
|
document read already says so, so it is not read a second time by mistake. A document read again is shown under
|
|
25
|
-
the name of its file, never a code.
|
|
25
|
+
the name of its file, never a code. A document sent from the Strom app finds the agent as the terminal does — one
|
|
26
|
+
installed in a folder of its own, by npm or Homebrew too — instead of saying no agent is set up.
|
|
26
27
|
- **Photos from a phone.** `strom phone on`, turned on by the person alone, lets a phone on the home network (Wi-Fi
|
|
27
28
|
or a cable — never a mobile network or a VPN) reach this computer. The phone is paired once by its QR code; its
|
|
28
29
|
page then takes photos or picks them from the gallery and sends them whole into a tree of the research, with a
|
|
@@ -35,7 +36,9 @@ The changes of each version in short. The full notes and the install lines are o
|
|
|
35
36
|
the home screen as an icon that opens it without pairing again. What waits and the changes read whole on the phone:
|
|
36
37
|
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
38
|
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).
|
|
39
|
+
it is not this one). Finds, under Now, say what the research found since the phone looked last (the last 24 hours
|
|
40
|
+
at least, older ones on a tap) — new people, sources, facts found, corrected or newly cited, conflicts resolved —
|
|
41
|
+
the new ones marked; something new while the page is open shows only as a count in the tab's title and a small label.
|
|
39
42
|
- **A consent in one language.** A consent only a person gives (unpacking a research, removing a tree, a window said
|
|
40
43
|
no to) is said whole in the research's language, nothing of it left in English.
|
|
41
44
|
- **An update said no to.** When `strom update` is declined, it says nothing was installed and which version stays,
|
package/assets/lang/cs.json
CHANGED
|
@@ -1416,6 +1416,26 @@
|
|
|
1416
1416
|
"ui.phone.page.log.lines": "{n, plural, one {a # další} few {a # další} other {a # dalších}}",
|
|
1417
1417
|
"ui.phone.page.log.fold": "Sbalit",
|
|
1418
1418
|
"ui.phone.page.log.none": "Zatím se nic neuložilo.",
|
|
1419
|
+
"ui.phone.page.finds": "Nálezy",
|
|
1420
|
+
"ui.phone.page.finds.since": "od {time}",
|
|
1421
|
+
"ui.phone.page.finds.none": "Nic nového se nenašlo.",
|
|
1422
|
+
"ui.phone.page.finds.older": "Ukázat starší nálezy",
|
|
1423
|
+
"ui.phone.page.finds.loading": "Načítá se…",
|
|
1424
|
+
"ui.phone.page.finds.cut": "{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}}",
|
|
1425
|
+
"ui.phone.page.finds.sources": "Nové prameny",
|
|
1426
|
+
"ui.phone.page.finds.resolved": "Vyřešeno",
|
|
1427
|
+
"ui.phone.page.finds.kind.person": "Nově v rodokmenu",
|
|
1428
|
+
"ui.phone.page.finds.kind.family": "Nová rodina v rodokmenu",
|
|
1429
|
+
"ui.phone.page.finds.kind.name": "Jméno: {text}",
|
|
1430
|
+
"ui.phone.page.finds.kind.child": "Dítě: {text}",
|
|
1431
|
+
"ui.phone.page.finds.kind.fact": "{text}",
|
|
1432
|
+
"ui.phone.page.finds.kind.fixed": "{text} · opraveno",
|
|
1433
|
+
"ui.phone.page.finds.kind.cited": "{text} · nově doloženo",
|
|
1434
|
+
"ui.phone.page.finds.kind.source": "{text}",
|
|
1435
|
+
"ui.phone.page.finds.kind.conflict": "Rozpor: {text}",
|
|
1436
|
+
"ui.phone.page.finds.kind.hypothesis": "Otázka: {text}",
|
|
1437
|
+
"ui.phone.page.finds.newMark": "nové",
|
|
1438
|
+
"ui.phone.page.finds.new": "{n, plural, one {# novinka ↓} few {# novinky ↓} other {# novinek ↓}}",
|
|
1419
1439
|
"ui.settings.phone": "Propojení s telefonem (fotky z telefonu do výzkumu v místní síti): {state}",
|
|
1420
1440
|
"ui.settings.phone.on": "zapnuto, spárované telefony: {n}",
|
|
1421
1441
|
"ui.settings.phone.error": "zapnuto, nedostupné",
|
package/assets/lang/de.json
CHANGED
|
@@ -1416,6 +1416,26 @@
|
|
|
1416
1416
|
"ui.phone.page.log.lines": "{n, plural, one {und # weitere} other {und # weitere}}",
|
|
1417
1417
|
"ui.phone.page.log.fold": "Weniger zeigen",
|
|
1418
1418
|
"ui.phone.page.log.none": "Noch nichts gespeichert.",
|
|
1419
|
+
"ui.phone.page.finds": "Funde",
|
|
1420
|
+
"ui.phone.page.finds.since": "seit {time}",
|
|
1421
|
+
"ui.phone.page.finds.none": "Nichts Neues gefunden.",
|
|
1422
|
+
"ui.phone.page.finds.older": "Ältere Funde zeigen",
|
|
1423
|
+
"ui.phone.page.finds.loading": "Wird geladen…",
|
|
1424
|
+
"ui.phone.page.finds.cut": "{n, plural, one {und # weiterer in der Forschungsübersicht am Computer} other {und # weitere in der Forschungsübersicht am Computer}}",
|
|
1425
|
+
"ui.phone.page.finds.sources": "Neue Quellen",
|
|
1426
|
+
"ui.phone.page.finds.resolved": "Geklärt",
|
|
1427
|
+
"ui.phone.page.finds.kind.person": "Neu im Stammbaum",
|
|
1428
|
+
"ui.phone.page.finds.kind.family": "Neue Familie im Stammbaum",
|
|
1429
|
+
"ui.phone.page.finds.kind.name": "Name: {text}",
|
|
1430
|
+
"ui.phone.page.finds.kind.child": "Kind: {text}",
|
|
1431
|
+
"ui.phone.page.finds.kind.fact": "{text}",
|
|
1432
|
+
"ui.phone.page.finds.kind.fixed": "{text} · korrigiert",
|
|
1433
|
+
"ui.phone.page.finds.kind.cited": "{text} · neu belegt",
|
|
1434
|
+
"ui.phone.page.finds.kind.source": "{text}",
|
|
1435
|
+
"ui.phone.page.finds.kind.conflict": "Widerspruch: {text}",
|
|
1436
|
+
"ui.phone.page.finds.kind.hypothesis": "Frage: {text}",
|
|
1437
|
+
"ui.phone.page.finds.newMark": "neu",
|
|
1438
|
+
"ui.phone.page.finds.new": "{n, plural, one {# neu ↓} other {# neu ↓}}",
|
|
1419
1439
|
"ui.settings.phone": "Verbindung mit dem Telefon (Fotos vom Telefon in die Forschung im lokalen Netz): {state}",
|
|
1420
1440
|
"ui.settings.phone.on": "an, gekoppelte Telefone: {n}",
|
|
1421
1441
|
"ui.settings.phone.error": "an, nicht erreichbar",
|
package/assets/phone/index.html
CHANGED
|
@@ -40,6 +40,12 @@
|
|
|
40
40
|
<button id="treeBtn" class="tree" type="button" aria-haspopup="dialog"><span id="treeName"></span><span class="caret" aria-hidden="true">▼</span></button>
|
|
41
41
|
<div class="status"><span id="dot" class="dot"></span><span id="statusText"></span></div>
|
|
42
42
|
</header>
|
|
43
|
+
<!-- something new while the page is open: a small label under the header (no sound, no vibration, no dialog); the
|
|
44
|
+
progress's label of changes above stacked under it, never over it -->
|
|
45
|
+
<div class="notes">
|
|
46
|
+
<div class="findnote" aria-live="polite"><button id="findPill" class="findpill" type="button" hidden></button></div>
|
|
47
|
+
<button id="newPill" class="newpill" type="button" hidden></button>
|
|
48
|
+
</div>
|
|
43
49
|
<div id="goneBanner" class="banner" hidden><span id="goneText"></span><button id="goneClose" class="x" type="button">×</button></div>
|
|
44
50
|
<section id="down" class="card down" hidden>
|
|
45
51
|
<h2 id="downTitle"></h2>
|
|
@@ -50,7 +56,7 @@
|
|
|
50
56
|
<p id="noTrees" class="quiet" hidden></p>
|
|
51
57
|
<section id="pairedCard" class="card" hidden><h2 id="pairedTitle"></h2><p id="pairedText" class="quiet"></p></section>
|
|
52
58
|
<section id="archiveCard" class="card" hidden><p id="archiveText" class="strong"></p><p id="archivePhotos" class="quiet"></p></section>
|
|
53
|
-
<!-- what the research does now, what
|
|
59
|
+
<!-- what the research does now, what it found, the last 24 hours, the progress and what waits: to look at alone (nothing here
|
|
54
60
|
answers, starts or decides anything) -->
|
|
55
61
|
<div id="overview" hidden>
|
|
56
62
|
<section id="nowCard" class="card">
|
|
@@ -58,6 +64,14 @@
|
|
|
58
64
|
<ul id="nowLines" class="lines"></ul>
|
|
59
65
|
<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
66
|
</section>
|
|
67
|
+
<!-- what the research found since this phone looked last (the last 24 hours at least), the new ones marked -->
|
|
68
|
+
<section id="findsCard" class="card">
|
|
69
|
+
<h2 class="cap"><span id="findsTitle"></span> <span id="findsSince" class="since-label"></span></h2>
|
|
70
|
+
<p id="findsNone" class="quiet" hidden></p>
|
|
71
|
+
<ul id="findsList" class="finds"></ul>
|
|
72
|
+
<p id="findsCut" class="quiet small" hidden></p>
|
|
73
|
+
<p class="morerow"><button id="findsOlder" class="linkbtn" type="button" hidden></button></p>
|
|
74
|
+
</section>
|
|
61
75
|
<section id="dayCard" class="card" hidden>
|
|
62
76
|
<h2 id="dayTitle" class="cap"></h2>
|
|
63
77
|
<div class="day">
|
|
@@ -91,7 +105,6 @@
|
|
|
91
105
|
<p><button id="homeClose" class="btn outline small" type="button"></button></p>
|
|
92
106
|
</section>
|
|
93
107
|
</div>
|
|
94
|
-
<button id="newPill" class="newpill" type="button" hidden></button>
|
|
95
108
|
<button id="strip" class="strip" type="button" hidden><span class="strip-bar"><span id="stripFill"></span></span><span id="stripText"></span></button>
|
|
96
109
|
<footer class="bar">
|
|
97
110
|
<button id="photoBtn" class="btn primary" type="button"></button>
|
package/assets/phone/phone.css
CHANGED
|
@@ -82,6 +82,25 @@ button{font:inherit;color:inherit}
|
|
|
82
82
|
.morerow{margin:6px 0 0}
|
|
83
83
|
.morerow .linkbtn{padding:10px 0;min-height:44px;text-align:left}
|
|
84
84
|
.morerow .linkbtn:disabled{color:var(--text-faint);text-decoration:none;cursor:default}
|
|
85
|
+
.finds li{padding:8px 0;border-top:1px solid var(--divider)}
|
|
86
|
+
.finds li:first-child{border-top:0;padding-top:0}
|
|
87
|
+
.finds .head{display:flex;align-items:baseline;gap:8px}
|
|
88
|
+
.finds .who{flex:1;min-width:0;font-size:15px;font-weight:600;overflow-wrap:anywhere}
|
|
89
|
+
.finds time{flex:none;overflow-wrap:normal;font-size:12.5px;color:var(--text-faint);font-variant-numeric:tabular-nums}
|
|
90
|
+
.finds p{margin:2px 0 0;font-size:14px;color:var(--text-light);overflow-wrap:anywhere}
|
|
91
|
+
.finds p.new{color:var(--text)}
|
|
92
|
+
.finds .nd{display:inline-block;width:7px;height:7px;border-radius:50%;background:var(--accent);margin-right:6px;vertical-align:middle}
|
|
93
|
+
.finds li.new{background:var(--primary-soft);border-radius:8px;margin:0 -8px;padding:8px;border-top-color:transparent}
|
|
94
|
+
.since-label{text-transform:none;letter-spacing:0;font-weight:400;color:var(--text-faint)}
|
|
95
|
+
#findsCard.none{display:flex;align-items:baseline;gap:12px;flex-wrap:wrap}
|
|
96
|
+
#findsCard.none .cap,#findsCard.none p{margin:0}
|
|
97
|
+
#findsCut{margin:10px 0 0}
|
|
98
|
+
#findsCard,#waitCard{scroll-margin-top:calc(80px + env(safe-area-inset-top))}
|
|
99
|
+
/* the labels of something new: one sticky column under the header, the finds' first, the progress's under it (never
|
|
100
|
+
one over the other); of no height itself, so nothing under it is covered but the labels */
|
|
101
|
+
.notes{position:sticky;top:calc(65px + env(safe-area-inset-top));z-index:5;height:0;display:flex;flex-direction:column;align-items:center}
|
|
102
|
+
.findnote{display:flex;justify-content:center}
|
|
103
|
+
.findpill{margin-top:8px;border:1px solid var(--border);border-radius:999px;background:var(--surface);color:var(--primary-dark);padding:0 14px;min-height:32px;font-size:13px;font-weight:600;cursor:pointer;box-shadow:0 1px 4px rgba(20,16,8,.15)}
|
|
85
104
|
.day{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:8px}
|
|
86
105
|
.day b{display:block;font-size:20px;font-weight:600;font-variant-numeric:tabular-nums}
|
|
87
106
|
.day span{font-size:12.5px;color:var(--text-light)}
|
|
@@ -92,7 +111,7 @@ button{font:inherit;color:inherit}
|
|
|
92
111
|
.log .more{font-size:13px;color:var(--text-faint)}
|
|
93
112
|
.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}
|
|
94
113
|
.log li.fresh{background:var(--primary-soft)}
|
|
95
|
-
.newpill{
|
|
114
|
+
.newpill{flex:none;margin-top:8px;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)}
|
|
96
115
|
|
|
97
116
|
/* the bar below: two buttons, the photos waiting above them */
|
|
98
117
|
.bar{position:fixed;left:0;right:0;bottom:0;z-index:6;display:flex;gap:10px;padding:12px 16px max(12px,env(safe-area-inset-bottom));background:var(--bg);max-width:560px;margin:0 auto}
|
package/assets/phone/phone.js
CHANGED
|
@@ -168,6 +168,12 @@
|
|
|
168
168
|
logLatest: "",
|
|
169
169
|
waitLoaded: null, // more of what waits, asked for on a tap: {tree, of (what the overview gave then), n, items}
|
|
170
170
|
waitBusy: false,
|
|
171
|
+
finds: null, // what the research found: {tree, key, since, groups, cut, earlier}
|
|
172
|
+
findsWin: {}, // the window of each tree: {since, base (what came after it is marked new)}
|
|
173
|
+
findsBusy: false,
|
|
174
|
+
seenMem: {}, // the last look at the finds of each tree, where the browser keeps nothing
|
|
175
|
+
waitBase: {}, // how much waited for each tree when it was looked at last
|
|
176
|
+
dwell: { finds: 0, wait: 0 }, // seconds each part has been in sight
|
|
171
177
|
newCount: 0, // changes added above while scrolled down
|
|
172
178
|
returning: false, // the page looked at again: the first answer after it awaited
|
|
173
179
|
};
|
|
@@ -264,6 +270,7 @@
|
|
|
264
270
|
S.shown = r.body.shown || null;
|
|
265
271
|
if (S.shown && S.shown.latest && (S.logTree !== S.tree || S.logLatest !== S.shown.latest)) void loadLog(S.tree, S.shown.latest);
|
|
266
272
|
keepWait();
|
|
273
|
+
wantFinds();
|
|
267
274
|
} else {
|
|
268
275
|
S.shown = null;
|
|
269
276
|
setTimeout(() => void refresh(), 0);
|
|
@@ -637,6 +644,7 @@
|
|
|
637
644
|
};
|
|
638
645
|
|
|
639
646
|
const render = () => {
|
|
647
|
+
notice();
|
|
640
648
|
if (S.view === "unpaired") return renderUnpaired();
|
|
641
649
|
if (S.view !== "main") return;
|
|
642
650
|
renderHeader();
|
|
@@ -707,7 +715,7 @@
|
|
|
707
715
|
$("overview").hidden = !tree || !o;
|
|
708
716
|
if (!tree || !o) return;
|
|
709
717
|
const archive = !!tree.archive;
|
|
710
|
-
$("nowCard").hidden = $("waitCard").hidden = archive;
|
|
718
|
+
$("nowCard").hidden = $("waitCard").hidden = $("findsCard").hidden = archive;
|
|
711
719
|
const lines = Array.isArray(o.now) ? o.now : [];
|
|
712
720
|
if (!archive && changed("now", [lines, o.lastWorked, new Date().toDateString()])) {
|
|
713
721
|
$("nowTitle").textContent = lines.length > 1 ? t("now.lines", { n: lines.length }) : t("now");
|
|
@@ -732,6 +740,7 @@
|
|
|
732
740
|
if (o.lastWorked) $("nowIdleLast").textContent = t("now.idleLast", { when: whenOf(o.lastWorked) });
|
|
733
741
|
}
|
|
734
742
|
if (!archive) drawWait();
|
|
743
|
+
if (!archive) drawFinds();
|
|
735
744
|
$("dayCard").hidden = !o.day;
|
|
736
745
|
if (o.day && changed("day", o.day)) {
|
|
737
746
|
const plus = (n) => (n > 0 ? "+" : "") + count(n);
|
|
@@ -778,6 +787,170 @@
|
|
|
778
787
|
$("waitWhere").hidden = !w.n;
|
|
779
788
|
};
|
|
780
789
|
|
|
790
|
+
// ── the finds: since this phone looked last (its browser keeps when, for each tree), the last 24 hours at least ──
|
|
791
|
+
const DAY_MS = 86_400_000;
|
|
792
|
+
// a look longer ago than this: the last 24 hours (older ones on a tap), all of them new
|
|
793
|
+
const LONG_MS = 7 * DAY_MS;
|
|
794
|
+
const seenKey = (tree) => `strom.phone.seen.${tree}`;
|
|
795
|
+
const later = (a, b) => (a > b ? a : b);
|
|
796
|
+
const seenOf = (tree) => {
|
|
797
|
+
const v = later(store.get(seenKey(tree)) || "", S.seenMem[tree] || "");
|
|
798
|
+
return Number.isFinite(Date.parse(v)) ? v : "";
|
|
799
|
+
};
|
|
800
|
+
// the window of a tree, set as it is opened: from its last look, the last 24 hours at least; no look known: the last
|
|
801
|
+
// 24 hours, none of them new (what comes after the first answer is)
|
|
802
|
+
const findsWindow = (tree, now = Date.now()) => {
|
|
803
|
+
let w = S.findsWin[tree];
|
|
804
|
+
if (!w) {
|
|
805
|
+
const seen = seenOf(tree);
|
|
806
|
+
const day = new Date(now - DAY_MS).toISOString();
|
|
807
|
+
w = S.findsWin[tree] = { since: seen && now - Date.parse(seen) < LONG_MS && seen < day ? seen : day, base: seen };
|
|
808
|
+
}
|
|
809
|
+
return w;
|
|
810
|
+
};
|
|
811
|
+
const newest = (groups) => groups.reduce((m, g) => later(m, String(g.at || "")), "");
|
|
812
|
+
const isNew = (at) => {
|
|
813
|
+
const w = S.findsWin[S.tree];
|
|
814
|
+
return !!w && !!w.base && at > w.base;
|
|
815
|
+
};
|
|
816
|
+
// asked when the tree's newest change or the window is another
|
|
817
|
+
let findsAsking = null;
|
|
818
|
+
const loadFinds = (tree, latest) =>
|
|
819
|
+
(findsAsking ??= (async () => {
|
|
820
|
+
const w = findsWindow(tree);
|
|
821
|
+
const key = `${tree}|${latest}|${w.since}`;
|
|
822
|
+
// a fact said in the page's language (its name and date as the rest of the line)
|
|
823
|
+
const r = await ask(`/phone/finds?tree=${encodeURIComponent(tree)}&since=${encodeURIComponent(w.since)}&lang=${encodeURIComponent(LANG)}`);
|
|
824
|
+
findsAsking = null;
|
|
825
|
+
S.findsBusy = false;
|
|
826
|
+
const ok = r.status === 200 && Array.isArray(r.body.groups);
|
|
827
|
+
if (ok && tree === S.tree) {
|
|
828
|
+
if (!w.base) w.base = newest(r.body.groups) || w.since;
|
|
829
|
+
S.finds = { tree, key, since: typeof r.body.since === "string" ? r.body.since : w.since, groups: r.body.groups, cut: Number(r.body.cut) || 0, earlier: typeof r.body.earlier === "string" ? r.body.earlier : "" };
|
|
830
|
+
}
|
|
831
|
+
drawFinds();
|
|
832
|
+
notice();
|
|
833
|
+
// another tree, a newer change or another window meanwhile: asked again for it (a failure: at the next answer)
|
|
834
|
+
if (ok || tree !== S.tree) wantFinds();
|
|
835
|
+
})());
|
|
836
|
+
const wantFinds = () => {
|
|
837
|
+
const tree = treeById(S.tree);
|
|
838
|
+
if (!tree || tree.archive || !S.shown || !S.shown.latest) return;
|
|
839
|
+
const w = findsWindow(S.tree);
|
|
840
|
+
if (!S.finds || S.finds.key !== `${S.tree}|${S.shown.latest}|${w.since}`) void loadFinds(S.tree, S.shown.latest);
|
|
841
|
+
};
|
|
842
|
+
const drawFind = (g) => {
|
|
843
|
+
const li = el("li");
|
|
844
|
+
const head = el("div", "head");
|
|
845
|
+
head.append(el("span", "who", g.kind === "sources" ? t("finds.sources") : g.kind === "resolved" ? t("finds.resolved") : String(g.title || "")));
|
|
846
|
+
const time = el("time", "", logTime(Date.parse(g.at)));
|
|
847
|
+
time.dateTime = g.at;
|
|
848
|
+
head.append(time);
|
|
849
|
+
li.append(head);
|
|
850
|
+
for (const l of Array.isArray(g.lines) ? g.lines : []) {
|
|
851
|
+
const p = el("p");
|
|
852
|
+
if (isNew(l.at)) {
|
|
853
|
+
p.className = "new";
|
|
854
|
+
li.className = "new";
|
|
855
|
+
const dot = el("span", "nd");
|
|
856
|
+
dot.setAttribute("aria-hidden", "true");
|
|
857
|
+
p.append(dot, el("span", "vh", `${t("finds.newMark")}: `));
|
|
858
|
+
}
|
|
859
|
+
p.append(el("span", "", t(`finds.kind.${l.kind}`, { text: l.text })));
|
|
860
|
+
li.append(p);
|
|
861
|
+
}
|
|
862
|
+
return li;
|
|
863
|
+
};
|
|
864
|
+
const drawFinds = () => {
|
|
865
|
+
const tree = treeById(S.tree);
|
|
866
|
+
if (!tree || tree.archive) return;
|
|
867
|
+
const f = S.finds && S.finds.tree === S.tree ? S.finds : null;
|
|
868
|
+
const w = S.findsWin[S.tree];
|
|
869
|
+
if (!changed("finds", [f && [f.groups, f.cut, f.earlier, f.since], w && w.base, S.findsBusy, !!(S.shown && S.shown.latest), new Date().toDateString()])) return;
|
|
870
|
+
const groups = f ? f.groups : [];
|
|
871
|
+
$("findsTitle").textContent = t("finds");
|
|
872
|
+
$("findsSince").textContent = f ? t("finds.since", { time: logTime(Date.parse(f.since)) }) : "";
|
|
873
|
+
$("findsCard").classList.toggle("none", !groups.length);
|
|
874
|
+
// nothing saved yet, or nothing found in the window: a short line
|
|
875
|
+
$("findsNone").hidden = groups.length > 0 || (!f && !!(S.shown && S.shown.latest));
|
|
876
|
+
$("findsNone").textContent = t("finds.none");
|
|
877
|
+
$("findsList").replaceChildren(...groups.map(drawFind));
|
|
878
|
+
$("findsList").hidden = !groups.length;
|
|
879
|
+
$("findsCut").hidden = !(f && f.cut);
|
|
880
|
+
if (f && f.cut) $("findsCut").textContent = t("finds.cut", { n: f.cut });
|
|
881
|
+
$("findsOlder").hidden = !(f && f.earlier);
|
|
882
|
+
$("findsOlder").disabled = S.findsBusy;
|
|
883
|
+
$("findsOlder").textContent = S.findsBusy ? t("finds.loading") : t("finds.older");
|
|
884
|
+
};
|
|
885
|
+
// older finds: a day before the newest of them (the server looks a month back at most)
|
|
886
|
+
const olderFinds = () => {
|
|
887
|
+
const f = S.finds && S.finds.tree === S.tree ? S.finds : null;
|
|
888
|
+
const w = S.findsWin[S.tree];
|
|
889
|
+
if (!f || !f.earlier || !w || S.findsBusy) return;
|
|
890
|
+
w.since = new Date(Date.parse(f.earlier) - DAY_MS).toISOString();
|
|
891
|
+
S.findsBusy = true;
|
|
892
|
+
drawFinds();
|
|
893
|
+
wantFinds();
|
|
894
|
+
};
|
|
895
|
+
|
|
896
|
+
// ── something new while the page is open: its count in the tab's title and a small label (no sound, no vibration,
|
|
897
|
+
// no dialog, no notification); gone once the part it is in has been in sight a while ──
|
|
898
|
+
const BASE_TITLE = document.title;
|
|
899
|
+
const unseen = () => {
|
|
900
|
+
const tree = treeById(S.tree);
|
|
901
|
+
if (!tree || tree.archive || S.view !== "main") return { finds: 0, wait: 0 };
|
|
902
|
+
const f = S.finds && S.finds.tree === S.tree ? S.finds : null;
|
|
903
|
+
const w = S.findsWin[S.tree];
|
|
904
|
+
const ref = later(seenOf(S.tree), (w && w.base) || "");
|
|
905
|
+
const finds = f && ref ? f.groups.reduce((n, g) => n + (Array.isArray(g.lines) ? g.lines : []).filter((l) => l.at > ref).length, 0) : 0;
|
|
906
|
+
const n = S.shown && S.shown.waiting ? Number(S.shown.waiting.n) || 0 : 0;
|
|
907
|
+
const base = S.waitBase[S.tree];
|
|
908
|
+
return { finds, wait: base === undefined ? 0 : Math.max(0, n - base) };
|
|
909
|
+
};
|
|
910
|
+
const notice = () => {
|
|
911
|
+
// what waits: counted from what waited as the tree was opened (fewer since: from those)
|
|
912
|
+
const n = S.shown && S.shown.waiting && S.view === "main" ? Number(S.shown.waiting.n) || 0 : null;
|
|
913
|
+
if (n !== null && (S.waitBase[S.tree] === undefined || n < S.waitBase[S.tree])) S.waitBase[S.tree] = n;
|
|
914
|
+
const u = unseen();
|
|
915
|
+
const all = u.finds + u.wait;
|
|
916
|
+
const title = all ? `(${count(all)}) ${BASE_TITLE}` : BASE_TITLE;
|
|
917
|
+
if (document.title !== title) document.title = title;
|
|
918
|
+
$("findPill").hidden = !all;
|
|
919
|
+
const text = all ? t("finds.new", { n: all }) : "";
|
|
920
|
+
if (all && $("findPill").textContent !== text) $("findPill").textContent = text;
|
|
921
|
+
};
|
|
922
|
+
// in sight: on the screen, the page in front, no sheet over it
|
|
923
|
+
const inView = (e) => {
|
|
924
|
+
const r = e.getBoundingClientRect();
|
|
925
|
+
return r.height > 0 && r.bottom > 0 && r.top < (window.innerHeight || 0);
|
|
926
|
+
};
|
|
927
|
+
const DWELL_S = 2;
|
|
928
|
+
// called every second: what has been in sight DWELL_S seconds is seen (the finds by their newest, what waits by its count)
|
|
929
|
+
const looked = () => {
|
|
930
|
+
const tree = S.tree;
|
|
931
|
+
const showing = S.view === "main" && document.visibilityState !== "hidden" && !S.sheet && $("switcher").hidden;
|
|
932
|
+
const f = S.finds && S.finds.tree === tree ? S.finds : null;
|
|
933
|
+
S.dwell.finds = showing && f && f.groups.length && inView($("findsCard")) ? S.dwell.finds + 1 : 0;
|
|
934
|
+
S.dwell.wait = showing && S.shown && S.shown.waiting && inView($("waitCard")) ? S.dwell.wait + 1 : 0;
|
|
935
|
+
let moved = false;
|
|
936
|
+
if (S.dwell.finds >= DWELL_S) {
|
|
937
|
+
const at = newest(f.groups);
|
|
938
|
+
if (at && at > seenOf(tree)) {
|
|
939
|
+
S.seenMem[tree] = at;
|
|
940
|
+
store.set(seenKey(tree), at);
|
|
941
|
+
moved = true;
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
if (S.dwell.wait >= DWELL_S) {
|
|
945
|
+
const n = Number(S.shown.waiting.n) || 0;
|
|
946
|
+
if (S.waitBase[tree] !== n) {
|
|
947
|
+
S.waitBase[tree] = n;
|
|
948
|
+
moved = true;
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
if (moved) notice();
|
|
952
|
+
};
|
|
953
|
+
|
|
781
954
|
// the progress: the changes drawn kept by their id, a new one put on top (lit for 6 s); scrolled down, what is under
|
|
782
955
|
// the finger stays where it is and a pill says how many came above
|
|
783
956
|
const logItems = new Map();
|
|
@@ -1067,6 +1240,9 @@
|
|
|
1067
1240
|
if (S.tree !== id) {
|
|
1068
1241
|
S.shown = null;
|
|
1069
1242
|
S.waitLoaded = null;
|
|
1243
|
+
S.finds = null;
|
|
1244
|
+
S.findsBusy = false;
|
|
1245
|
+
S.dwell = { finds: 0, wait: 0 };
|
|
1070
1246
|
S.logTree = "";
|
|
1071
1247
|
for (const li of logItems.values()) li.remove();
|
|
1072
1248
|
logItems.clear();
|
|
@@ -1281,6 +1457,12 @@
|
|
|
1281
1457
|
homeTimer = setTimeout(homeOff, Math.max(0, Date.parse(r.body.expiresAt) - Date.now()));
|
|
1282
1458
|
};
|
|
1283
1459
|
$("homeClose").onclick = homeOff;
|
|
1460
|
+
$("findsOlder").onclick = olderFinds;
|
|
1461
|
+
// to the finds (only what waits is new: to that)
|
|
1462
|
+
$("findPill").onclick = () => {
|
|
1463
|
+
const u = unseen();
|
|
1464
|
+
$(u.finds || !u.wait ? "findsCard" : "waitCard").scrollIntoView({ block: "start", behavior: "smooth" });
|
|
1465
|
+
};
|
|
1284
1466
|
$("newPill").onclick = () => {
|
|
1285
1467
|
S.newCount = 0;
|
|
1286
1468
|
window.scrollTo({ top: 0, behavior: "smooth" });
|
|
@@ -1302,6 +1484,7 @@
|
|
|
1302
1484
|
if (S.view === "main") {
|
|
1303
1485
|
renderHeader();
|
|
1304
1486
|
restampLog();
|
|
1487
|
+
looked();
|
|
1305
1488
|
if (ticks % 10 === 0 && document.visibilityState !== "hidden") void refresh();
|
|
1306
1489
|
} else if (S.view === "unpaired" && ticks % 10 === 0) void whoami().then((w) => w === "paired" && void refresh());
|
|
1307
1490
|
}, 1000);
|
package/dist/cli/main.js
CHANGED
|
@@ -23,6 +23,7 @@ import { fireHooks, HOOK_INTERFACE } from "../core/hooks.js";
|
|
|
23
23
|
import { prependPath } from "../runners/runner.js";
|
|
24
24
|
import { shimDir } from "../commands/session.js";
|
|
25
25
|
import { isAgent } from "../core/which.js";
|
|
26
|
+
import { completePath, pathToRemember } from "../core/userpath.js";
|
|
26
27
|
import { appUrlShown, noticeStromApp } from "../core/stromapp.js";
|
|
27
28
|
import { lastChannelOf, pendingTransition, updateChannel, whatsNewUrl } from "../core/update.js";
|
|
28
29
|
import { backupBefore, backupSaid } from "./backups.js";
|
|
@@ -220,6 +221,22 @@ export async function main(argv, io, env, cwd) {
|
|
|
220
221
|
.map((x) => `${x[0].toUpperCase()}${x.slice(1).padStart(4, "0")}`);
|
|
221
222
|
ran = ctx;
|
|
222
223
|
command = def.path.join(" ");
|
|
224
|
+
// The PATH of the person's terminal remembered (never an agent's, a hook's, one strom started); a strom the system
|
|
225
|
+
// started (a link from the Strom app, the bridge, the phone's server) gets what of it is missing — and so do the
|
|
226
|
+
// agents and readers it starts (core/userpath.ts)
|
|
227
|
+
if (io.tty && !isAgent(env) && env.STROM_SPAWNED !== "1" && v.hook !== true && ctx.settings.home()) {
|
|
228
|
+
const remember = pathToRemember(env, ctx.settings.config.terminalPath);
|
|
229
|
+
if (remember) {
|
|
230
|
+
try {
|
|
231
|
+
ctx.settings.config.terminalPath = remember;
|
|
232
|
+
ctx.settings.save();
|
|
233
|
+
}
|
|
234
|
+
catch {
|
|
235
|
+
// remembered next time
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
completePath(env, ctx.settings.config.terminalPath);
|
|
223
240
|
// a person's own pace for a host taken as given until a command to an archive finds settings not the person's
|
|
224
241
|
netTrust.ownPace = true;
|
|
225
242
|
// the person's own settings folder (another than their default one, outside an agent): listed as theirs, so that an
|
package/dist/cli/ui.js
CHANGED
|
@@ -1317,6 +1317,26 @@ export const UI = {
|
|
|
1317
1317
|
"ui.phone.page.log.lines": "{n, plural, one {and # more} other {and # more}}",
|
|
1318
1318
|
"ui.phone.page.log.fold": "Show less",
|
|
1319
1319
|
"ui.phone.page.log.none": "Nothing saved yet.",
|
|
1320
|
+
"ui.phone.page.finds": "Finds",
|
|
1321
|
+
"ui.phone.page.finds.since": "since {time}",
|
|
1322
|
+
"ui.phone.page.finds.none": "Nothing new found.",
|
|
1323
|
+
"ui.phone.page.finds.older": "Show older finds",
|
|
1324
|
+
"ui.phone.page.finds.loading": "Loading…",
|
|
1325
|
+
"ui.phone.page.finds.cut": "{n, plural, one {and # more in the Research overview on the computer} other {and # more in the Research overview on the computer}}",
|
|
1326
|
+
"ui.phone.page.finds.sources": "New sources",
|
|
1327
|
+
"ui.phone.page.finds.resolved": "Resolved",
|
|
1328
|
+
"ui.phone.page.finds.kind.person": "New in the tree",
|
|
1329
|
+
"ui.phone.page.finds.kind.family": "New family in the tree",
|
|
1330
|
+
"ui.phone.page.finds.kind.name": "Name: {text}",
|
|
1331
|
+
"ui.phone.page.finds.kind.child": "Child: {text}",
|
|
1332
|
+
"ui.phone.page.finds.kind.fact": "{text}",
|
|
1333
|
+
"ui.phone.page.finds.kind.fixed": "{text} · corrected",
|
|
1334
|
+
"ui.phone.page.finds.kind.cited": "{text} · newly cited",
|
|
1335
|
+
"ui.phone.page.finds.kind.source": "{text}",
|
|
1336
|
+
"ui.phone.page.finds.kind.conflict": "Conflict: {text}",
|
|
1337
|
+
"ui.phone.page.finds.kind.hypothesis": "Question: {text}",
|
|
1338
|
+
"ui.phone.page.finds.newMark": "new",
|
|
1339
|
+
"ui.phone.page.finds.new": "{n, plural, one {# new ↓} other {# new ↓}}",
|
|
1320
1340
|
"ui.settings.phone": "Phone connection (photos from a phone into the research on the local network): {state}",
|
|
1321
1341
|
"ui.settings.phone.on": "on, paired phones: {n}",
|
|
1322
1342
|
"ui.settings.phone.error": "on, unreachable",
|
package/dist/core/changelog.js
CHANGED
|
@@ -144,6 +144,15 @@ function kindOf(op) {
|
|
|
144
144
|
return "story";
|
|
145
145
|
return "other";
|
|
146
146
|
}
|
|
147
|
+
/** A fact of a person or a family as a person reads it ("narození: 3. 4. 1842, Sloup"); undefined when it is not there. */
|
|
148
|
+
export function factSaid(tree, owner, eventId, lang) {
|
|
149
|
+
const e = (owner ? tree.get(owner) : undefined)?.events.find((x) => x.id === eventId);
|
|
150
|
+
if (!e)
|
|
151
|
+
return undefined;
|
|
152
|
+
const f = cardFact(tree, e);
|
|
153
|
+
const what = [f.value, humanDate(f.date, lang), humanPlace(f.place, f.house, lang)].filter(Boolean).join(", ");
|
|
154
|
+
return `${eventName(f.kind, lang, f.label)}${what ? `: ${what}` : ""}`;
|
|
155
|
+
}
|
|
147
156
|
/** Lines of what a commit saved, in the research language, each with what it is about; its subject when it logged no operation. */
|
|
148
157
|
export function changeLines(tree, ops, subject, lang, was) {
|
|
149
158
|
const get = (id) => (id ? tree.get(id) : undefined);
|
|
@@ -198,14 +207,7 @@ export function changeLines(tree, ops, subject, lang, was) {
|
|
|
198
207
|
return r.id;
|
|
199
208
|
}
|
|
200
209
|
};
|
|
201
|
-
const fact = (owner, eventId, at = tree) =>
|
|
202
|
-
const e = (owner ? at.get(owner) : undefined)?.events.find((x) => x.id === eventId);
|
|
203
|
-
if (!e)
|
|
204
|
-
return undefined;
|
|
205
|
-
const f = cardFact(at, e);
|
|
206
|
-
const what = [f.value, humanDate(f.date, lang), humanPlace(f.place, f.house, lang)].filter(Boolean).join(", ");
|
|
207
|
-
return `${eventName(f.kind, lang, f.label)}${what ? `: ${what}` : ""}`;
|
|
208
|
-
};
|
|
210
|
+
const fact = (owner, eventId, at = tree) => factSaid(at, owner, eventId, lang);
|
|
209
211
|
/** A fact as an operation's summary names it, its record gone since ("+E0274 BIRT P0076 MAR 1776 [probable]"). */
|
|
210
212
|
const factOf = (summary) => {
|
|
211
213
|
const m = /^\+E\d+ (\S+) \S+(?: (.+?))? \[\p{L}+\]$/u.exec(summary);
|
package/dist/core/live-read.js
CHANGED
|
@@ -21,7 +21,7 @@ import { readableTypes } from "./readable.js";
|
|
|
21
21
|
import { makeView } from "./views.js";
|
|
22
22
|
import { inputPath } from "./media.js";
|
|
23
23
|
import { sandboxOf } from "./sandbox.js";
|
|
24
|
-
import { AGENTS,
|
|
24
|
+
import { AGENTS, findAgent } from "./which.js";
|
|
25
25
|
import { assetPath } from "./assets.js";
|
|
26
26
|
import { EXIT } from "./errors.js";
|
|
27
27
|
import { forApp, refusal } from "./receive.js";
|
|
@@ -281,7 +281,7 @@ function notReady(tree, settings, env, seen, now) {
|
|
|
281
281
|
const agent = settings.agent(tree.config).value;
|
|
282
282
|
// (the agents' table of core, not the runners': a runner's module reaches the bridge, and so would come round to itself)
|
|
283
283
|
const known = AGENTS.find((a) => a.id === agent);
|
|
284
|
-
if (agent !== "script" && (!known || !
|
|
284
|
+
if (agent !== "script" && (!known || !findAgent(known.command, env)))
|
|
285
285
|
return { why: "agent.none" };
|
|
286
286
|
if (!READ_TESTED.includes(agent))
|
|
287
287
|
return { why: "agent.untested" };
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
// What the research found, for a paired phone's page (strom phone): its Finds since the person last looked. A find is
|
|
2
|
+
// what an agent of the research wrote of the family — never the person's own edits (the Strom app, the terminal) and
|
|
3
|
+
// never the research's upkeep:
|
|
4
|
+
//
|
|
5
|
+
// a new person (person.add, person.import) · a name of a person found (person.name) · a new family, a child found in
|
|
6
|
+
// one (family.add, family.import, family.child) · a fact found, corrected or newly cited (event.add, event.edit,
|
|
7
|
+
// event.cite — not a citation taken off or put back by an undo) · a new source (source.add) · a conflict resolved
|
|
8
|
+
// (conflict.resolve) · a hypothesis decided (hypothesis.decide — not abandoned)
|
|
9
|
+
//
|
|
10
|
+
// Tasks, sessions, images, readings, notes, places, settings, stories and what waits are none. Read from the logs of
|
|
11
|
+
// each day (data/ops/<yyyy-mm>/<dd>/…, never written again once committed), never from git: an agent at work makes a
|
|
12
|
+
// thousand commits a day. Each log is read once and only its finds kept; the answer is kept for the head and the
|
|
13
|
+
// window asked, so the page asking again and again reads nothing new. Records gone since (taken back, merged into
|
|
14
|
+
// another) say nothing; each text by the records as they are now, cleaned for the phone (forPhone).
|
|
15
|
+
import fs from "node:fs";
|
|
16
|
+
import path from "node:path";
|
|
17
|
+
import crypto from "node:crypto";
|
|
18
|
+
import { Tree } from "./tree.js";
|
|
19
|
+
import { actsAsArchive } from "./state.js";
|
|
20
|
+
import { runGit } from "./git.js";
|
|
21
|
+
import { factSaid } from "./changelog.js";
|
|
22
|
+
import { conflictTitle, displayName, lifespan } from "./people.js";
|
|
23
|
+
import { ownerName } from "./overview.js";
|
|
24
|
+
import { forPhone } from "./phone-watch.js";
|
|
25
|
+
/** The window when the page asks for none: the last 24 hours. */
|
|
26
|
+
export const FINDS_HOURS = 24;
|
|
27
|
+
/** How far back the page may ask, and how far back a find before the window is looked for. */
|
|
28
|
+
export const FINDS_BACK_DAYS = 31;
|
|
29
|
+
/** At most so many groups in one answer (the newest); the rest counted. */
|
|
30
|
+
export const FINDS_GROUPS = 100;
|
|
31
|
+
const FIND_OPS = new Set(["person.add", "person.import", "person.name", "family.add", "family.import", "family.child", "event.add", "event.edit", "event.cite", "source.add", "conflict.resolve", "hypothesis.decide"]);
|
|
32
|
+
/** The finds of one log as they were read, by its size (a log of a commit is never written again). */
|
|
33
|
+
const logsRead = new Map();
|
|
34
|
+
/** The answers kept, by the tree, its head and the window (a few: two phones may ask for two windows). */
|
|
35
|
+
const kept = new Map();
|
|
36
|
+
const KEPT = 16;
|
|
37
|
+
/** Is it a find: an agent's (or a session's), of a kind that is one, not an undo. */
|
|
38
|
+
function isFind(o) {
|
|
39
|
+
if (!FIND_OPS.has(o.op) || o.by === "user")
|
|
40
|
+
return false;
|
|
41
|
+
if (/^\S+ cites no more /u.test(o.summary) || (o.op === "event.cite" && /^\S+ cites \S+(?:, \S+)* again: /u.test(o.summary)))
|
|
42
|
+
return false;
|
|
43
|
+
if (o.op === "hypothesis.decide" && / abandoned/u.test(o.summary))
|
|
44
|
+
return false;
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
function findsOfLog(file) {
|
|
48
|
+
let size;
|
|
49
|
+
try {
|
|
50
|
+
size = fs.statSync(file).size;
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return [];
|
|
54
|
+
}
|
|
55
|
+
const held = logsRead.get(file);
|
|
56
|
+
if (held && held.size === size)
|
|
57
|
+
return held.ops;
|
|
58
|
+
const ops = [];
|
|
59
|
+
let text = "";
|
|
60
|
+
try {
|
|
61
|
+
text = fs.readFileSync(file, "utf8");
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
return [];
|
|
65
|
+
}
|
|
66
|
+
for (const line of text.split("\n")) {
|
|
67
|
+
if (!line.trim())
|
|
68
|
+
continue;
|
|
69
|
+
try {
|
|
70
|
+
const o = JSON.parse(line);
|
|
71
|
+
if (o && typeof o.op === "string" && typeof o.at === "string" && Array.isArray(o.targets) && isFind(o))
|
|
72
|
+
ops.push(o);
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
// a line being written this moment: read whole the next time (the size differs)
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
logsRead.set(file, { size, ops });
|
|
79
|
+
return ops;
|
|
80
|
+
}
|
|
81
|
+
function list(dir) {
|
|
82
|
+
try {
|
|
83
|
+
return fs.readdirSync(dir).sort();
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
return [];
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
/** The logs of each day from `from` (a day, yyyy-mm-dd) on, oldest first; the older logs of a month or a session aside. */
|
|
90
|
+
function logsFrom(dataDir, from) {
|
|
91
|
+
const dir = path.join(dataDir, "ops");
|
|
92
|
+
const out = [];
|
|
93
|
+
for (const month of list(dir).filter((n) => /^\d{4}-\d{2}$/u.test(n) && n >= from.slice(0, 7)))
|
|
94
|
+
for (const day of list(path.join(dir, month)).filter((n) => /^\d{2}$/u.test(n) && `${month}-${n}` >= from))
|
|
95
|
+
for (const f of list(path.join(dir, month, day)))
|
|
96
|
+
if (f.endsWith(".jsonl"))
|
|
97
|
+
out.push(path.join(dir, month, day, f));
|
|
98
|
+
return out;
|
|
99
|
+
}
|
|
100
|
+
const hashed = (s) => crypto.createHash("sha256").update(s).digest("hex").slice(0, 12);
|
|
101
|
+
/**
|
|
102
|
+
* The finds of operations, grouped, newest group first; each line once (a fact found and cited: found). A fact is said
|
|
103
|
+
* in `lang` (the page's: its name and its date as the rest of its line; the research language when none).
|
|
104
|
+
*/
|
|
105
|
+
export function findGroups(tree, ops, lang = tree.lang) {
|
|
106
|
+
const groups = new Map();
|
|
107
|
+
// a fact said once, by what happened to it first in the window; a person or family new in it: no line for its names
|
|
108
|
+
const facts = new Map();
|
|
109
|
+
const fresh = new Set();
|
|
110
|
+
const live = (id) => {
|
|
111
|
+
const r = id ? tree.get(id) : undefined;
|
|
112
|
+
if (!r || (r.type !== "person" && r.type !== "family"))
|
|
113
|
+
return undefined;
|
|
114
|
+
return r.retracted || r.mergedInto ? undefined : r;
|
|
115
|
+
};
|
|
116
|
+
const group = (key, make) => {
|
|
117
|
+
let g = groups.get(key);
|
|
118
|
+
if (!g) {
|
|
119
|
+
const made = make();
|
|
120
|
+
if (!made)
|
|
121
|
+
return undefined;
|
|
122
|
+
g = { key: hashed(`${tree.config.id}:${key}`), ...made, at: "", lines: [] };
|
|
123
|
+
groups.set(key, g);
|
|
124
|
+
}
|
|
125
|
+
return g;
|
|
126
|
+
};
|
|
127
|
+
const ofRecord = (r) => group(r.id, () => {
|
|
128
|
+
if (r.type === "person") {
|
|
129
|
+
const span = lifespan(r);
|
|
130
|
+
const title = forPhone(`${displayName(r)}${span ? ` (${span})` : ""}`);
|
|
131
|
+
return title ? { kind: "person", title } : undefined;
|
|
132
|
+
}
|
|
133
|
+
const title = forPhone(ownerName(tree, r.id));
|
|
134
|
+
return title ? { kind: "family", title } : undefined;
|
|
135
|
+
});
|
|
136
|
+
const add = (g, line) => {
|
|
137
|
+
if (!g || g.lines.some((l) => l.kind === line.kind && l.text === line.text))
|
|
138
|
+
return;
|
|
139
|
+
g.lines.push(line);
|
|
140
|
+
if (line.at > g.at)
|
|
141
|
+
g.at = line.at;
|
|
142
|
+
};
|
|
143
|
+
for (const o of [...ops].sort((a, b) => a.at.localeCompare(b.at))) {
|
|
144
|
+
const [first, second] = o.targets;
|
|
145
|
+
const at = o.at;
|
|
146
|
+
switch (o.op) {
|
|
147
|
+
case "person.add":
|
|
148
|
+
case "person.import":
|
|
149
|
+
case "family.add":
|
|
150
|
+
case "family.import": {
|
|
151
|
+
const r = live(first);
|
|
152
|
+
if (!r)
|
|
153
|
+
break;
|
|
154
|
+
fresh.add(r.id);
|
|
155
|
+
add(ofRecord(r), { kind: r.type === "person" ? "person" : "family", text: "", at });
|
|
156
|
+
break;
|
|
157
|
+
}
|
|
158
|
+
case "person.name": {
|
|
159
|
+
const r = live(first);
|
|
160
|
+
const m = /^\S+ name (.+?)(?: \([\w-]+\))?(?: ← \S+)?$/u.exec(o.summary);
|
|
161
|
+
const name = m ? forPhone(m[1].replace(/\//gu, " ").replace(/\s+/gu, " ").trim()) : "";
|
|
162
|
+
if (r && name && !fresh.has(r.id))
|
|
163
|
+
add(ofRecord(r), { kind: "name", text: name, at });
|
|
164
|
+
break;
|
|
165
|
+
}
|
|
166
|
+
case "family.child": {
|
|
167
|
+
const r = live(first);
|
|
168
|
+
const child = live(second ?? /\+child (P\d+)/u.exec(o.summary)?.[1]);
|
|
169
|
+
if (!r || !child || child.type !== "person")
|
|
170
|
+
break;
|
|
171
|
+
const span = lifespan(child);
|
|
172
|
+
const text = forPhone(`${displayName(child)}${span ? ` (${span})` : ""}`);
|
|
173
|
+
if (text)
|
|
174
|
+
add(ofRecord(r), { kind: "child", text, at });
|
|
175
|
+
break;
|
|
176
|
+
}
|
|
177
|
+
case "event.add":
|
|
178
|
+
case "event.edit":
|
|
179
|
+
case "event.cite": {
|
|
180
|
+
const r = live(first);
|
|
181
|
+
const id = second ?? /^\+?(E\d+) /u.exec(o.summary)?.[1];
|
|
182
|
+
const e = r?.events.find((x) => x.id === id);
|
|
183
|
+
if (!r || !e || e.retracted)
|
|
184
|
+
break;
|
|
185
|
+
const said = forPhone(factSaid(tree, r.id, e.id, lang) ?? "");
|
|
186
|
+
if (!said)
|
|
187
|
+
break;
|
|
188
|
+
// a line of its own on the phone: its first letter as a sentence's
|
|
189
|
+
const text = said.charAt(0).toLocaleUpperCase(lang) + said.slice(1);
|
|
190
|
+
const kind = o.op === "event.add" ? "fact" : o.op === "event.edit" ? "fixed" : "cited";
|
|
191
|
+
const was = facts.get(`${r.id}:${e.id}`);
|
|
192
|
+
if (was) {
|
|
193
|
+
// said already: as it is now, at its newest moment
|
|
194
|
+
was.text = text;
|
|
195
|
+
was.at = at;
|
|
196
|
+
const g = ofRecord(r);
|
|
197
|
+
if (g && at > g.at)
|
|
198
|
+
g.at = at;
|
|
199
|
+
break;
|
|
200
|
+
}
|
|
201
|
+
const line = { kind, text, at };
|
|
202
|
+
facts.set(`${r.id}:${e.id}`, line);
|
|
203
|
+
add(ofRecord(r), line);
|
|
204
|
+
break;
|
|
205
|
+
}
|
|
206
|
+
case "source.add": {
|
|
207
|
+
const s = first ? tree.get(first) : undefined;
|
|
208
|
+
const text = s && s.type === "source" && !s.retracted ? forPhone(s.title) : "";
|
|
209
|
+
if (text)
|
|
210
|
+
add(group("sources", () => ({ kind: "sources" })), { kind: "source", text, at });
|
|
211
|
+
break;
|
|
212
|
+
}
|
|
213
|
+
case "conflict.resolve": {
|
|
214
|
+
const c = first ? tree.get(first) : undefined;
|
|
215
|
+
const text = c && c.type === "conflict" && c.state === "resolved" ? forPhone(conflictTitle(tree, c)) : "";
|
|
216
|
+
if (text)
|
|
217
|
+
add(group("resolved", () => ({ kind: "resolved" })), { kind: "conflict", text, at });
|
|
218
|
+
break;
|
|
219
|
+
}
|
|
220
|
+
case "hypothesis.decide": {
|
|
221
|
+
const h = first ? tree.get(first) : undefined;
|
|
222
|
+
const text = h && h.type === "hypothesis" && h.state === "decided" ? forPhone(h.question) : "";
|
|
223
|
+
if (text)
|
|
224
|
+
add(group("resolved", () => ({ kind: "resolved" })), { kind: "hypothesis", text, at });
|
|
225
|
+
break;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
return [...groups.values()].filter((g) => g.lines.length).sort((a, b) => b.at.localeCompare(a.at));
|
|
230
|
+
}
|
|
231
|
+
function head(root) {
|
|
232
|
+
const r = runGit(root, ["rev-parse", "HEAD"]);
|
|
233
|
+
return r.status === 0 ? r.stdout.trim() : "";
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* The finds of a tree since `since` (an ISO moment; none or not one: the last FINDS_HOURS; at most FINDS_BACK_DAYS
|
|
237
|
+
* back), newest first, and the newest find before them; a fact said in `lang`, the page's (a language code; none or not
|
|
238
|
+
* one: the research language). Kept for the head, the window and the language: asked again with none of them changed,
|
|
239
|
+
* nothing is read. An archive: none.
|
|
240
|
+
*/
|
|
241
|
+
export function treeFinds(root, env, since, now = Date.now(), lang) {
|
|
242
|
+
const said = lang && /^[a-z]{2,3}$/u.test(lang) ? lang : undefined;
|
|
243
|
+
const floor = now - FINDS_BACK_DAYS * 86_400_000;
|
|
244
|
+
const asked = since ? Date.parse(since) : NaN;
|
|
245
|
+
const from = new Date(Number.isFinite(asked) ? Math.min(now, Math.max(floor, asked)) : now - FINDS_HOURS * 3_600_000).toISOString();
|
|
246
|
+
const h = head(root);
|
|
247
|
+
// the day moves the floor of what is looked for before the window
|
|
248
|
+
const key = `${root}|${h}|${from}|${new Date(floor).toISOString().slice(0, 10)}|${said ?? ""}`;
|
|
249
|
+
const was = kept.get(key);
|
|
250
|
+
if (was)
|
|
251
|
+
return was;
|
|
252
|
+
const tree = Tree.open(root, env);
|
|
253
|
+
let value = { since: from, groups: [] };
|
|
254
|
+
if (!actsAsArchive(tree)) {
|
|
255
|
+
const dataDir = path.join(root, "data");
|
|
256
|
+
const ops = logsFrom(dataDir, new Date(floor).toISOString().slice(0, 10)).flatMap(findsOfLog);
|
|
257
|
+
const inWindow = ops.filter((o) => o.at >= from);
|
|
258
|
+
const groups = findGroups(tree, inWindow, said ?? tree.lang);
|
|
259
|
+
// the newest find before the window that still says something (a record gone since says nothing)
|
|
260
|
+
let earlier;
|
|
261
|
+
const before = ops.filter((o) => o.at < from && o.at >= new Date(floor).toISOString()).sort((a, b) => b.at.localeCompare(a.at));
|
|
262
|
+
for (let i = 0; i < before.length && !earlier; i += 50)
|
|
263
|
+
earlier = findGroups(tree, before.slice(i, i + 50))[0]?.at;
|
|
264
|
+
value = { since: from, groups: groups.slice(0, FINDS_GROUPS), ...(groups.length > FINDS_GROUPS ? { cut: groups.length - FINDS_GROUPS } : {}), ...(earlier ? { earlier } : {}) };
|
|
265
|
+
}
|
|
266
|
+
kept.set(key, value);
|
|
267
|
+
for (const old of kept.keys()) {
|
|
268
|
+
if (kept.size <= KEPT)
|
|
269
|
+
break;
|
|
270
|
+
kept.delete(old);
|
|
271
|
+
}
|
|
272
|
+
return value;
|
|
273
|
+
}
|
package/dist/core/phone-send.js
CHANGED
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
// rest?}], more}), at most 20; rest: the lines of a change past the 3 shown
|
|
10
10
|
// GET /phone/waiting?tree=&from=&limit= more of what waits than the state gives ({n, items: [{what, on?}]}), of the
|
|
11
11
|
// same list, at most 200 at once
|
|
12
|
+
// GET /phone/finds?tree=&since= what the research found since then (phone-finds.ts: {since, groups: [{key, kind,
|
|
13
|
+
// title?, at, lines: [{kind, text, at}]}], cut?, earlier?}), at most 31 days back
|
|
12
14
|
// GET /phone/people?tree=&q= up to 8 people of the tree matching q (2 letters at least; case, accents and NFD
|
|
13
15
|
// aside): {id, name, birth, death, place}
|
|
14
16
|
// GET /phone/media/<sha256>?tree= is that content in the research already ({known: false} or {known, kind})
|
|
@@ -32,6 +34,7 @@ import { waysHere } from "./readable.js";
|
|
|
32
34
|
import { birthEvent, deathEvent, displayName, findPersons } from "./people.js";
|
|
33
35
|
import { yearLabel } from "./gdate.js";
|
|
34
36
|
import { foldText } from "./text.js";
|
|
37
|
+
import { treeFinds } from "./phone-finds.js";
|
|
35
38
|
import { LOG_PHONE, WAITING_ASKED, WAITING_SHOWN, treeLog, treeMarks, treeOverview, treeWaiting } from "./phone-watch.js";
|
|
36
39
|
/** How often the batches of a phone are looked at, and the parts of uploads cut short. */
|
|
37
40
|
const INCOMING_EVERY_MS = 60 * 60_000;
|
|
@@ -130,6 +133,17 @@ export function phoneSend(env, note) {
|
|
|
130
133
|
const limit = Math.min(LOG_PHONE, Math.max(1, Number.parseInt(url.searchParams.get("limit") ?? "", 10) || LOG_PHONE));
|
|
131
134
|
reply(200, treeLog(t.root, env, limit));
|
|
132
135
|
};
|
|
136
|
+
if (req.method === "GET" && p === "/phone/finds")
|
|
137
|
+
return () => {
|
|
138
|
+
if (foreign())
|
|
139
|
+
return refuse(403, refusal("only the phone's page asks", "phone.origin"));
|
|
140
|
+
req.resume();
|
|
141
|
+
const t = treeOf();
|
|
142
|
+
if (!t)
|
|
143
|
+
return gone();
|
|
144
|
+
// a fact said in the page's language (its name and date as the rest of the line)
|
|
145
|
+
reply(200, { ...treeFinds(t.root, env, url.searchParams.get("since") ?? undefined, Date.now(), url.searchParams.get("lang") ?? undefined) });
|
|
146
|
+
};
|
|
133
147
|
if (req.method === "GET" && p === "/phone/waiting")
|
|
134
148
|
return () => {
|
|
135
149
|
if (foreign())
|
package/dist/core/phone-watch.js
CHANGED
|
@@ -34,6 +34,10 @@ const ALONE = String.raw `(?<![\p{L}\p{M}\p{N}])`;
|
|
|
34
34
|
const ENDS = String.raw `(?![\p{L}\p{M}\p{N}])`;
|
|
35
35
|
const ONE_ID = String.raw `${ALONE}(?:D\d{14}-[0-9a-f]{4}|[${[...new Set(Object.values(ALL_PREFIXES))].join("")}]\d{4,})${ENDS}`;
|
|
36
36
|
const IDS = new RegExp(String.raw `${ONE_ID}(?:(?:\s*[,;/&+–—-]\s*|\s+(?:\p{L}\p{M}*){1,4}\s+)${ONE_ID})*(?:\s*,?\s*(?:…|\.{3}))?`, "gu");
|
|
37
|
+
// an ID written short in square brackets ("[I103]", "[P12, F3]"): a capital or two of any script and a number — never a
|
|
38
|
+
// year ("[1842]"), a word ("[sic]") or what has a space or a small letter in it ("[fol. 3]", "[č. 12]")
|
|
39
|
+
const SHORT_ID = String.raw `\p{Lu}\p{M}*(?:\p{Lu}\p{M}*)?\p{Nd}+`;
|
|
40
|
+
const BRACKET_IDS = new RegExp(String.raw `\[\s*${SHORT_ID}(?:\s*[,;/&+–—-]\s*${SHORT_ID})*\s*\]`, "gu");
|
|
37
41
|
// a link, with its scheme or www.: its site alone
|
|
38
42
|
const LINK = /(?<![\p{L}\p{M}\p{N}.@])(?:(?:https?|ftp):\/\/|www\.)[^\s<>"„“”«»]+/giu;
|
|
39
43
|
// a path: of Windows (C:\…, C:/…, \\server\…), the home's (~/…), absolute (/Users/…), in the research (notes/…, data/…),
|
|
@@ -86,6 +90,7 @@ export function forPhone(text) {
|
|
|
86
90
|
return `${siteOf(core)}${tail}`;
|
|
87
91
|
})
|
|
88
92
|
.replace(PATHS, (m) => peeled(m).tail)
|
|
93
|
+
.replace(BRACKET_IDS, "")
|
|
89
94
|
.replace(IDS, "")
|
|
90
95
|
.replace(/\s+/gu, " ");
|
|
91
96
|
for (let before = ""; before !== s;) {
|
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.
|
|
30
|
+
export const VERSION = "1.18.0-beta.8";
|
|
31
31
|
const GITIGNORE = `# Strom Research — local state and large files are not versioned
|
|
32
32
|
.strom/
|
|
33
33
|
/media/
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// The PATH of the person's terminal, for a strom started without it.
|
|
2
|
+
//
|
|
3
|
+
// A strom the system starts — a link from the Strom app through the applet (macOS: do shell script), the bridge and
|
|
4
|
+
// the phone's server it starts, a login item — gets the system's bare PATH (/usr/bin:/bin:/usr/sbin:/sbin), not the
|
|
5
|
+
// one the person's shell profile builds: the agent in ~/.local/bin, Node of an agent installed by npm, are not on it
|
|
6
|
+
// (found on Mac: "Read the document" from the app said no agent is set up). strom remembers the PATH of the last
|
|
7
|
+
// strom the person ran in a terminal, and every strom puts what of it, and of the folders the agents' installers use,
|
|
8
|
+
// is missing at the end of its own PATH — never before it: what the process was given comes first (the strom of a
|
|
9
|
+
// research's .strom/bin before the person's own). The person's shell is never run to ask (a profile may print,
|
|
10
|
+
// ask, take long — or be fish's).
|
|
11
|
+
import path from "node:path";
|
|
12
|
+
import { agentBinDirs } from "./install.js";
|
|
13
|
+
/** The PATH of this environment (Windows: Path in a copy of it). */
|
|
14
|
+
export function pathOf(env) {
|
|
15
|
+
return env.PATH ?? env.Path ?? "";
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* The PATH to remember from a strom the person runs in a terminal (not an agent's, not one strom started), or
|
|
19
|
+
* undefined: none to remember, or the one remembered already.
|
|
20
|
+
*/
|
|
21
|
+
export function pathToRemember(env, remembered) {
|
|
22
|
+
const now = pathOf(env);
|
|
23
|
+
if (!now.trim() || now === remembered)
|
|
24
|
+
return undefined;
|
|
25
|
+
return now;
|
|
26
|
+
}
|
|
27
|
+
/** The PATH with what is missing of the remembered one and of the agents' folders at its end; undefined: nothing missing. */
|
|
28
|
+
export function fullerPath(env, remembered, platform = process.platform) {
|
|
29
|
+
const now = pathOf(env);
|
|
30
|
+
const delimiter = platform === "win32" ? ";" : ":";
|
|
31
|
+
const key = (dir) => {
|
|
32
|
+
const d = dir.replace(/[\\/]+$/, "") || dir;
|
|
33
|
+
return platform === "win32" ? d.replace(/\//g, "\\").toLowerCase() : d;
|
|
34
|
+
};
|
|
35
|
+
const dirs = now.split(delimiter).filter(Boolean);
|
|
36
|
+
const have = new Set(dirs.map(key));
|
|
37
|
+
const more = [];
|
|
38
|
+
for (const dir of [...(remembered ?? "").split(delimiter), ...agentBinDirs(env, platform)]) {
|
|
39
|
+
if (!dir || !(platform === "win32" ? path.win32 : path.posix).isAbsolute(dir) || have.has(key(dir)))
|
|
40
|
+
continue;
|
|
41
|
+
have.add(key(dir));
|
|
42
|
+
more.push(dir);
|
|
43
|
+
}
|
|
44
|
+
return more.length ? [...dirs, ...more].join(delimiter) : undefined;
|
|
45
|
+
}
|
|
46
|
+
/** The environment given the fuller PATH (in place: what this strom starts gets it too). Whether it changed. */
|
|
47
|
+
export function completePath(env, remembered, platform = process.platform) {
|
|
48
|
+
const fuller = fullerPath(env, remembered, platform);
|
|
49
|
+
if (fuller === undefined)
|
|
50
|
+
return false;
|
|
51
|
+
// (a copy of Windows' environment keeps the name Path; the process's own takes either)
|
|
52
|
+
if (platform === "win32" && env.PATH === undefined && env.Path !== undefined)
|
|
53
|
+
env.Path = fuller;
|
|
54
|
+
else
|
|
55
|
+
env.PATH = fuller;
|
|
56
|
+
return true;
|
|
57
|
+
}
|