hmgr 0.0.7 → 0.0.14

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/dist/gui/main.js CHANGED
@@ -3,6 +3,9 @@
3
3
  // Fenster. Kein zweiter Prozess: der Kern läuft direkt im Main-Prozess, die GUI ist ein zweiter
4
4
  // Kompositions-Einstieg neben der CLI (ARCHITEKTUR.md „Eingang").
5
5
  import { app, BrowserWindow, dialog, ipcMain, Menu, shell } from 'electron';
6
+ // electron-updater ist CommonJS: in ESM kommt nur der Default-Export an, `autoUpdater` wird aus ihm
7
+ // herausgegriffen. Ein benannter Import (`import { autoUpdater } from …`) scheitert zur Laufzeit.
8
+ import electronUpdater from 'electron-updater';
6
9
  import path from 'node:path';
7
10
  import { randomUUID } from 'node:crypto';
8
11
  import { stat } from 'node:fs/promises';
@@ -17,6 +20,8 @@ import { entscheideOeffnen } from "./oeffnen.js";
17
20
  import { mcpUebernahmeSerialisieren, mcpUebersichtSerialisieren, mcpWirkungSerialisieren } from "./mcp-ansicht.js";
18
21
  import { beobachten } from "./beobachter.js";
19
22
  import { loginPathUebernehmen } from "./login-pfad.js";
23
+ import { hmgrCliVersion } from "./hmgr-cli.js";
24
+ import { cliUpdateHinweis } from "../adapter/npm-registry.js";
20
25
  const hierVerzeichnis = path.dirname(fileURLToPath(import.meta.url));
21
26
  const smoketest = process.env.HMGR_GUI_SMOKETEST === '1';
22
27
  /**
@@ -118,6 +123,18 @@ const { anwendungsfaelle, ports } = bauen(umgebung, {
118
123
  rueckfrage,
119
124
  ...(smoketest ? { claudeAusfuehrer: smoketestClaudeAusfuehrer } : {}),
120
125
  });
126
+ /**
127
+ * Wer den laufenden Aktualisieren-Lauf ausgelöst hat (E80): der Renderer reicht es bei jedem Aufruf
128
+ * mit, hier gemerkt für die beiden hmgr-Selbstprüfungen (`update-downloaded`, `cliUpdatePruefen`),
129
+ * die selbst keinen eigenen Auslöser kennen. Default 'start', bevor der erste Lauf gemeldet hat.
130
+ */
131
+ let letzterAusloeser = 'start';
132
+ // Merkt die GUI-Version beim Start im Verlauf (E80): ein Versionssprung gegen den letzten Start
133
+ // bekommt einen Eintrag „hmgr-GUI X installiert". Läuft auch im Smoke-Test — hmgrHome zeigt dort auf
134
+ // ein Wegwerf-Verzeichnis (fixtureAufbauen in scripts/gui-smoke-test.ts), nie auf das echte ~/.hmgr.
135
+ void anwendungsfaelle.versionsSprungVermerken('gui', paketVersion(), 'start').catch((fehler) => {
136
+ console.log(`[verlauf] ${fehler instanceof Error ? fehler.message : String(fehler)}`);
137
+ });
121
138
  async function auffangen(fn) {
122
139
  await pathBereit; // erst der Login-PATH (F47=O2), dann der Adapter — sonst findet er die CLI nicht
123
140
  try {
@@ -296,7 +313,25 @@ function registriereIpc() {
296
313
  lokal: args.ort.art === 'arbeitsverzeichnis',
297
314
  })));
298
315
  ipcMain.handle('hmgr:eintragLoeschen', async (_event, args) => auffangen(async () => anwendungsfaelle.eintragLoeschen({ ziel: zielAusIpc(args.harness, args.ort), name: args.name })));
299
- ipcMain.handle('hmgr:aktualisieren', async (_event, args) => auffangen(async () => anwendungsfaelle.aktualisieren(args)));
316
+ ipcMain.handle('hmgr:aktualisieren', async (_event, args) => auffangen(async () => {
317
+ if (args?.ausloeser !== undefined)
318
+ letzterAusloeser = args.ausloeser;
319
+ return anwendungsfaelle.aktualisieren(args);
320
+ }));
321
+ // Changelog der letzten Läufe (E80), neueste zuerst.
322
+ ipcMain.handle('hmgr:verlauf', async () => auffangen(async () => anwendungsfaelle.verlauf()));
323
+ // E77: der Prüfen-Knopf stößt zusätzlich zum Quellen-Lauf die Prüfung auf hmgr selbst an. Kein
324
+ // `handle`, sondern `send`/`on`: der Renderer wartet auf nichts, das Ergebnis kommt später von
325
+ // sich aus über 'hmgr:selbstUpdate'.
326
+ ipcMain.on('hmgr:selbstUpdatePruefen', () => selbstUpdatePruefen());
327
+ // Neustart-Knopf an der GUI-Update-Zeile (E80): installiert das geladene Paket sofort, statt auf
328
+ // das nächste Beenden zu warten. Im Smoke-Test ein No-op — dort läuft `updatePruefen` ohnehin nie,
329
+ // der Knopf bleibt verborgen, aber ein Absturz durch ein fehlendes Update darf trotzdem nie sein.
330
+ ipcMain.on('hmgr:neuStarten', () => {
331
+ if (smoketest)
332
+ return;
333
+ autoUpdater.quitAndInstall();
334
+ });
300
335
  ipcMain.handle('hmgr:arbeitsverzeichnisHinzufuegen', async (_event, pfad) => auffangen(async () => anwendungsfaelle.arbeitsverzeichnisHinzufuegen(pfad)));
301
336
  ipcMain.handle('hmgr:arbeitsverzeichnisVergessen', async (_event, pfad) => auffangen(async () => anwendungsfaelle.arbeitsverzeichnisVergessen(pfad)));
302
337
  // Nativer Verzeichnis-Dialog fürs Pfadfeld in „Verzeichnis hinzufügen" (A1). Im Smoke-Test darf
@@ -319,6 +354,14 @@ function registriereIpc() {
319
354
  ipcMain.handle('hmgr:harnessAktivieren', async (_event, name) => auffangen(async () => anwendungsfaelle.harnessAktivieren(name)));
320
355
  ipcMain.handle('hmgr:harnessDeaktivieren', async (_event, name) => auffangen(async () => anwendungsfaelle.harnessDeaktivieren(name)));
321
356
  ipcMain.handle('hmgr:saeubern', async (_event, name) => auffangen(async () => anwendungsfaelle.saeubern(name)));
357
+ // Homedirs je Harness (E78): zeigen, das primäre umstellen (F89), zusätzliche in einer
358
+ // WSL-Distro eintragen und vergessen (F87). Die Distro-Liste kommt aus `wsl -l -q`.
359
+ ipcMain.handle('hmgr:harnessHeime', async () => auffangen(async () => anwendungsfaelle.harnessHeime()));
360
+ ipcMain.handle('hmgr:wslDistros', async () => auffangen(async () => anwendungsfaelle.wslDistros()));
361
+ ipcMain.handle('hmgr:harnessHeimSetzen', async (_event, args) => auffangen(async () => anwendungsfaelle.harnessHeimSetzen(args.harness, args.pfad)));
362
+ ipcMain.handle('hmgr:harnessHeimZuruecksetzen', async (_event, harness) => auffangen(async () => anwendungsfaelle.harnessHeimZuruecksetzen(harness)));
363
+ ipcMain.handle('hmgr:harnessHeimHinzufuegen', async (_event, args) => auffangen(async () => anwendungsfaelle.harnessHeimHinzufuegen(args.harness, args.adresse)));
364
+ ipcMain.handle('hmgr:harnessHeimEntfernen', async (_event, args) => auffangen(async () => anwendungsfaelle.harnessHeimEntfernen(args.harness, args.adresse)));
322
365
  // Verwalteter Hermes-Skill-Ordner (E56): Zeigen kommt schon über hmgr:istZustand, hier nur Ändern.
323
366
  ipcMain.handle('hmgr:skillOrdnerFestlegen', async (_event, args) => auffangen(async () => anwendungsfaelle.skillOrdnerFestlegen(zielAusIpc(args.harness, args.ort), args.pfad)));
324
367
  ipcMain.handle('hmgr:skillOrdnerEntfernen', async (_event, args) => auffangen(async () => anwendungsfaelle.skillOrdnerEntfernen(zielAusIpc(args.harness, args.ort))));
@@ -474,6 +517,25 @@ function fensterOeffnen() {
474
517
  var harnessSchalter = Array.from(document.querySelectorAll('#harnessliste input[type=checkbox]'))
475
518
  .map((i) => i.dataset.harness + ':' + i.checked)
476
519
  .join(',');
520
+ // Homedir-Zeilen unter den Schaltern (E78): je Harness eine Zeile mit dem Pfad, dazu
521
+ // der Ändern-Knopf. Der Weg zu einem WSL-Homedir steht auf dem Mac bewusst nicht da.
522
+ var heimZeilen = Array.from(document.querySelectorAll('#harnessliste .heimzeile'))
523
+ .map((z) => z.textContent.replace(/\\s+/g, ' ').trim())
524
+ .join(' | ');
525
+ var heimWslKnopfVorhanden = Array.from(document.querySelectorAll('#harnessliste .heime > button'))
526
+ .some((b) => b.textContent.indexOf('WSL') !== -1);
527
+ // Der Pfad-Dialog lässt sich öffnen und per Abbrechen wieder schließen, ohne IPC-Lauf.
528
+ var heimPfadFehler = null;
529
+ var dHeimPfadOffenNachOeffnen = false;
530
+ var dHeimPfadOffenNachAbbrechen = true;
531
+ try {
532
+ document.querySelector('#harnessliste .heimknoepfe button')?.click();
533
+ dHeimPfadOffenNachOeffnen = document.getElementById('d-heim-pfad')?.open === true;
534
+ document.querySelector('#d-heim-pfad button[value="abbruch"]')?.click();
535
+ dHeimPfadOffenNachAbbrechen = document.getElementById('d-heim-pfad')?.open === true;
536
+ } catch (fehler) {
537
+ heimPfadFehler = String(fehler);
538
+ }
477
539
  // Quellen-Abschnitt der Einstellungen (E54): Knopf und Dialoge vorhanden, der
478
540
  // Hinzufügen-Dialog lässt sich öffnen und per Abbrechen ohne Fehler wieder schließen.
479
541
  // Kein echter Quellen-Lauf: der Abbrechen-Knopf löst keinen IPC-Aufruf aus.
@@ -534,6 +596,17 @@ function fensterOeffnen() {
534
596
  } catch (fehler) {
535
597
  quelleUmbenennenFehler = String(fehler);
536
598
  }
599
+ // Leertext der Quellenliste (E75): der Smoke-Lauf hat immer eine Quelle, den leeren
600
+ // Zustand erreicht er nie. Geprüft wird darum die Regel, an der er zerbrach — ein
601
+ // <li class="leerhinweis"> in genau dieser Liste ist vom Grid ausgenommen und nimmt
602
+ // die ganze Breite, statt in die 20px-Icon-Spalte zu fallen und Wort für Wort
603
+ // umzubrechen. Die Probe wird sofort wieder entfernt, die Liste bleibt unberührt.
604
+ var quellenLeerProbe = document.createElement('li');
605
+ quellenLeerProbe.className = 'leerhinweis';
606
+ quellenLeerProbe.textContent = 'Probe';
607
+ document.getElementById('quellenliste')?.appendChild(quellenLeerProbe);
608
+ var quellenLeerDisplay = getComputedStyle(quellenLeerProbe).display;
609
+ quellenLeerProbe.remove();
537
610
  document.getElementById('d-einstellungen')?.close();
538
611
  // Konfigzeile am Hermes-Ziel (E56): der verwaltete Pfad steht in der Karte, auch
539
612
  // ungeklappt (hidden ändert textContent nicht). Dazu der Festlegen-Weg am unmanaged
@@ -553,6 +626,52 @@ function fensterOeffnen() {
553
626
  } catch (fehler) {
554
627
  skillOrdnerFehler = String(fehler);
555
628
  }
629
+ // E77: der Ort für die Update-Hinweise steht in der Seitenleiste, direkt vor dem
630
+ // Einstellungen-Knopf, und ist im Ruhezustand leer und verborgen. Der Smoke-Lauf
631
+ // prüft nie nach — er stößt weder Updater noch CLI-Prüfung an, es gibt nichts zu
632
+ // melden. Geprüft wird darum nur, dass der Ort da ist und stumm bleibt.
633
+ var selbstupdateEl = document.getElementById('selbstupdate');
634
+ var selbstupdateVorhanden = selbstupdateEl !== null;
635
+ // Nicht nur das Attribut, sondern das tatsächliche Rendern: die Klasse setzt
636
+ // display:flex, und eine Autoren-Regel überstimmt das hidden-Attribut des Browsers —
637
+ // ohne die eigene hidden-Regel in styles.css stünde hier ein leerer Kasten samt
638
+ // seinem auto-Rand. Das Attribut allein würde den Fehler nicht zeigen.
639
+ var selbstupdateVerborgen =
640
+ selbstupdateEl?.hidden === true && getComputedStyle(selbstupdateEl).display === 'none';
641
+ // Seit E80 steckt im Kasten auch der Neustart-Knopf, sein Text bleibt aber im DOM
642
+ // stehen, auch verborgen (anders als bei display:none zählt hidden nicht für
643
+ // textContent) — „leer" meint deshalb: kein sichtbarer (nicht verborgener) Kindknoten
644
+ // trägt Text, nicht der rohe textContent des ganzen Kastens.
645
+ var selbstupdateLeer = Array.from(selbstupdateEl?.children ?? [])
646
+ .filter((kind) => !kind.hidden)
647
+ .every((kind) => (kind.textContent ?? '').trim() === '');
648
+ // Er sitzt zwischen Stand-Zeile und Einstellungen-Knopf: der Knopf ist sein nächster
649
+ // Geschwister-Knoten, sonst stünde der Hinweis irgendwo anders in der Leiste.
650
+ var selbstupdateVorEinstellungen = selbstupdateEl?.nextElementSibling?.id === 'einstellungen';
651
+ // E80: Neustart-Knopf an der GUI-Update-Zeile, vorhanden und im Ruhezustand verborgen
652
+ // (der Smoke-Lauf löst nie ein update-downloaded aus, das ihn einblenden würde).
653
+ var selbstupdateNeustartVorhanden = document.getElementById('selbstupdate-neustart') !== null;
654
+ var selbstupdateNeustartVerborgen = document.getElementById('selbstupdate-neustart')?.hidden === true;
655
+ // E80: die Seitenleiste zeigt nur noch die Stand-Zeile und ein „›" zum Öffnen der
656
+ // Ansicht, kein „Prüfen"-Wort mehr und kein eigener Lauf am Standknopf-Klick. In der
657
+ // Ansicht selbst ersetzt ein runder Kringel den früheren großen Knopf, und der alte
658
+ // Sitzungsbericht (#bericht) ist ganz weg, ersetzt durch das Changelog (#verlauf).
659
+ var standknopfText = document.getElementById('standknopf')?.textContent ?? null;
660
+ var updatestartKringel = document.getElementById('updatestart')?.classList.contains('kringel') === true;
661
+ var updatestartLaeuft = document.getElementById('updatestart')?.classList.contains('laeuft') === true;
662
+ var berichtVorhanden = document.getElementById('bericht') !== null;
663
+ var verlaufVorhanden = document.getElementById('verlauf') !== null;
664
+ // Der Standknopf öffnet nur noch die Ansicht (kein Lauf mehr an diesem Klick), die
665
+ // Ansicht lädt ihren Verlauf dabei aus der Datei — das ist ein IPC-Aufruf, also kurz
666
+ // nachfassen, bis das Ergebnis im DOM steht (Muster wie bei mcpWahlOeffnen oben).
667
+ document.getElementById('standknopf')?.click();
668
+ var verlaufBloecke = 0;
669
+ for (var wartezeit = 0; wartezeit < 2000; wartezeit += 100) {
670
+ await new Promise((r) => setTimeout(r, 100));
671
+ verlaufBloecke = document.querySelectorAll('#verlauf .block').length;
672
+ if (verlaufBloecke > 0) break;
673
+ }
674
+ var verlaufText = document.getElementById('verlauf')?.textContent?.replace(/\\s+/g, ' ').trim() ?? null;
556
675
  // E60: Beschreibung in der Ziel-Tabelle (nicht nur im Katalog), „manuell" entfällt,
557
676
  // Zahnrad statt Sonne am Einstellungen-Knopf.
558
677
  var zielBeschreibung = document.querySelector('#zielliste .linkliste .was')?.textContent ?? null;
@@ -567,6 +686,57 @@ function fensterOeffnen() {
567
686
  // E70/F70=O1: die Herkunftszeile trägt hinter dem Ref das Änderungsdatum des Skills.
568
687
  var katalogHerkunft = document.querySelector('#katalog .zeile .herkunft')?.textContent
569
688
  ?.replace(/\\s+/g, ' ').trim() ?? null;
689
+ // E79: Sortier-Gruppe der Skills-Rubrik. Erst „Zuletzt geändert" klicken und die
690
+ // sichtbaren Änderungsdaten der ersten fünf Zeilen samt Knopfstand festhalten, dann
691
+ // zurück auf „Name", damit die obigen Prüfungen (erste Zeile) unverändert gelten.
692
+ document.getElementById('sortnach-datum')?.click();
693
+ var katalogSortierungGeaendert = Array.from(document.querySelectorAll('#katalog .zeile:not([hidden])'))
694
+ .slice(0, 5)
695
+ .map((li) => li.dataset.geaendert ?? '')
696
+ .join('|');
697
+ var katalogSortierungDatumGedrueckt = document.getElementById('sortnach-datum')?.getAttribute('aria-pressed') ?? null;
698
+ var katalogSortierungNameGedrueckt = document.getElementById('sortnach-name')?.getAttribute('aria-pressed') ?? null;
699
+ // E81: Richtung und Pfeil nach dem ersten Klick. Standardrichtung von „datum" ist
700
+ // 'ab' (jüngste zuerst, wie E79), der Name-Chip ist dabei inaktiv und trägt keinen
701
+ // Pfeil.
702
+ var katalogSortierungDatumRichtung = document.getElementById('sortnach-datum')?.getAttribute('data-richtung') ?? null;
703
+ var katalogSortierungDatumPfeil = document.querySelector('#sortnach-datum .sortpfeil')?.textContent ?? null;
704
+ var katalogSortierungNamePfeil = document.querySelector('#sortnach-name .sortpfeil')?.textContent ?? null;
705
+ // E81: zweiter Klick auf denselben Chip kehrt nur die Richtung um (Schlüssel bleibt
706
+ // „datum", jetzt älteste zuerst).
707
+ document.getElementById('sortnach-datum')?.click();
708
+ var katalogSortierungGeaendertAuf = Array.from(document.querySelectorAll('#katalog .zeile:not([hidden])'))
709
+ .slice(0, 5)
710
+ .map((li) => li.dataset.geaendert ?? '')
711
+ .join('|');
712
+ var katalogSortierungDatumRichtungAuf = document.getElementById('sortnach-datum')?.getAttribute('data-richtung') ?? null;
713
+ var katalogSortierungDatumPfeilAuf = document.querySelector('#sortnach-datum .sortpfeil')?.textContent ?? null;
714
+ document.getElementById('sortnach-name')?.click();
715
+ // Reiner Textknoten des Namens (erstes Kind ist das Quellen-Icon, ein etwaiges
716
+ // Markenfeld käme als weiteres Element danach) statt h3.textContent, sonst würde
717
+ // eine Marke direkt an den Namen geklebt.
718
+ var ersterKatalogH3 = document.querySelector('#katalog .zeile:not([hidden]) h3');
719
+ var katalogErsterNachName = ersterKatalogH3
720
+ ? Array.from(ersterKatalogH3.childNodes).find((n) => n.nodeType === 3)?.textContent ?? null
721
+ : null;
722
+ // E81: Chipwechsel (anderer Schlüssel als zuvor „datum") setzt dessen Standardrichtung,
723
+ // hier 'auf' für Name — die bisherige, unveränderte Reihenfolge (A→Z).
724
+ var katalogNamenAuf = Array.from(document.querySelectorAll('#katalog .zeile:not([hidden]) h3'))
725
+ .map((h3) => Array.from(h3.childNodes).find((n) => n.nodeType === 3)?.textContent ?? '')
726
+ .join('|');
727
+ var katalogSortierungNameRichtungAuf = document.getElementById('sortnach-name')?.getAttribute('data-richtung') ?? null;
728
+ var katalogSortierungNamePfeilAuf = document.querySelector('#sortnach-name .sortpfeil')?.textContent ?? null;
729
+ // E81: zweiter Klick auf „Name" kehrt die Richtung um (Z→A).
730
+ document.getElementById('sortnach-name')?.click();
731
+ var katalogNamenAb = Array.from(document.querySelectorAll('#katalog .zeile:not([hidden]) h3'))
732
+ .map((h3) => Array.from(h3.childNodes).find((n) => n.nodeType === 3)?.textContent ?? '')
733
+ .join('|');
734
+ var katalogSortierungNameRichtungAb = document.getElementById('sortnach-name')?.getAttribute('data-richtung') ?? null;
735
+ var katalogSortierungNamePfeilAb = document.querySelector('#sortnach-name .sortpfeil')?.textContent ?? null;
736
+ // E81: dritter Klick stellt name/auf wieder her, damit die restliche Sonde (unten)
737
+ // wie gewohnt gegen die Standardsortierung prüft.
738
+ document.getElementById('sortnach-name')?.click();
739
+ var katalogSortierungNameRichtungNachDrittemKlick = document.getElementById('sortnach-name')?.getAttribute('data-richtung') ?? null;
570
740
  // Besonderheiten aus dem Frontmatter (E55): Marken an der Zeile, Details erst nach
571
741
  // einem Klick auf den Aufklapp-Knopf.
572
742
  var katalogMarken = document.querySelector('#katalog .zeile .marken')?.textContent
@@ -597,6 +767,17 @@ function fensterOeffnen() {
597
767
  ?.replace(/\\s+/g, ' ').trim() ?? null;
598
768
  document.getElementById('d-skills')?.close();
599
769
  }
770
+ // Verlinken-Dialog „Wohin?" (E76): er bietet nur Ziele aktiver Harnesses an. Der Text
771
+ // der Wahlliste sagt, welche das sind; ist gar keines aktiv, steht dort der Leerhinweis.
772
+ var dzZieleText = null;
773
+ var dzKnopf = Array.from(document.querySelectorAll('#katalog .zeile button'))
774
+ .find((b) => b.textContent === 'Verlinken');
775
+ if (dzKnopf) {
776
+ dzKnopf.click();
777
+ dzZieleText = document.getElementById('dz-ziele')?.textContent
778
+ ?.replace(/\\s+/g, ' ').trim() ?? null;
779
+ document.getElementById('d-ziel')?.close();
780
+ }
600
781
  // Bundle-Gruppe an der Ziel-Karte (E69): Kopf mit Name, Quelle und Entfernen, darunter
601
782
  // die Zeilen der Mitglieder — sie stehen in der Gruppe, nicht in der allgemeinen Liste.
602
783
  var bundleGruppeText = document.querySelector('#zielliste .bundlegruppe .bundlekopf')?.textContent
@@ -736,6 +917,19 @@ function fensterOeffnen() {
736
917
  var agentsKarteVorhanden = agentsKarte !== undefined;
737
918
  var agentsMcpAbschnittVorhanden = (agentsKarte?.querySelector('.mcpkarteabschnitt') ?? null) !== null;
738
919
  var agentsMcpKnopfVorhanden = mcpHinzuKnopfAn(agentsKarte) !== undefined;
920
+ // Warnzahl am Kartenkopf (E75): das Dreieck nennt die Zahl der Auslöser. Im Fixture
921
+ // liegt ein fremder Ordner im Hermes-Home-Ziel (1) und drei im Verzeichnis kunde-x,
922
+ // ungleich auf die zwei Harness-Teile verteilt — dort ist die Zahl die Summe. Die
923
+ // agents-Karte hat keinen Auslöser und darum gar kein Dreieck.
924
+ var warnungVon = function (karte) { return karte?.querySelector('.zielkopf .warnung') ?? null; };
925
+ var warnzahlVon = function (karte) {
926
+ return warnungVon(karte)?.querySelector('.warnzahl')?.textContent?.trim() ?? null;
927
+ };
928
+ var hermesWarnzahl = warnzahlVon(hermesKarte);
929
+ var hermesWarnlabel = warnungVon(hermesKarte)?.getAttribute('aria-label') ?? null;
930
+ var kundeXWarnzahl = warnzahlVon(kundeXKarte);
931
+ var kundeXWarnlabel = warnungVon(kundeXKarte)?.getAttribute('aria-label') ?? null;
932
+ var agentsWarnungVorhanden = warnungVon(agentsKarte) !== null;
739
933
  // Dialog „MCP hinzufügen" (E47-Muster): öffnen an der Hermes-Karte und über Abbrechen
740
934
  // wieder schließen, ohne IPC-Schreibaktion (mcpEinhaengen liefe erst nach „Einhängen").
741
935
  var dmhFehler = null;
@@ -763,12 +957,34 @@ function fensterOeffnen() {
763
957
  meldung: document.getElementById('meldungtext')?.textContent,
764
958
  einstellungenOffen,
765
959
  harnessSchalter,
960
+ heimZeilen,
961
+ heimWslKnopfVorhanden,
962
+ heimPfadFehler,
963
+ dHeimPfadOffenNachOeffnen,
964
+ dHeimPfadOffenNachAbbrechen,
766
965
  zielBeschreibung,
767
966
  zielMarken,
768
967
  zielManuellVorhanden,
769
968
  zahnradVorhanden,
770
969
  katalogBeschreibung,
771
970
  katalogHerkunft,
971
+ katalogSortierungGeaendert,
972
+ katalogSortierungDatumGedrueckt,
973
+ katalogSortierungNameGedrueckt,
974
+ katalogSortierungDatumRichtung,
975
+ katalogSortierungDatumPfeil,
976
+ katalogSortierungNamePfeil,
977
+ katalogSortierungGeaendertAuf,
978
+ katalogSortierungDatumRichtungAuf,
979
+ katalogSortierungDatumPfeilAuf,
980
+ katalogErsterNachName,
981
+ katalogNamenAuf,
982
+ katalogSortierungNameRichtungAuf,
983
+ katalogSortierungNamePfeilAuf,
984
+ katalogNamenAb,
985
+ katalogSortierungNameRichtungAb,
986
+ katalogSortierungNamePfeilAb,
987
+ katalogSortierungNameRichtungNachDrittemKlick,
772
988
  katalogMarken,
773
989
  katalogDetailsZu,
774
990
  katalogAufgeklappt,
@@ -777,6 +993,7 @@ function fensterOeffnen() {
777
993
  dskMarken,
778
994
  dskBundleBoxSichtbar,
779
995
  dskBundleText,
996
+ dzZieleText,
780
997
  bundleGruppeText,
781
998
  bundleGruppeZeilen,
782
999
  bundleZerbrichtText,
@@ -831,6 +1048,12 @@ function fensterOeffnen() {
831
1048
  kundeXMcpKnopfVorhanden,
832
1049
  kundeXKopfZahl,
833
1050
  hermesKopfZahl,
1051
+ hermesWarnzahl,
1052
+ hermesWarnlabel,
1053
+ kundeXWarnzahl,
1054
+ kundeXWarnlabel,
1055
+ agentsWarnungVorhanden,
1056
+ quellenLeerDisplay,
834
1057
  agentsKarteVorhanden,
835
1058
  agentsMcpAbschnittVorhanden,
836
1059
  agentsMcpKnopfVorhanden,
@@ -838,6 +1061,19 @@ function fensterOeffnen() {
838
1061
  dMcpHinzuOffenNachAbbrechen,
839
1062
  dmhListeText,
840
1063
  dmhFehler,
1064
+ selbstupdateVorhanden,
1065
+ selbstupdateVerborgen,
1066
+ selbstupdateLeer,
1067
+ selbstupdateVorEinstellungen,
1068
+ selbstupdateNeustartVorhanden,
1069
+ selbstupdateNeustartVerborgen,
1070
+ standknopfText,
1071
+ updatestartKringel,
1072
+ updatestartLaeuft,
1073
+ berichtVorhanden,
1074
+ verlaufVorhanden,
1075
+ verlaufText,
1076
+ verlaufBloecke,
841
1077
  });
842
1078
  })()`)
843
1079
  .then((auszug) => console.log(`[smoke] DOM-Auszug: ${auszug}`))
@@ -850,13 +1086,14 @@ function fensterOeffnen() {
850
1086
  }
851
1087
  // Selbsttätiges Aktualisieren (E44): einmal, sobald der Renderer bereit ist, danach alle sechs
852
1088
  // Stunden, solange das Fenster offen bleibt. Im Smoke-Test nie, sonst würde ein automatisierter
853
- // Lauf einen echten Git-Fetch anstoßen.
1089
+ // Lauf einen echten Git-Fetch anstoßen. Der Auslöser (E80) geht mit, damit der Verlauf zeigt, ob
1090
+ // ein Fund vom Start, vom Intervall oder einem Klick kam.
854
1091
  if (!smoketest) {
855
1092
  const fenster = hauptfenster;
856
1093
  fenster.webContents.once('did-finish-load', () => {
857
- fenster.webContents.send('hmgr:aktualisierenAnstossen');
1094
+ fenster.webContents.send('hmgr:aktualisierenAnstossen', 'start');
858
1095
  });
859
- const intervall = setInterval(() => fenster.webContents.send('hmgr:aktualisierenAnstossen'), SECHS_STUNDEN_MS);
1096
+ const intervall = setInterval(() => fenster.webContents.send('hmgr:aktualisierenAnstossen', 'intervall'), SECHS_STUNDEN_MS);
860
1097
  fenster.on('closed', () => clearInterval(intervall));
861
1098
  }
862
1099
  // Die GUI erfährt von Änderungen außerhalb ihrer selbst (E62): Fokus-Reload und Datei-Watcher.
@@ -878,6 +1115,123 @@ function fensterOeffnen() {
878
1115
  }
879
1116
  void hauptfenster.loadFile(path.join(hierVerzeichnis, 'renderer', 'index.html'));
880
1117
  }
1118
+ const { autoUpdater } = electronUpdater;
1119
+ /** Die Listener hängen am Modul, nicht am Aufruf: `updatePruefen` läuft beim Start und je Prüfen-Klick. */
1120
+ let updaterVerdrahtet = false;
1121
+ /**
1122
+ * Autoupdate der Windows-GUI (E73). Der Feed sind die Gitea-Releases des hmgr-Repos: die
1123
+ * `publish`-Angabe im build-Block der package.json erzeugt beim Bauen die `latest.yml` und legt
1124
+ * `app-update.yml` ins Paket, aus der electron-updater die Feed-URL liest.
1125
+ *
1126
+ * `checkForUpdatesAndNotify()` heißt: im Hintergrund prüfen, ein gefundenes Update herunterladen,
1127
+ * eine Windows-Benachrichtigung zeigen und beim Beenden der App installieren
1128
+ * (`autoInstallOnAppQuit` ist Vorgabe). Das Einspielen bleibt damit der App-Neustart; die GUI zeigt
1129
+ * seit E77 zusätzlich eine Zeile in der Sidebar, sobald das Paket geladen ist (`update-downloaded`).
1130
+ *
1131
+ * Drei Bedingungen, alle nötig:
1132
+ * — `app.isPackaged`: ungepackt gibt es keine `app-update.yml`, die Prüfung stiege ohnehin aus.
1133
+ * — `win32`: macOS lehnt electron-updater unsigniert ab, das wartet auf die Signierung (A4).
1134
+ * — kein Smoke-Test: dieselbe Zurückhaltung wie bei Watcher und Auto-Aktualisieren — ein
1135
+ * automatisierter Lauf greift nie ins Netz.
1136
+ *
1137
+ * Fehler werden nur vermerkt, nie weitergereicht: auf den Team-Rechnern kann das Gitea unerreichbar
1138
+ * sein (VPN, Abschaltung, DNS), und daran darf die App nicht scheitern. Der `error`-Listener ist
1139
+ * dabei nicht optional — electron-updater meldet Fehler als Event, und ein `error`-Event ohne
1140
+ * Zuhörer wirft in Node.
1141
+ */
1142
+ function updatePruefen() {
1143
+ if (smoketest || !app.isPackaged || process.platform !== 'win32')
1144
+ return;
1145
+ try {
1146
+ if (!updaterVerdrahtet) {
1147
+ updaterVerdrahtet = true;
1148
+ autoUpdater.on('error', (fehler) => {
1149
+ console.log(`[updater] ${fehler instanceof Error ? fehler.message : String(fehler)}`);
1150
+ });
1151
+ // E77: das geladene Paket wird sichtbar, seit E80 mit Neustart-Knopf. Zusätzlich ein
1152
+ // Verlaufseintrag „GUI-Update X geladen" (E80), einmal je neuer Version — vor der Meldung an
1153
+ // die Sidebar, damit die Ansicht ihn beim nächsten `ladeVerlauf()` schon findet.
1154
+ autoUpdater.on('update-downloaded', (info) => {
1155
+ const version = info?.version;
1156
+ const guiVersion = typeof version === 'string' ? version : paketVersion();
1157
+ void anwendungsfaelle
1158
+ .hmgrEreignisVermerken({ art: 'gui-update-geladen', version: guiVersion }, letzterAusloeser)
1159
+ .catch((fehler) => {
1160
+ console.log(`[verlauf] ${fehler instanceof Error ? fehler.message : String(fehler)}`);
1161
+ });
1162
+ selbstUpdateMelden({ art: 'gui', version: guiVersion });
1163
+ });
1164
+ }
1165
+ void autoUpdater.checkForUpdatesAndNotify().catch((fehler) => {
1166
+ console.log(`[updater] ${fehler instanceof Error ? fehler.message : String(fehler)}`);
1167
+ });
1168
+ }
1169
+ catch (fehler) {
1170
+ console.log(`[updater] ${fehler instanceof Error ? fehler.message : String(fehler)}`);
1171
+ }
1172
+ }
1173
+ /** Ein Hinweis für die Sidebar (E77). Ist das Fenster schon zu, verfällt er stillschweigend. */
1174
+ function selbstUpdateMelden(nachricht) {
1175
+ if (hauptfenster === null || hauptfenster.isDestroyed())
1176
+ return;
1177
+ hauptfenster.webContents.send('hmgr:selbstUpdate', nachricht);
1178
+ }
1179
+ /**
1180
+ * Die daneben installierte hmgr-CLI gegen npm (E77, F83=O1). Läuft auf allen Plattformen — anders
1181
+ * als die GUI-Prüfung hängt sie an keinem Paket-Format und an keiner Signierung. Gemeldet wird nur,
1182
+ * nie installiert: ein `npm install -g` aus der App scheitert leicht an nvm-Pfaden und Rechten
1183
+ * (F82).
1184
+ *
1185
+ * Still bei „fehlt": keine CLI im PATH, das ist der Normalfall bei einer reinen GUI-Installation.
1186
+ * Bei „version" wie bisher still bei Fehlercode, Registry unerreichbar, gleicher oder älterer
1187
+ * Version auf npm. Bei „ohne-version" (E82: CLI vor 0.0.7, kennt --version nicht) gibt es IMMER
1188
+ * eine Sidebar-Meldung — sie hängt nicht am Netz —, der Verlaufseintrag aber nur, wenn npm
1189
+ * erreichbar war und eine Version lieferte. Der Login-PATH (F47) muss vorher stehen, sonst findet
1190
+ * der Aufruf die per nvm installierte CLI nicht.
1191
+ */
1192
+ async function cliUpdatePruefen() {
1193
+ await pathBereit;
1194
+ const befund = await hmgrCliVersion();
1195
+ if (befund.art === 'fehlt')
1196
+ return;
1197
+ if (befund.art === 'version') {
1198
+ const hinweis = await cliUpdateHinweis(befund.version, { protokoll: (zeile) => console.log(zeile) });
1199
+ if (hinweis === null)
1200
+ return;
1201
+ // Verlaufseintrag „CLI X verfügbar" (E80), einmal je neuer Version, vor der Sidebar-Meldung.
1202
+ await anwendungsfaelle
1203
+ .hmgrEreignisVermerken({ art: 'cli-verfuegbar', version: hinweis.neu }, letzterAusloeser)
1204
+ .catch((fehler) => {
1205
+ console.log(`[verlauf] ${fehler instanceof Error ? fehler.message : String(fehler)}`);
1206
+ });
1207
+ selbstUpdateMelden({ art: 'cli', ...hinweis });
1208
+ return;
1209
+ }
1210
+ // befund.art === 'ohne-version'
1211
+ const hinweis = await cliUpdateHinweis(null, { protokoll: (zeile) => console.log(zeile) });
1212
+ if (hinweis.neu !== null) {
1213
+ await anwendungsfaelle
1214
+ .hmgrEreignisVermerken({ art: 'cli-verfuegbar', version: hinweis.neu }, letzterAusloeser)
1215
+ .catch((fehler) => {
1216
+ console.log(`[verlauf] ${fehler instanceof Error ? fehler.message : String(fehler)}`);
1217
+ });
1218
+ }
1219
+ selbstUpdateMelden({ art: 'cli', ...hinweis });
1220
+ }
1221
+ /**
1222
+ * Jeder Aktualisieren-Lauf prüft auch hmgr selbst (E80: Start, Intervall, Klick auf den Kringel) —
1223
+ * der Renderer ruft das aus `aktualisierenLauf`, unabhängig vom Auslöser. Beide Wege sind
1224
+ * unabhängig: die GUI-Prüfung gilt nur unter den E73-Bedingungen, die CLI-Prüfung überall. Im
1225
+ * Smoke-Test unterbleiben beide — kein Spawn, kein Netz.
1226
+ */
1227
+ function selbstUpdatePruefen() {
1228
+ if (smoketest)
1229
+ return;
1230
+ updatePruefen();
1231
+ void cliUpdatePruefen().catch((fehler) => {
1232
+ console.log(`[cli-update] ${fehler instanceof Error ? fehler.message : String(fehler)}`);
1233
+ });
1234
+ }
881
1235
  /**
882
1236
  * Holt das vorhandene Fenster nach vorn — die Antwort der laufenden Instanz auf einen zweiten
883
1237
  * Start (E67). Minimiert kommt es erst wieder hoch, dann bekommt es den Fokus.
@@ -912,6 +1266,8 @@ if (einzelinstanz) {
912
1266
  Menu.setApplicationMenu(null);
913
1267
  }
914
1268
  fensterOeffnen();
1269
+ // Seit E80 prüft schon der erste Aktualisieren-Lauf (Renderer, ausgelöst bei `did-finish-load`)
1270
+ // auch hmgr selbst über `selbstUpdatePruefen` — ein eigener Start-Aufruf hier wäre der zweite.
915
1271
  app.on('activate', () => {
916
1272
  if (BrowserWindow.getAllWindows().length === 0)
917
1273
  fensterOeffnen();
@@ -33,6 +33,12 @@ contextBridge.exposeInMainWorld('hmgr', {
33
33
  eintragLoeschen: (args) => ipcRenderer.invoke('hmgr:eintragLoeschen', args),
34
34
  uebernehmen: (args) => ipcRenderer.invoke('hmgr:uebernehmen', args),
35
35
  aktualisieren: (args) => ipcRenderer.invoke('hmgr:aktualisieren', args),
36
+ /** Changelog der letzten Läufe (E80), neueste zuerst. */
37
+ verlauf: () => ipcRenderer.invoke('hmgr:verlauf'),
38
+ /** Installiert das geladene GUI-Update und startet neu (E80), Gegenstück zum Neustart-Knopf. */
39
+ neuStarten: () => {
40
+ ipcRenderer.send('hmgr:neuStarten');
41
+ },
36
42
  arbeitsverzeichnisHinzufuegen: (pfad) => ipcRenderer.invoke('hmgr:arbeitsverzeichnisHinzufuegen', pfad),
37
43
  arbeitsverzeichnisVergessen: (pfad) => ipcRenderer.invoke('hmgr:arbeitsverzeichnisVergessen', pfad),
38
44
  /** Nativer Verzeichnis-Dialog fürs Pfadfeld in „Verzeichnis hinzufügen" (A1). */
@@ -43,6 +49,13 @@ contextBridge.exposeInMainWorld('hmgr', {
43
49
  harnessAktivieren: (name) => ipcRenderer.invoke('hmgr:harnessAktivieren', name),
44
50
  harnessDeaktivieren: (name) => ipcRenderer.invoke('hmgr:harnessDeaktivieren', name),
45
51
  saeubern: (name) => ipcRenderer.invoke('hmgr:saeubern', name),
52
+ /** Homedirs je Harness (E78): zeigen, umstellen (F89), zusätzliche eintragen und vergessen (F87). */
53
+ harnessHeime: () => ipcRenderer.invoke('hmgr:harnessHeime'),
54
+ wslDistros: () => ipcRenderer.invoke('hmgr:wslDistros'),
55
+ harnessHeimSetzen: (args) => ipcRenderer.invoke('hmgr:harnessHeimSetzen', args),
56
+ harnessHeimZuruecksetzen: (harness) => ipcRenderer.invoke('hmgr:harnessHeimZuruecksetzen', harness),
57
+ harnessHeimHinzufuegen: (args) => ipcRenderer.invoke('hmgr:harnessHeimHinzufuegen', args),
58
+ harnessHeimEntfernen: (args) => ipcRenderer.invoke('hmgr:harnessHeimEntfernen', args),
46
59
  /** Verwalteter Hermes-Skill-Ordner (E56): zeigen kommt über hmgr:istZustand, hier nur Ändern. */
47
60
  skillOrdnerFestlegen: (args) => ipcRenderer.invoke('hmgr:skillOrdnerFestlegen', args),
48
61
  skillOrdnerEntfernen: (args) => ipcRenderer.invoke('hmgr:skillOrdnerEntfernen', args),
@@ -54,15 +67,26 @@ contextBridge.exposeInMainWorld('hmgr', {
54
67
  antwortenRueckfrage: (id, antwort) => {
55
68
  ipcRenderer.send('hmgr:rueckfrage:antwort', { id, antwort });
56
69
  },
57
- /** Anstoß zum selbsttätigen Aktualisieren (E44): Main ruft nach dem Start und alle sechs Stunden. */
70
+ /** Anstoß zum selbsttätigen Aktualisieren (E44): Main ruft nach dem Start und alle sechs Stunden,
71
+ mit dem Auslöser für den Verlauf (E80). */
58
72
  onAktualisierenAnstossen: (hoerer) => {
59
- ipcRenderer.on('hmgr:aktualisierenAnstossen', () => hoerer());
73
+ ipcRenderer.on('hmgr:aktualisierenAnstossen', (_event, ausloeser) => hoerer(ausloeser));
60
74
  },
61
75
  /** Anstoß zum stillen Neuladen der Anzeige (E62): Main ruft bei Fokus-Wechsel und beobachteten
62
76
  Dateiänderungen, nie mit Netzzugriff (im Unterschied zu onAktualisierenAnstossen). */
63
77
  onNeuLadenAnstossen: (hoerer) => {
64
78
  ipcRenderer.on('hmgr:neuLadenAnstossen', () => hoerer());
65
79
  },
80
+ /** Prüfung auf neue hmgr-Stände (E77), angestoßen bei jedem Aktualisieren-Lauf (E80: Start,
81
+ Intervall, Klick). Antwortet nicht, meldet sich später über onSelbstUpdate — Netz und Spawn
82
+ dauern länger als ein Knopfdruck. */
83
+ selbstUpdatePruefen: () => {
84
+ ipcRenderer.send('hmgr:selbstUpdatePruefen');
85
+ },
86
+ /** Ein Fund der Selbst-Prüfung (E77): geladenes GUI-Update oder neuere CLI auf npm. */
87
+ onSelbstUpdate: (hoerer) => {
88
+ ipcRenderer.on('hmgr:selbstUpdate', (_event, nachricht) => hoerer(nachricht));
89
+ },
66
90
  /** MCP-Ansicht (E61-Folgerunde): Katalog gelesen, nie geschrieben. */
67
91
  mcpUebersicht: () => ipcRenderer.invoke('hmgr:mcpUebersicht'),
68
92
  /** Plätze für den Einhängen-Dialog (F42): kommt aus dem Kern, hängt an der Fähigkeit (E16). */