hmgr 0.0.5 → 0.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -0
- package/dist/adapter/bundle-yaml.js +87 -0
- package/dist/adapter/checkout-pfad.js +10 -2
- package/dist/adapter/claude-code.js +231 -7
- package/dist/adapter/composer.js +4 -3
- package/dist/adapter/datei-text.js +18 -0
- package/dist/adapter/dateisystem.js +42 -4
- package/dist/adapter/git.js +182 -8
- package/dist/adapter/hermes.js +73 -1
- package/dist/adapter/marketplace-json.js +2 -1
- package/dist/adapter/mcp-katalog.js +124 -0
- package/dist/adapter/paket-version.js +25 -0
- package/dist/adapter/quelle-text.js +10 -1
- package/dist/adapter/skill-md.js +2 -1
- package/dist/adapter/umgebung.js +43 -0
- package/dist/adapter/vergleich.js +20 -0
- package/dist/adapter/zustand-json.js +11 -3
- package/dist/cli/hilfe.js +197 -0
- package/dist/cli/main.js +255 -7
- package/dist/gui/beobachter.js +108 -0
- package/dist/gui/login-pfad.js +120 -0
- package/dist/gui/main.js +462 -19
- package/dist/gui/mcp-ansicht.js +57 -0
- package/dist/gui/oeffnen.js +1 -1
- package/dist/gui/preload.cjs +20 -1
- package/dist/gui/renderer/index.html +108 -7
- package/dist/gui/renderer/renderer.js +1066 -142
- package/dist/gui/renderer/styles.css +85 -2
- package/dist/kern/anwendungsfaelle.js +624 -12
- package/dist/kern/modell.js +221 -3
- package/dist/kern/quelle-ort.js +14 -1
- package/dist/ports/harness.js +3 -0
- package/dist/ports/mcp-katalog.js +1 -0
- package/dist/ports/umgebung.js +1 -0
- package/dist/ports/zustand.js +15 -1
- package/package.json +59 -2
package/dist/gui/main.js
CHANGED
|
@@ -2,19 +2,42 @@
|
|
|
2
2
|
// registriert IPC-Handler für den Renderer und implementiert den Rückfrage-Port als Dialog im
|
|
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
|
-
import { app, BrowserWindow, dialog, ipcMain, shell } from 'electron';
|
|
5
|
+
import { app, BrowserWindow, dialog, ipcMain, Menu, shell } from 'electron';
|
|
6
6
|
import path from 'node:path';
|
|
7
|
-
import os from 'node:os';
|
|
8
7
|
import { randomUUID } from 'node:crypto';
|
|
9
8
|
import { stat } from 'node:fs/promises';
|
|
10
9
|
import { fileURLToPath } from 'node:url';
|
|
11
10
|
import { bauen, umgebungLesen } from "../cli/main.js";
|
|
12
11
|
import { quelleAlsText, quelleAusText } from "../adapter/quelle-text.js";
|
|
12
|
+
import { paketVersion } from "../adapter/paket-version.js";
|
|
13
13
|
import { quelleName, quelleSchluessel } from "../kern/modell.js";
|
|
14
|
+
import { kannProfile } from "../ports/harness.js";
|
|
14
15
|
import { iconFuerQuelle } from "./icons.js";
|
|
15
16
|
import { entscheideOeffnen } from "./oeffnen.js";
|
|
17
|
+
import { mcpUebernahmeSerialisieren, mcpUebersichtSerialisieren, mcpWirkungSerialisieren } from "./mcp-ansicht.js";
|
|
18
|
+
import { beobachten } from "./beobachter.js";
|
|
19
|
+
import { loginPathUebernehmen } from "./login-pfad.js";
|
|
16
20
|
const hierVerzeichnis = path.dirname(fileURLToPath(import.meta.url));
|
|
17
21
|
const smoketest = process.env.HMGR_GUI_SMOKETEST === '1';
|
|
22
|
+
/**
|
|
23
|
+
* Nur eine hmgr-Instanz je Nutzer (E67). Grund: zwei laufende Fenster schrieben nacheinander auf
|
|
24
|
+
* dieselbe `state.json` und überschrieben sich gegenseitig — der Zustand ist einer, nicht einer je
|
|
25
|
+
* Fenster. Die zweite Instanz bekommt den Lock nicht und beendet sich; die erste holt in
|
|
26
|
+
* `second-instance` ihr Fenster nach vorn, damit der zweite Start nicht wirkungslos aussieht.
|
|
27
|
+
* Gilt auf allen Plattformen: unter macOS ist der Doppelstart selten (das Dock startet nicht neu),
|
|
28
|
+
* unter Windows ist ein zweiter Klick im Startmenü der Normalfall.
|
|
29
|
+
*
|
|
30
|
+
* Der Lock wird ganz früh geholt, noch vor `bauen()` und der PATH-Ermittlung: eine Instanz, die
|
|
31
|
+
* ohnehin gleich wieder geht, soll nichts mehr anfassen und keine Shell mehr starten.
|
|
32
|
+
*
|
|
33
|
+
* Im Smoke-Test ist er aus. Er hängt am Electron-userData-Verzeichnis, und das leitet macOS nicht
|
|
34
|
+
* über das gefälschte HOME um (gemessen, siehe E67) — eine offene hmgr-GUI auf dem Rechner würde
|
|
35
|
+
* den Smoke-Lauf sonst stumm abwürgen. Dieselbe Zurückhaltung wie bei Watcher und
|
|
36
|
+
* Auto-Aktualisieren: was einen automatisierten Lauf von außen stören kann, bleibt im Smoke aus.
|
|
37
|
+
*/
|
|
38
|
+
const einzelinstanz = smoketest || app.requestSingleInstanceLock();
|
|
39
|
+
if (!einzelinstanz)
|
|
40
|
+
app.quit();
|
|
18
41
|
const umgebung = umgebungLesen();
|
|
19
42
|
/** Cache der Quellen-Icons (E60), unterhalb von HMGR_HOME wie der Zustand: Tests/Smoke fassen so
|
|
20
43
|
nie ein echtes Verzeichnis an. */
|
|
@@ -23,10 +46,16 @@ const iconCacheVerzeichnis = path.join(umgebung.hmgrHome, 'icon-cache');
|
|
|
23
46
|
const SECHS_STUNDEN_MS = 6 * 60 * 60 * 1000;
|
|
24
47
|
/**
|
|
25
48
|
* Fake der claude-CLI für den Smoke-Test (E40 R4), nach dem Muster von `fakeClaude` in
|
|
26
|
-
* tests/helfer.ts
|
|
27
|
-
*
|
|
49
|
+
* tests/helfer.ts. Kein Kindprozess, keine echte CLI: ein automatisierter Lauf fasst die echte
|
|
50
|
+
* claude-CLI formal nie an, auch wenn sie auf dem PATH liegt.
|
|
51
|
+
*
|
|
52
|
+
* `--version` läuft durch, damit Claude Code im Smoke-Test seine Fähigkeiten bekommt (E61: die
|
|
53
|
+
* MCP-Fähigkeit hängt an der vorhandenen CLI). Sonst blieben die Claude-Code-Zeilen der MCP-Ansicht
|
|
54
|
+
* ungetestet. Jeder andere Aufruf scheitert mit Code 1: die lesenden Pfade tragen das (Plugin-Liste
|
|
55
|
+
* leer, `eingehaengte` liest ohnehin nur ~/.claude.json), und die schreibenden bleiben so
|
|
56
|
+
* verschlossen — der Smoke-Test hängt nichts ein und aus, er öffnet die Dialoge nur.
|
|
28
57
|
*/
|
|
29
|
-
const smoketestClaudeAusfuehrer = async () =>
|
|
58
|
+
const smoketestClaudeAusfuehrer = async (args) => args[0] === '--version' ? { stdout: 'claude 0.0.0-smoke\n', code: 0 } : { stdout: '', code: 1 };
|
|
30
59
|
let hauptfenster = null;
|
|
31
60
|
/**
|
|
32
61
|
* Rückfrage-Port als Dialog im Renderer (E28, E34): der Main-Prozess schickt die Frage per
|
|
@@ -63,12 +92,34 @@ class GuiRueckfrage {
|
|
|
63
92
|
return typeof antwort === 'number' ? antwort : undefined;
|
|
64
93
|
}
|
|
65
94
|
}
|
|
95
|
+
/**
|
|
96
|
+
* PATH aus der Login-Shell (F47=O2, E63): aus dem Finder gestartet bekommt die App nur den
|
|
97
|
+
* macOS-Minimal-PATH, ohne z. B. ~/.local/bin mit der claude-CLI. Die Ermittlung läuft ab hier
|
|
98
|
+
* parallel zum Electron-Start; gewartet wird zentral in `auffangen`, und damit vor jedem
|
|
99
|
+
* Adapter-Aufruf, denn jeder IPC-Handler geht durch `auffangen` und Spawns gibt es nur dort
|
|
100
|
+
* (claude-code.ts, git.ts — Watcher, Fokus-Reload und Auto-Aktualisieren stoßen den Renderer an,
|
|
101
|
+
* der wieder über IPC hereinkommt).
|
|
102
|
+
*
|
|
103
|
+
* Bewusst kein Top-Level-`await`: im ESM-Main-Entry läuft vor `ready` nur, was die Imports als
|
|
104
|
+
* Nebeneffekt tun; ein Await dort verschiebt die Registrierung aller App-Handler und verzögert
|
|
105
|
+
* das Fenster um die Laufzeit der Shell (~2 s). So erscheint das Fenster sofort, und höchstens
|
|
106
|
+
* der erste Datenabruf wartet. Im Smoke-Test unterbleibt die Ermittlung ganz (nie eine echte
|
|
107
|
+
* Shell, Muster: Watcher/Auto-Aktualisieren sind im Smoke auch aus). Der `catch` ist die letzte
|
|
108
|
+
* Leine: dieser Aufruf darf die App unter keinen Umständen stören.
|
|
109
|
+
*
|
|
110
|
+
* Unter Windows steigt `loginPathUebernehmen` selbst sofort aus (E67): dort gibt es keine
|
|
111
|
+
* Login-Shell im POSIX-Sinn und keine PATH-Lücke zu füllen.
|
|
112
|
+
*/
|
|
113
|
+
const pathBereit = smoketest || !einzelinstanz
|
|
114
|
+
? Promise.resolve()
|
|
115
|
+
: loginPathUebernehmen().catch(() => undefined);
|
|
66
116
|
const rueckfrage = new GuiRueckfrage();
|
|
67
|
-
const { anwendungsfaelle } = bauen(umgebung, {
|
|
117
|
+
const { anwendungsfaelle, ports } = bauen(umgebung, {
|
|
68
118
|
rueckfrage,
|
|
69
119
|
...(smoketest ? { claudeAusfuehrer: smoketestClaudeAusfuehrer } : {}),
|
|
70
120
|
});
|
|
71
121
|
async function auffangen(fn) {
|
|
122
|
+
await pathBereit; // erst der Login-PATH (F47=O2), dann der Adapter — sonst findet er die CLI nicht
|
|
72
123
|
try {
|
|
73
124
|
return { ok: true, daten: await fn() };
|
|
74
125
|
}
|
|
@@ -86,6 +137,14 @@ function zielZustandSerialisieren(z) {
|
|
|
86
137
|
gruende: Object.fromEntries(z.gruende),
|
|
87
138
|
ignoriert: z.ignoriert,
|
|
88
139
|
uebernahme: Object.fromEntries(z.uebernahme),
|
|
140
|
+
// Bundles des Ziels (E69): Adresse und Anzeigename der Quelle kommen mit, damit der Renderer
|
|
141
|
+
// die Gruppe beschriften kann, ohne die Quelle selbst nachzuschlagen.
|
|
142
|
+
bundles: z.bundles.map((b) => ({
|
|
143
|
+
name: b.name,
|
|
144
|
+
skills: b.skills,
|
|
145
|
+
quelleText: quelleAlsText(b.quelle),
|
|
146
|
+
quelleName: quelleName(b.quelle),
|
|
147
|
+
})),
|
|
89
148
|
istRegistriert: z.istRegistriert,
|
|
90
149
|
kannSkillOrdner: z.kannSkillOrdner,
|
|
91
150
|
...(z.plugins === undefined ? {} : { plugins: z.plugins }),
|
|
@@ -97,8 +156,13 @@ function zielAusIpc(harness, ort) {
|
|
|
97
156
|
}
|
|
98
157
|
function registriereIpc() {
|
|
99
158
|
ipcMain.handle('hmgr:version', async () => auffangen(async () => app.getVersion()));
|
|
100
|
-
// Für pfadKurz im Renderer (Anzeige-Kürzung auf ~): nur lesend, nie in einen anderen Aufruf
|
|
101
|
-
|
|
159
|
+
// Für pfadKurz im Renderer (Anzeige-Kürzung auf ~): nur lesend, nie in einen anderen Aufruf
|
|
160
|
+
// zurückgespeist. Dasselbe Home wie der Kern (`umgebungLesen`: `HOME` vor `os.homedir()`, E68) —
|
|
161
|
+
// sonst kürzt die Anzeige dort nicht, wo HOME abweichend gesetzt ist (MSYS/Git-Bash).
|
|
162
|
+
ipcMain.handle('hmgr:home', async () => auffangen(async () => umgebung.home));
|
|
163
|
+
// Ebenfalls nur für die Anzeige (E68): der Renderer leitet daraus den Pfadtrenner ab, mit dem er
|
|
164
|
+
// eigene Anzeigepfade zusammensetzt („~/.claude.json" bzw. „~\.claude.json").
|
|
165
|
+
ipcMain.handle('hmgr:plattform', async () => auffangen(async () => process.platform));
|
|
102
166
|
ipcMain.handle('hmgr:istZustand', async () => auffangen(async () => (await anwendungsfaelle.istZustand()).map(zielZustandSerialisieren)));
|
|
103
167
|
ipcMain.handle('hmgr:marketplaces', async () => auffangen(async () => (await anwendungsfaelle.marketplaces()).map((m) => ({
|
|
104
168
|
quelleText: quelleAlsText(m.quelle),
|
|
@@ -126,6 +190,7 @@ function registriereIpc() {
|
|
|
126
190
|
...(skill.modell === undefined ? {} : { modell: skill.modell }),
|
|
127
191
|
...(skill.eigenerKontext === undefined ? {} : { eigenerKontext: skill.eigenerKontext }),
|
|
128
192
|
...(skill.argumentHinweis === undefined ? {} : { argumentHinweis: skill.argumentHinweis }),
|
|
193
|
+
...(skill.geaendert === undefined ? {} : { geaendert: skill.geaendert }),
|
|
129
194
|
quelleText: quelleAlsText(eintrag.quelle),
|
|
130
195
|
quelleName: quelleName(eintrag.quelle),
|
|
131
196
|
quelleSchluessel: quelleSchluessel(eintrag.quelle),
|
|
@@ -134,6 +199,45 @@ function registriereIpc() {
|
|
|
134
199
|
}
|
|
135
200
|
return ergebnis;
|
|
136
201
|
}));
|
|
202
|
+
// Bundles aller bekannten Quellen (E69): flach wie der Skill-Katalog, je Bundle seine Quelle und
|
|
203
|
+
// die Zahl seiner Mitglieder. Kaputte Definitionen kommen als eigene Liste mit, damit die GUI sie
|
|
204
|
+
// nennen kann, statt sie zu verschlucken.
|
|
205
|
+
ipcMain.handle('hmgr:bundles', async () => auffangen(async () => {
|
|
206
|
+
const bundles = [];
|
|
207
|
+
const kaputte = [];
|
|
208
|
+
for (const eintrag of await anwendungsfaelle.bundleKatalog()) {
|
|
209
|
+
for (const bundle of eintrag.bundles)
|
|
210
|
+
bundles.push({
|
|
211
|
+
name: bundle.name,
|
|
212
|
+
beschreibung: bundle.beschreibung,
|
|
213
|
+
mitglieder: bundle.mitglieder.length,
|
|
214
|
+
// Anzeige im Dialog: aus welchen Quellen das Bundle Skills holt, ohne Dubletten.
|
|
215
|
+
quellen: [...new Set(bundle.mitglieder.map((m) => quelleAlsText(m.quelle)))],
|
|
216
|
+
quelleText: quelleAlsText(bundle.quelle),
|
|
217
|
+
quelleName: quelleName(bundle.quelle),
|
|
218
|
+
});
|
|
219
|
+
for (const kaputt of eintrag.kaputte)
|
|
220
|
+
kaputte.push({ quelleName: quelleName(eintrag.quelle), datei: kaputt.datei, fehler: kaputt.fehler });
|
|
221
|
+
}
|
|
222
|
+
return { bundles, kaputte };
|
|
223
|
+
}));
|
|
224
|
+
// Installieren und Entfernen wirken auf das Bundle als Ganzes (E69). Der Zeiger ist nicht im
|
|
225
|
+
// Spiel, die Zugehörigkeit steht im Zustand — anders als bei linkLegen braucht es kein `lokal`.
|
|
226
|
+
ipcMain.handle('hmgr:bundleInstallieren', async (_event, args) => auffangen(async () => {
|
|
227
|
+
const ergebnis = await anwendungsfaelle.bundleInstallieren({
|
|
228
|
+
bundle: args.bundle,
|
|
229
|
+
ziel: zielAusIpc(args.harness, args.ort),
|
|
230
|
+
...(args.quelle === undefined ? {} : { quelle: args.quelle }),
|
|
231
|
+
});
|
|
232
|
+
return {
|
|
233
|
+
name: ergebnis.bundle.name,
|
|
234
|
+
gelegt: ergebnis.gelegt,
|
|
235
|
+
uebernommen: ergebnis.uebernommen,
|
|
236
|
+
neueQuellen: ergebnis.neueQuellen.map((q) => quelleName(q)),
|
|
237
|
+
...(ergebnis.hinweis === undefined ? {} : { hinweis: ergebnis.hinweis }),
|
|
238
|
+
};
|
|
239
|
+
}));
|
|
240
|
+
ipcMain.handle('hmgr:bundleEntfernen', async (_event, args) => auffangen(async () => anwendungsfaelle.bundleEntfernen({ bundle: args.bundle, ziel: zielAusIpc(args.harness, args.ort) })));
|
|
137
241
|
// Nur die Herkunft, ohne die Quellen zu lesen: der Katalog daneben tut das schon.
|
|
138
242
|
ipcMain.handle('hmgr:quellen', async () => auffangen(async () => (await anwendungsfaelle.quellen()).map((quelle) => ({
|
|
139
243
|
art: quelle.art,
|
|
@@ -230,7 +334,7 @@ function registriereIpc() {
|
|
|
230
334
|
// Quelle öffnen (E60): die Protokoll-Weiche steht in oeffnen.ts (rein, testbar), hier nur die
|
|
231
335
|
// Wirkung. `shell.openExternal` bekommt ausschließlich http(s)-URLs, `shell.openPath` nur einen
|
|
232
336
|
// Ordner, den es wirklich gibt — nichts anderes wird zugelassen. Alles, was es gibt, aber kein
|
|
233
|
-
// Ordner ist, wird im
|
|
337
|
+
// Ordner ist, wird im Dateimanager nur gezeigt, nicht geöffnet: `openPath` startet sonst eine Datei
|
|
234
338
|
// mit ihrem Standardprogramm, und ein Pfad aus dem Renderer soll nichts starten können.
|
|
235
339
|
ipcMain.handle('hmgr:quelleOeffnen', async (_event, quelleText) => auffangen(async () => {
|
|
236
340
|
const entscheidung = entscheideOeffnen(quelleAusText(quelleText));
|
|
@@ -247,7 +351,83 @@ function registriereIpc() {
|
|
|
247
351
|
}
|
|
248
352
|
return entscheidung;
|
|
249
353
|
}));
|
|
354
|
+
// MCP-Ansicht (E61-Folgerunde): Whitelisting und der F39-Vermerk je Karte stehen in mcp-ansicht.ts
|
|
355
|
+
// (rein, ohne Electron, wie oeffnen.ts und icons.ts), damit sie testbar sind. Der Main-Prozess
|
|
356
|
+
// ruft nur noch auf und liefert die eigene Prozess-Umgebung für die F39-Prüfung.
|
|
357
|
+
ipcMain.handle('hmgr:mcpUebersicht', async () => auffangen(async () => mcpUebersichtSerialisieren(await anwendungsfaelle.mcpUebersicht(), process.env)));
|
|
358
|
+
// Plätze für den Einhängen-Dialog (F42): die Fähigkeit hängt am Adapter, nicht am Namen (E16),
|
|
359
|
+
// deshalb kommt die Liste unverändert aus dem Kern statt aus einer Harness-Fallunterscheidung hier.
|
|
360
|
+
ipcMain.handle('hmgr:mcpPlaetze', async () => auffangen(async () => anwendungsfaelle.mcpPlaetze()));
|
|
361
|
+
ipcMain.handle('hmgr:mcpEinhaengen', async (_event, args) => auffangen(async () => mcpWirkungSerialisieren(await anwendungsfaelle.mcpEinhaengen({ name: args.name, ziel: zielAusIpc(args.harness, args.ort) }))));
|
|
362
|
+
ipcMain.handle('hmgr:mcpAushaengen', async (_event, args) => auffangen(async () => mcpWirkungSerialisieren(await anwendungsfaelle.mcpAushaengen({ name: args.name, ziel: zielAusIpc(args.harness, args.ort) }))));
|
|
363
|
+
// Übernehmen aus der geteilten .mcp.json (E64): die Bestätigung holt der Renderer ein, hier läuft
|
|
364
|
+
// nur die Ausführung. Der Name muss nicht im Katalog stehen.
|
|
365
|
+
ipcMain.handle('hmgr:mcpUebernehmen', async (_event, args) => auffangen(async () => mcpUebernahmeSerialisieren(await anwendungsfaelle.mcpUebernehmen({ name: args.name, ziel: zielAusIpc(args.harness, args.ort) }))));
|
|
366
|
+
}
|
|
367
|
+
/** Mindestabstand zwischen zwei Fokus-Reloads (E62): schnelles Fenster-Wechseln löst keine Lawine
|
|
368
|
+
von Anzeige-Neuladungen aus. Getrennt vom Entprellen im Datei-Watcher (das sammelt Events vor dem
|
|
369
|
+
ersten Aufruf), hier wird zusätzlich zwischen aufeinanderfolgenden Aufrufen gedrosselt (trailing:
|
|
370
|
+
ein Aufruf innerhalb der Sperrzeit wird nicht verworfen, sondern ans Ende der Sperrzeit verschoben). */
|
|
371
|
+
const FOKUS_RELOAD_MIN_ABSTAND_MS = 3000;
|
|
372
|
+
/** Sammelzeit des Datei-Watchers (E62): mehrere Events in kurzer Folge lösen einen Reload aus. */
|
|
373
|
+
const BEOBACHTER_ENTPRELLEN_MS = 400;
|
|
374
|
+
/**
|
|
375
|
+
* Pfade, die die GUI kennt und darum beobachtet (E62): hmgr-Zustand samt MCP-Katalog, die
|
|
376
|
+
* Harness-MCP-Konfigurationen (`~/.claude.json`, Hermes' `config.yaml`) und die `.env`-Dateien, die
|
|
377
|
+
* die F39-Prüfung beim MCP-Einhängen liest — bei Hermes liegt die `.env` je Ort neben der
|
|
378
|
+
* `config.yaml` (so liest sie der Adapter in `mcp.envDatei`), darum auch je Profil. Die Profile
|
|
379
|
+
* werden einmal beim Start am Harness selbst erfragt (`profile()`, ein Verzeichnis-Blick, kein
|
|
380
|
+
* ganzer Erhebungslauf); ein zur Laufzeit neu angelegtes Profil beobachtet der Watcher nicht
|
|
381
|
+
* dynamisch, das deckt der Fokus-Reload ab (E62).
|
|
382
|
+
*
|
|
383
|
+
* Nicht dabei: die geteilten `.mcp.json` der Arbeitsverzeichnisse, die die MCP-Übersicht seit dem
|
|
384
|
+
* E61-Nachtrag mitliest. Ihre Liste ist zur Laufzeit veränderlich, wie die der Hermes-Profile, und
|
|
385
|
+
* damit gilt für sie dasselbe: der Fokus-Reload deckt sie ab.
|
|
386
|
+
*/
|
|
387
|
+
async function beobachtetePfade() {
|
|
388
|
+
const hermesHome = path.join(umgebung.home, '.hermes');
|
|
389
|
+
const pfade = [
|
|
390
|
+
path.join(umgebung.hmgrHome, 'state.json'),
|
|
391
|
+
path.join(umgebung.hmgrHome, 'mcp.yaml'),
|
|
392
|
+
path.join(umgebung.home, '.claude.json'),
|
|
393
|
+
path.join(hermesHome, 'config.yaml'),
|
|
394
|
+
path.join(hermesHome, '.env'),
|
|
395
|
+
];
|
|
396
|
+
const hermes = ports.harnesses.find((h) => h.name === 'hermes');
|
|
397
|
+
if (hermes !== undefined && kannProfile(hermes))
|
|
398
|
+
for (const name of await hermes.profile().catch(() => [])) {
|
|
399
|
+
const profilHome = path.join(hermesHome, 'profiles', name);
|
|
400
|
+
pfade.push(path.join(profilHome, 'config.yaml'), path.join(profilHome, '.env'));
|
|
401
|
+
}
|
|
402
|
+
return pfade;
|
|
250
403
|
}
|
|
404
|
+
/**
|
|
405
|
+
* Stößt beim Renderer ein stilles Neuladen der Anzeige an (E62), nie einen Aktualisieren-Lauf mit
|
|
406
|
+
* Netzzugriff. Gedrosselt (trailing throttle): unmittelbar, wenn seit dem letzten Anstoß genug Zeit
|
|
407
|
+
* verging, sonst genau ein nachgeholter Anstoß am Ende der Sperrzeit.
|
|
408
|
+
*/
|
|
409
|
+
function neuLadenAnstossenFabrik(minAbstandMs) {
|
|
410
|
+
let letzter = 0;
|
|
411
|
+
let timer;
|
|
412
|
+
return function anstossen() {
|
|
413
|
+
const jetzt = Date.now();
|
|
414
|
+
const seitLetztem = jetzt - letzter;
|
|
415
|
+
if (seitLetztem >= minAbstandMs) {
|
|
416
|
+
letzter = jetzt;
|
|
417
|
+
hauptfenster?.webContents.send('hmgr:neuLadenAnstossen');
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
if (timer !== undefined)
|
|
421
|
+
return; // schon ein Anstoß fürs Ende der Sperrzeit vorgemerkt
|
|
422
|
+
timer = setTimeout(() => {
|
|
423
|
+
timer = undefined;
|
|
424
|
+
letzter = Date.now();
|
|
425
|
+
hauptfenster?.webContents.send('hmgr:neuLadenAnstossen');
|
|
426
|
+
}, minAbstandMs - seitLetztem);
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
const neuLadenAnstossen = neuLadenAnstossenFabrik(FOKUS_RELOAD_MIN_ABSTAND_MS);
|
|
430
|
+
let beobachter;
|
|
251
431
|
function fensterOeffnen() {
|
|
252
432
|
hauptfenster = new BrowserWindow({
|
|
253
433
|
width: 1280,
|
|
@@ -255,6 +435,11 @@ function fensterOeffnen() {
|
|
|
255
435
|
minWidth: 860,
|
|
256
436
|
minHeight: 560,
|
|
257
437
|
backgroundColor: '#E3E7E0', // --platte, verhindert einen weißen Blitz beim Start
|
|
438
|
+
// E72: der Fenstertitel nennt die Version, damit im Feld erkennbar ist, welcher Stand läuft.
|
|
439
|
+
// Nicht `app.getVersion()`: das liefert nur im gepackten Bundle die Paketversion, beim Start
|
|
440
|
+
// über `electron dist/gui/main.js` (npm run gui, Smoke-Test) dagegen die Electron-Version —
|
|
441
|
+
// dort liegt neben dem Hauptskript keine package.json (gemessen: 44.0.0 statt 0.0.6).
|
|
442
|
+
title: `hmgr ${paketVersion()}`,
|
|
258
443
|
// Im Smoke-Test bleibt das Fenster unsichtbar: der Renderer lädt trotzdem vollständig, der
|
|
259
444
|
// DOM-Auszug kommt per executeJavaScript. So klaut ein automatisierter Lauf niemandem den
|
|
260
445
|
// Eingabefokus und öffnet kein sichtbares Fenster.
|
|
@@ -269,6 +454,9 @@ function fensterOeffnen() {
|
|
|
269
454
|
hauptfenster.on('closed', () => {
|
|
270
455
|
hauptfenster = null;
|
|
271
456
|
});
|
|
457
|
+
// Der <title> der index.html setzt den Fenstertitel beim Laden sonst zurück und überschriebe die
|
|
458
|
+
// Option oben (E72). Der DOM-Titel bleibt dabei, wie er ist, nur das Fenster folgt ihm nicht mehr.
|
|
459
|
+
hauptfenster.on('page-title-updated', (ereignis) => ereignis.preventDefault());
|
|
272
460
|
// Renderer-Konsole im Main-Log, für Fehlersuche und den Smoke-Test.
|
|
273
461
|
hauptfenster.webContents.on('console-message', (event) => {
|
|
274
462
|
console.log(`[renderer] ${event.message}`);
|
|
@@ -368,11 +556,17 @@ function fensterOeffnen() {
|
|
|
368
556
|
// E60: Beschreibung in der Ziel-Tabelle (nicht nur im Katalog), „manuell" entfällt,
|
|
369
557
|
// Zahnrad statt Sonne am Einstellungen-Knopf.
|
|
370
558
|
var zielBeschreibung = document.querySelector('#zielliste .linkliste .was')?.textContent ?? null;
|
|
559
|
+
// E55-Marken auch in der Ziel-Tabelle, unter der Beschreibung (2026-09-05).
|
|
560
|
+
var zielMarken = document.querySelector('#zielliste .linkliste .wasfeld .marken')?.textContent
|
|
561
|
+
?.replace(/\\s+/g, ' ').trim() ?? null;
|
|
371
562
|
var zielManuellVorhanden = (document.getElementById('zielliste')?.textContent ?? '').includes('manuell');
|
|
372
563
|
var zahnradVorhanden = (document.querySelector('#einstellungen svg')?.innerHTML ?? '').includes('6.47 3.56');
|
|
373
564
|
// Katalog (E40 R3, R5): Beschreibung aus dem Fixture muss in der Skill-Zeile stehen.
|
|
374
565
|
document.querySelector('[data-ansicht="katalog"]')?.click();
|
|
375
566
|
var katalogBeschreibung = document.querySelector('#katalog .zeile .beschreibung')?.textContent ?? null;
|
|
567
|
+
// E70/F70=O1: die Herkunftszeile trägt hinter dem Ref das Änderungsdatum des Skills.
|
|
568
|
+
var katalogHerkunft = document.querySelector('#katalog .zeile .herkunft')?.textContent
|
|
569
|
+
?.replace(/\\s+/g, ' ').trim() ?? null;
|
|
376
570
|
// Besonderheiten aus dem Frontmatter (E55): Marken an der Zeile, Details erst nach
|
|
377
571
|
// einem Klick auf den Aufklapp-Knopf.
|
|
378
572
|
var katalogMarken = document.querySelector('#katalog .zeile .marken')?.textContent
|
|
@@ -390,12 +584,173 @@ function fensterOeffnen() {
|
|
|
390
584
|
var dskKnopf = Array.from(document.querySelectorAll('button'))
|
|
391
585
|
.find((b) => b.textContent === 'Skills hinzufügen');
|
|
392
586
|
var dskMarken = null;
|
|
587
|
+
// Bundle-Sektion im selben Dialog (E69, F68): sie steht dort, wo die Entscheidung
|
|
588
|
+
// fällt, was ins Ziel kommt, und zeigt je Bundle Beschreibung und Herkunft.
|
|
589
|
+
var dskBundleBoxSichtbar = false;
|
|
590
|
+
var dskBundleText = null;
|
|
393
591
|
if (dskKnopf) {
|
|
394
592
|
dskKnopf.click();
|
|
395
593
|
dskMarken = document.querySelector('#dsk-liste .wahlzeile .marken')?.textContent
|
|
396
594
|
?.replace(/\\s+/g, ' ').trim() ?? null;
|
|
595
|
+
dskBundleBoxSichtbar = document.getElementById('dsk-bundlebox')?.hidden === false;
|
|
596
|
+
dskBundleText = document.querySelector('#dsk-bundles .wahlzeile')?.textContent
|
|
597
|
+
?.replace(/\\s+/g, ' ').trim() ?? null;
|
|
397
598
|
document.getElementById('d-skills')?.close();
|
|
398
599
|
}
|
|
600
|
+
// Bundle-Gruppe an der Ziel-Karte (E69): Kopf mit Name, Quelle und Entfernen, darunter
|
|
601
|
+
// die Zeilen der Mitglieder — sie stehen in der Gruppe, nicht in der allgemeinen Liste.
|
|
602
|
+
var bundleGruppeText = document.querySelector('#zielliste .bundlegruppe .bundlekopf')?.textContent
|
|
603
|
+
?.replace(/\\s+/g, ' ').trim() ?? null;
|
|
604
|
+
var bundleGruppeZeilen = Array.from(document.querySelectorAll('#zielliste .bundlegruppe .linkliste .name'))
|
|
605
|
+
.map((n) => n.textContent.trim()).join(',');
|
|
606
|
+
// Entfernen an der Einzelzeile innerhalb der Gruppe sagt, dass das Bundle zerbricht.
|
|
607
|
+
var bundleZerbrichtText = null;
|
|
608
|
+
try {
|
|
609
|
+
document.querySelector('#zielliste .bundlegruppe .linkliste .knopf')?.click();
|
|
610
|
+
bundleZerbrichtText = document.getElementById('dx-text')?.textContent ?? null;
|
|
611
|
+
document.querySelector('#d-entfernen button[value="nein"]')?.click();
|
|
612
|
+
} catch (fehler) {
|
|
613
|
+
bundleZerbrichtText = String(fehler);
|
|
614
|
+
}
|
|
615
|
+
// MCP-Ansicht (E61-Folgerunde, F46: abgespeckt zum reinen Katalog): Navigationsreihenfolge
|
|
616
|
+
// Ziele, MCP, Plugins, Skills, Karten mit Name/Beschreibung/Homepage-Link, F39-Warnzeile,
|
|
617
|
+
// Einhängen-Dialog öffnen/abbrechen ohne IPC-Schreibaktion. Orte-Liste und Fremdkasten
|
|
618
|
+
// gehören seit F46 nicht mehr in diese Ansicht (die Prüfung darauf ist unten invertiert),
|
|
619
|
+
// sie leben nur noch am MCP-Abschnitt der Ziel-Karten (weiter unten unverändert geprüft).
|
|
620
|
+
var navReihenfolge = Array.from(document.querySelectorAll('.nav [data-ansicht]'))
|
|
621
|
+
.map((b) => b.dataset.ansicht).join(',');
|
|
622
|
+
document.querySelector('[data-ansicht="mcp"]')?.click();
|
|
623
|
+
var mcpKartenText = document.getElementById('mcpliste')?.textContent
|
|
624
|
+
?.replace(/\\s+/g, ' ').trim() ?? null;
|
|
625
|
+
var mcpHeimLinkVorhanden = document.querySelector('#mcpliste .mcpkopf .qlink') !== null;
|
|
626
|
+
var mcpWarnzeileVorhanden = document.querySelector('#mcpliste .mcp .ruhezeile') !== null;
|
|
627
|
+
// F46: die Orte-Liste je Karte und der Fremdkasten der Ansicht sind ganz weg, nicht nur
|
|
628
|
+
// leer — beide Selektoren dürfen im DOM überhaupt nicht mehr auftauchen.
|
|
629
|
+
var mcpOrteVorhanden = document.querySelector('#mcpliste .mcporte') !== null;
|
|
630
|
+
var mcpFremdkastenVorhanden = document.getElementById('mcpfremd') !== null;
|
|
631
|
+
var mcpFehler = null;
|
|
632
|
+
var dMcpOffenNachOeffnen = false;
|
|
633
|
+
var dMcpOffenNachAbbrechen = true;
|
|
634
|
+
// Die Wahlzeilen des Einhängen-Dialogs (F42): sie kommen aus der Adapter-Fähigkeit,
|
|
635
|
+
// hier steht, welche Orte zur Wahl stehen und wohin der Eintrag jeweils ginge. Zwei
|
|
636
|
+
// Karten, weil die Zeilen verschieden aussehen: „mail" hängt an beiden Home-Orten
|
|
637
|
+
// schon (gesperrte Zeile), „teamwork" nirgends (die Notizen je Ort).
|
|
638
|
+
var mcpWahlText = null;
|
|
639
|
+
var mcpWahlTextFrei = null;
|
|
640
|
+
try {
|
|
641
|
+
var mcpEinKnoepfe = Array.from(document.querySelectorAll('#mcpliste .mcpkopf button'))
|
|
642
|
+
.filter((b) => (b.textContent || '').indexOf('Einhängen') === 0);
|
|
643
|
+
var mcpWahlOeffnen = async (knopf) => {
|
|
644
|
+
knopf?.click();
|
|
645
|
+
// window.hmgr.mcpPlaetze() ist asynchron: kurz nachfassen, bis die Wahlzeilen stehen.
|
|
646
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
647
|
+
return document.getElementById('dm-orte')?.textContent?.replace(/\\s+/g, ' ').trim() ?? null;
|
|
648
|
+
};
|
|
649
|
+
mcpWahlText = await mcpWahlOeffnen(mcpEinKnoepfe[0]);
|
|
650
|
+
dMcpOffenNachOeffnen = document.getElementById('d-mcp')?.open === true;
|
|
651
|
+
document.querySelector('#d-mcp button[value="abbruch"]')?.click();
|
|
652
|
+
dMcpOffenNachAbbrechen = document.getElementById('d-mcp')?.open === true;
|
|
653
|
+
mcpWahlTextFrei = await mcpWahlOeffnen(mcpEinKnoepfe[1]);
|
|
654
|
+
document.querySelector('#d-mcp button[value="abbruch"]')?.click();
|
|
655
|
+
} catch (fehler) {
|
|
656
|
+
mcpFehler = String(fehler);
|
|
657
|
+
}
|
|
658
|
+
// Aushängen-Rückfrage (F46: nur noch über den Entfernen-Knopf am MCP-Abschnitt einer
|
|
659
|
+
// Ziel-Karte auslösbar, die MCP-Ansicht selbst hat dafür keinen Knopf mehr).
|
|
660
|
+
var mcpAusFehler = null;
|
|
661
|
+
var dMcpAusOffenNachOeffnen = false;
|
|
662
|
+
var dMcpAusOffenNachAbbrechen = true;
|
|
663
|
+
// Seit E64 stehen im MCP-Abschnitt zwei Knopfarten nebeneinander (Entfernen und
|
|
664
|
+
// Übernehmen), deshalb wird hier nicht mehr der erste Knopf genommen, sondern der
|
|
665
|
+
// Entfernen-Knopf an seinem aria-label erkannt.
|
|
666
|
+
var mcpZeilenKnoepfe = Array.from(document.querySelectorAll('#zielliste .mcpkartezeilen button'));
|
|
667
|
+
try {
|
|
668
|
+
var mcpAusKnopf = mcpZeilenKnoepfe.find(function (b) {
|
|
669
|
+
return (b.getAttribute('aria-label') || '').indexOf('aushängen') > -1;
|
|
670
|
+
});
|
|
671
|
+
mcpAusKnopf?.click();
|
|
672
|
+
dMcpAusOffenNachOeffnen = document.getElementById('d-mcpaus')?.open === true;
|
|
673
|
+
document.querySelector('#d-mcpaus button[value="nein"]')?.click();
|
|
674
|
+
dMcpAusOffenNachAbbrechen = document.getElementById('d-mcpaus')?.open === true;
|
|
675
|
+
} catch (fehler) {
|
|
676
|
+
mcpAusFehler = String(fehler);
|
|
677
|
+
}
|
|
678
|
+
// Übernehmen aus der geteilten .mcp.json (E64): Knopf an einer geteilten Zeile,
|
|
679
|
+
// Rückfrage öffnen und über „Nein" wieder schließen — ohne IPC-Schreibaktion, wie bei
|
|
680
|
+
// den anderen MCP-Dialogen. Der Text der Rückfrage läuft mit, er ist die Warnung an
|
|
681
|
+
// den Nutzer und darf nicht stillschweigend verschwinden.
|
|
682
|
+
var mcpUebFehler = null;
|
|
683
|
+
var dMcpUebOffenNachOeffnen = false;
|
|
684
|
+
var dMcpUebOffenNachAbbrechen = true;
|
|
685
|
+
var mcpUebText = null;
|
|
686
|
+
try {
|
|
687
|
+
var mcpUebKnopf = mcpZeilenKnoepfe.find(function (b) {
|
|
688
|
+
return (b.textContent || '').indexOf('Übernehmen') === 0;
|
|
689
|
+
});
|
|
690
|
+
mcpUebKnopf?.click();
|
|
691
|
+
dMcpUebOffenNachOeffnen = document.getElementById('d-mcpueb')?.open === true;
|
|
692
|
+
mcpUebText = document.getElementById('dmu-text')?.textContent?.replace(/\\s+/g, ' ').trim() ?? null;
|
|
693
|
+
document.querySelector('#d-mcpueb button[value="nein"]')?.click();
|
|
694
|
+
dMcpUebOffenNachAbbrechen = document.getElementById('d-mcpueb')?.open === true;
|
|
695
|
+
} catch (fehler) {
|
|
696
|
+
mcpUebFehler = String(fehler);
|
|
697
|
+
}
|
|
698
|
+
// MCP an der Ziel-Karte (E61-GUI-Nachtrag), Gegenrichtung zur MCP-Ansicht: Abschnitt und
|
|
699
|
+
// Knopf an der Hermes-Home-Karte (Katalog-Zeile mit Entfernen, Fremd-Zeile ohne Knopf)
|
|
700
|
+
// und an der kunde-x-Verzeichnis-Karte (zwei Zeilen aus der geteilten .mcp.json, seit
|
|
701
|
+
// E64 beide mit Übernehmen-Knopf), dazu die Knopf-Sichtbarkeit: an der agents-Karte
|
|
702
|
+
// darf weder Abschnitt noch Knopf stehen, agents implementiert die MCP-Fähigkeit
|
|
703
|
+
// nicht (E16).
|
|
704
|
+
var zielKarten = Array.from(document.querySelectorAll('#zielliste .ziel'));
|
|
705
|
+
var karteMitKopftext = function (t) {
|
|
706
|
+
return zielKarten.find(function (k) {
|
|
707
|
+
return (k.querySelector('.zielkopf')?.textContent || '').indexOf(t) > -1;
|
|
708
|
+
});
|
|
709
|
+
};
|
|
710
|
+
var mcpHinzuKnopfAn = function (karte) {
|
|
711
|
+
return karte
|
|
712
|
+
? Array.from(karte.querySelectorAll('button')).find((b) => (b.textContent || '').indexOf('MCP hinzufügen') === 0)
|
|
713
|
+
: undefined;
|
|
714
|
+
};
|
|
715
|
+
var hermesKarte = karteMitKopftext('Hermes · Home');
|
|
716
|
+
var hermesMcpText = hermesKarte?.querySelector('.mcpkarteabschnitt')?.textContent
|
|
717
|
+
?.replace(/\\s+/g, ' ').trim() ?? null;
|
|
718
|
+
var hermesMcpZeilenKnopfZahl = hermesKarte?.querySelectorAll('.mcpkartezeilen button').length ?? 0;
|
|
719
|
+
var hermesMcpKnopfVorhanden = mcpHinzuKnopfAn(hermesKarte) !== undefined;
|
|
720
|
+
var kundeXKarte = karteMitKopftext('kunde-x');
|
|
721
|
+
var kundeXMcpText = kundeXKarte?.querySelector('.mcpkarteabschnitt')?.textContent
|
|
722
|
+
?.replace(/\\s+/g, ' ').trim() ?? null;
|
|
723
|
+
var kundeXMcpZeilenKnopfZahl = kundeXKarte?.querySelectorAll('.mcpkartezeilen button').length ?? 0;
|
|
724
|
+
var kundeXMcpKnopfVorhanden = mcpHinzuKnopfAn(kundeXKarte) !== undefined;
|
|
725
|
+
// Kartenkopf-Zahl (E71): der Kopf zählt Skills, gebündelt wie der Körper zeilt. An der
|
|
726
|
+
// Verzeichnis-Karte kunde-x hängen dieselben zwei Skills an zwei Harnesses, eine Summe
|
|
727
|
+
// über die Abschnitte ergäbe hier 4.
|
|
728
|
+
var kopfZahlVon = function (karte) {
|
|
729
|
+
return karte?.querySelector('.zielkopf .zahl')?.textContent?.trim() ?? null;
|
|
730
|
+
};
|
|
731
|
+
var kundeXKopfZahl = kopfZahlVon(kundeXKarte);
|
|
732
|
+
var hermesKopfZahl = kopfZahlVon(hermesKarte);
|
|
733
|
+
var agentsKarte = karteMitKopftext('Agents · Home');
|
|
734
|
+
// Ohne diese Zusicherung wären die beiden Abwesenheits-Prüfungen darunter leer erfüllt,
|
|
735
|
+
// sobald die Karte selbst aus der Liste fiele: „kein Abschnitt" hieße dann nur „keine Karte".
|
|
736
|
+
var agentsKarteVorhanden = agentsKarte !== undefined;
|
|
737
|
+
var agentsMcpAbschnittVorhanden = (agentsKarte?.querySelector('.mcpkarteabschnitt') ?? null) !== null;
|
|
738
|
+
var agentsMcpKnopfVorhanden = mcpHinzuKnopfAn(agentsKarte) !== undefined;
|
|
739
|
+
// Dialog „MCP hinzufügen" (E47-Muster): öffnen an der Hermes-Karte und über Abbrechen
|
|
740
|
+
// wieder schließen, ohne IPC-Schreibaktion (mcpEinhaengen liefe erst nach „Einhängen").
|
|
741
|
+
var dmhFehler = null;
|
|
742
|
+
var dMcpHinzuOffenNachOeffnen = false;
|
|
743
|
+
var dMcpHinzuOffenNachAbbrechen = true;
|
|
744
|
+
var dmhListeText = null;
|
|
745
|
+
try {
|
|
746
|
+
mcpHinzuKnopfAn(hermesKarte)?.click();
|
|
747
|
+
dMcpHinzuOffenNachOeffnen = document.getElementById('d-mcp-hinzufuegen')?.open === true;
|
|
748
|
+
dmhListeText = document.getElementById('dmh-liste')?.textContent?.replace(/\\s+/g, ' ').trim() ?? null;
|
|
749
|
+
document.querySelector('#d-mcp-hinzufuegen button[value="abbruch"]')?.click();
|
|
750
|
+
dMcpHinzuOffenNachAbbrechen = document.getElementById('d-mcp-hinzufuegen')?.open === true;
|
|
751
|
+
} catch (fehler) {
|
|
752
|
+
dmhFehler = String(fehler);
|
|
753
|
+
}
|
|
399
754
|
// Schriften aus fonts/ (Zusatzpunkt 6): erzwingt das Laden und belegt es danach.
|
|
400
755
|
await Promise.all([
|
|
401
756
|
document.fonts.load('400 16px Inter'),
|
|
@@ -409,15 +764,22 @@ function fensterOeffnen() {
|
|
|
409
764
|
einstellungenOffen,
|
|
410
765
|
harnessSchalter,
|
|
411
766
|
zielBeschreibung,
|
|
767
|
+
zielMarken,
|
|
412
768
|
zielManuellVorhanden,
|
|
413
769
|
zahnradVorhanden,
|
|
414
770
|
katalogBeschreibung,
|
|
771
|
+
katalogHerkunft,
|
|
415
772
|
katalogMarken,
|
|
416
773
|
katalogDetailsZu,
|
|
417
774
|
katalogAufgeklappt,
|
|
418
775
|
katalogDetails,
|
|
419
776
|
katalogMarkenWarnend,
|
|
420
777
|
dskMarken,
|
|
778
|
+
dskBundleBoxSichtbar,
|
|
779
|
+
dskBundleText,
|
|
780
|
+
bundleGruppeText,
|
|
781
|
+
bundleGruppeZeilen,
|
|
782
|
+
bundleZerbrichtText,
|
|
421
783
|
interGeladen: document.fonts.check('400 16px Inter'),
|
|
422
784
|
monoGeladen: document.fonts.check('500 16px "JetBrains Mono"'),
|
|
423
785
|
quelleneuVorhanden,
|
|
@@ -443,9 +805,45 @@ function fensterOeffnen() {
|
|
|
443
805
|
dSkillordnerOffenNachOeffnen,
|
|
444
806
|
dSkillordnerOffenNachAbbrechen,
|
|
445
807
|
skillOrdnerFehler,
|
|
808
|
+
navReihenfolge,
|
|
809
|
+
mcpKartenText,
|
|
810
|
+
mcpHeimLinkVorhanden,
|
|
811
|
+
mcpWarnzeileVorhanden,
|
|
812
|
+
mcpOrteVorhanden,
|
|
813
|
+
mcpFremdkastenVorhanden,
|
|
814
|
+
dMcpOffenNachOeffnen,
|
|
815
|
+
dMcpOffenNachAbbrechen,
|
|
816
|
+
mcpWahlText,
|
|
817
|
+
mcpWahlTextFrei,
|
|
818
|
+
mcpFehler,
|
|
819
|
+
dMcpAusOffenNachOeffnen,
|
|
820
|
+
dMcpAusOffenNachAbbrechen,
|
|
821
|
+
mcpAusFehler,
|
|
822
|
+
dMcpUebOffenNachOeffnen,
|
|
823
|
+
dMcpUebOffenNachAbbrechen,
|
|
824
|
+
mcpUebText,
|
|
825
|
+
mcpUebFehler,
|
|
826
|
+
hermesMcpText,
|
|
827
|
+
hermesMcpZeilenKnopfZahl,
|
|
828
|
+
hermesMcpKnopfVorhanden,
|
|
829
|
+
kundeXMcpText,
|
|
830
|
+
kundeXMcpZeilenKnopfZahl,
|
|
831
|
+
kundeXMcpKnopfVorhanden,
|
|
832
|
+
kundeXKopfZahl,
|
|
833
|
+
hermesKopfZahl,
|
|
834
|
+
agentsKarteVorhanden,
|
|
835
|
+
agentsMcpAbschnittVorhanden,
|
|
836
|
+
agentsMcpKnopfVorhanden,
|
|
837
|
+
dMcpHinzuOffenNachOeffnen,
|
|
838
|
+
dMcpHinzuOffenNachAbbrechen,
|
|
839
|
+
dmhListeText,
|
|
840
|
+
dmhFehler,
|
|
446
841
|
});
|
|
447
842
|
})()`)
|
|
448
843
|
.then((auszug) => console.log(`[smoke] DOM-Auszug: ${auszug}`))
|
|
844
|
+
// E72: der Fenstertitel steht nicht im DOM (dort steht weiter der <title> der
|
|
845
|
+
// index.html), er lässt sich nur hier im Main-Prozess ablesen.
|
|
846
|
+
.then(() => console.log(`[smoke] Fenster: ${JSON.stringify({ fenstertitel: fenster.getTitle() })}`))
|
|
449
847
|
.finally(() => app.quit());
|
|
450
848
|
}, 3000);
|
|
451
849
|
});
|
|
@@ -461,20 +859,65 @@ function fensterOeffnen() {
|
|
|
461
859
|
const intervall = setInterval(() => fenster.webContents.send('hmgr:aktualisierenAnstossen'), SECHS_STUNDEN_MS);
|
|
462
860
|
fenster.on('closed', () => clearInterval(intervall));
|
|
463
861
|
}
|
|
862
|
+
// Die GUI erfährt von Änderungen außerhalb ihrer selbst (E62): Fokus-Reload und Datei-Watcher.
|
|
863
|
+
// Beide stoßen nur ein stilles Neuladen der Anzeige an, nie einen Aktualisieren-Lauf mit
|
|
864
|
+
// Netzzugriff. Im Smoke-Test keins von beiden, dieselbe Zurückhaltung wie beim automatischen
|
|
865
|
+
// Aktualisieren (E44): ein Event dürfte sonst in den DOM-Auszug pfuschen.
|
|
866
|
+
if (!smoketest) {
|
|
867
|
+
const fenster = hauptfenster;
|
|
868
|
+
fenster.on('focus', () => neuLadenAnstossen());
|
|
869
|
+
void beobachtetePfade().then((pfade) => {
|
|
870
|
+
if (fenster.isDestroyed())
|
|
871
|
+
return; // Fenster schon zu, während die Pfade ermittelt wurden
|
|
872
|
+
beobachter = beobachten(pfade, () => neuLadenAnstossen(), { entprellenMs: BEOBACHTER_ENTPRELLEN_MS });
|
|
873
|
+
});
|
|
874
|
+
fenster.on('closed', () => {
|
|
875
|
+
beobachter?.close();
|
|
876
|
+
beobachter = undefined;
|
|
877
|
+
});
|
|
878
|
+
}
|
|
464
879
|
void hauptfenster.loadFile(path.join(hierVerzeichnis, 'renderer', 'index.html'));
|
|
465
880
|
}
|
|
881
|
+
/**
|
|
882
|
+
* Holt das vorhandene Fenster nach vorn — die Antwort der laufenden Instanz auf einen zweiten
|
|
883
|
+
* Start (E67). Minimiert kommt es erst wieder hoch, dann bekommt es den Fokus.
|
|
884
|
+
*/
|
|
885
|
+
function fensterNachVornHolen() {
|
|
886
|
+
if (hauptfenster === null || hauptfenster.isDestroyed())
|
|
887
|
+
return;
|
|
888
|
+
if (hauptfenster.isMinimized())
|
|
889
|
+
hauptfenster.restore();
|
|
890
|
+
hauptfenster.show();
|
|
891
|
+
hauptfenster.focus();
|
|
892
|
+
}
|
|
466
893
|
registriereIpc();
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
894
|
+
if (einzelinstanz) {
|
|
895
|
+
app.on('second-instance', () => fensterNachVornHolen());
|
|
896
|
+
app.whenReady().then(() => {
|
|
897
|
+
// Im Smoke-Test kein Dock-Icon und kein Fokuswechsel: das Fenster bleibt unsichtbar (show:false
|
|
898
|
+
// oben), macOS würde aber ohne diese Zeile trotzdem ein Dock-Icon zeigen und aktivieren.
|
|
899
|
+
if (smoketest && process.platform === 'darwin')
|
|
900
|
+
app.dock?.hide();
|
|
901
|
+
// Windows-Fensterkonventionen (E67), beide nur dort:
|
|
902
|
+
// — Die Taskleiste gruppiert Fenster und Verknüpfungen über die AppUserModelID. Ohne sie steht
|
|
903
|
+
// der Prozess unter einer von Windows geratenen ID, und das angeheftete Startmenü-Symbol
|
|
904
|
+
// gehört dann nicht zum laufenden Fenster. Dieselbe ID wie die `appId` im build-Block, denn
|
|
905
|
+
// die schreibt der NSIS-Installer in die Verknüpfung.
|
|
906
|
+
// — Electron blendet unter Windows sonst sein Standardmenü ein (Datei/Bearbeiten/Ansicht/…),
|
|
907
|
+
// eine Menüleiste, die hmgr nicht bedient und die nur Platz über der Ansicht kostet. macOS
|
|
908
|
+
// bleibt unangetastet: dort ist die Menüleiste des Systems Pflicht (Beenden, Kopieren,
|
|
909
|
+
// Einfügen hängen daran).
|
|
910
|
+
if (process.platform === 'win32') {
|
|
911
|
+
app.setAppUserModelId('de.timkaufmann.hmgr');
|
|
912
|
+
Menu.setApplicationMenu(null);
|
|
913
|
+
}
|
|
914
|
+
fensterOeffnen();
|
|
915
|
+
app.on('activate', () => {
|
|
916
|
+
if (BrowserWindow.getAllWindows().length === 0)
|
|
917
|
+
fensterOeffnen();
|
|
918
|
+
});
|
|
476
919
|
});
|
|
477
|
-
}
|
|
920
|
+
}
|
|
478
921
|
app.on('window-all-closed', () => {
|
|
479
922
|
if (process.platform !== 'darwin')
|
|
480
923
|
app.quit();
|