wasibase 1.0.0
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 +69 -0
- package/bin/wasibase.js +5 -0
- package/package.json +54 -0
- package/src/config.js +11 -0
- package/src/index.js +54 -0
- package/src/storage.js +262 -0
- package/src/ui/backup.js +248 -0
- package/src/ui/graph.js +21 -0
- package/src/ui/manage.js +320 -0
- package/src/ui/note.js +449 -0
- package/src/ui/search.js +21 -0
- package/src/ui/sync.js +348 -0
- package/src/utils.js +104 -0
- package/src/web/graphServer.js +897 -0
- package/src/web/server.js +2132 -0
|
@@ -0,0 +1,897 @@
|
|
|
1
|
+
import express from 'express';
|
|
2
|
+
import { marked } from 'marked';
|
|
3
|
+
import * as storage from '../storage.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Extrahiert alle Backlinks aus einem Markdown-Inhalt
|
|
7
|
+
*/
|
|
8
|
+
function extractBacklinks(content) {
|
|
9
|
+
if (!content) return [];
|
|
10
|
+
const regex = /\[\[([^\]]+)\]\]/g;
|
|
11
|
+
const matches = [];
|
|
12
|
+
let match;
|
|
13
|
+
while ((match = regex.exec(content)) !== null) {
|
|
14
|
+
matches.push(match[1].trim());
|
|
15
|
+
}
|
|
16
|
+
return [...new Set(matches)];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Baut hierarchische Graph-Daten auf
|
|
21
|
+
*/
|
|
22
|
+
function buildGraphData() {
|
|
23
|
+
const nodes = [];
|
|
24
|
+
const links = [];
|
|
25
|
+
const noteNodeMap = new Map();
|
|
26
|
+
|
|
27
|
+
const oberkategorien = storage.getOberkategorien();
|
|
28
|
+
|
|
29
|
+
for (const ober of oberkategorien) {
|
|
30
|
+
const oberId = 'ober:' + ober;
|
|
31
|
+
nodes.push({
|
|
32
|
+
id: oberId,
|
|
33
|
+
label: ober,
|
|
34
|
+
type: 'oberkategorie'
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const unterkategorien = storage.getUnterkategorien(ober);
|
|
38
|
+
|
|
39
|
+
for (const unter of unterkategorien) {
|
|
40
|
+
const unterId = 'unter:' + ober + '/' + unter;
|
|
41
|
+
nodes.push({
|
|
42
|
+
id: unterId,
|
|
43
|
+
label: unter,
|
|
44
|
+
type: 'unterkategorie',
|
|
45
|
+
parent: oberId
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
links.push({
|
|
49
|
+
source: oberId,
|
|
50
|
+
target: unterId,
|
|
51
|
+
type: 'hierarchy'
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const notes = storage.getNotes(ober, unter);
|
|
55
|
+
|
|
56
|
+
for (const thema of notes) {
|
|
57
|
+
const noteId = 'note:' + ober + '/' + unter + '/' + thema;
|
|
58
|
+
const content = storage.readNote(ober, unter, thema);
|
|
59
|
+
const backlinks = extractBacklinks(content);
|
|
60
|
+
|
|
61
|
+
const noteNode = {
|
|
62
|
+
id: noteId,
|
|
63
|
+
label: thema,
|
|
64
|
+
type: 'note',
|
|
65
|
+
oberkategorie: ober,
|
|
66
|
+
unterkategorie: unter,
|
|
67
|
+
thema: thema,
|
|
68
|
+
backlinks: backlinks
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
nodes.push(noteNode);
|
|
72
|
+
noteNodeMap.set(thema.toLowerCase(), noteNode);
|
|
73
|
+
|
|
74
|
+
links.push({
|
|
75
|
+
source: unterId,
|
|
76
|
+
target: noteId,
|
|
77
|
+
type: 'hierarchy'
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Backlink-Verbindungen
|
|
84
|
+
for (const node of nodes) {
|
|
85
|
+
if (node.type === 'note' && node.backlinks) {
|
|
86
|
+
for (const backlinkName of node.backlinks) {
|
|
87
|
+
const targetNode = noteNodeMap.get(backlinkName.toLowerCase());
|
|
88
|
+
if (targetNode && targetNode.id !== node.id) {
|
|
89
|
+
links.push({
|
|
90
|
+
source: node.id,
|
|
91
|
+
target: targetNode.id,
|
|
92
|
+
type: 'backlink'
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return { nodes, links };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function startGraphServer(config, callback) {
|
|
103
|
+
const app = express();
|
|
104
|
+
app.use(express.json());
|
|
105
|
+
|
|
106
|
+
let serverInstance = null;
|
|
107
|
+
const { port = 3335 } = config;
|
|
108
|
+
|
|
109
|
+
app.get('/api/graph', (req, res) => {
|
|
110
|
+
res.json(buildGraphData());
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
app.get('/api/note/:ober/:unter/:thema', (req, res) => {
|
|
114
|
+
const { ober, unter, thema } = req.params;
|
|
115
|
+
const content = storage.readNote(
|
|
116
|
+
decodeURIComponent(ober),
|
|
117
|
+
decodeURIComponent(unter),
|
|
118
|
+
decodeURIComponent(thema)
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
if (!content) {
|
|
122
|
+
return res.status(404).json({ error: 'Note nicht gefunden' });
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const contentWithoutFrontmatter = content.replace(/^---[\s\S]*?---\n?/, '').trim();
|
|
126
|
+
const html = marked(contentWithoutFrontmatter);
|
|
127
|
+
|
|
128
|
+
res.json({
|
|
129
|
+
thema: decodeURIComponent(thema),
|
|
130
|
+
oberkategorie: decodeURIComponent(ober),
|
|
131
|
+
unterkategorie: decodeURIComponent(unter),
|
|
132
|
+
html
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
app.post('/close', (req, res) => {
|
|
137
|
+
res.json({ success: true });
|
|
138
|
+
setTimeout(() => {
|
|
139
|
+
if (serverInstance) serverInstance.close();
|
|
140
|
+
if (callback) callback();
|
|
141
|
+
}, 200);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
app.get('/', (req, res) => {
|
|
145
|
+
res.send(getGraphHTML());
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
serverInstance = app.listen(port);
|
|
149
|
+
return { port, close: () => serverInstance.close() };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function getGraphHTML() {
|
|
153
|
+
return `<!DOCTYPE html>
|
|
154
|
+
<html lang="de">
|
|
155
|
+
<head>
|
|
156
|
+
<meta charset="UTF-8">
|
|
157
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
158
|
+
<title>Wasibase Graph</title>
|
|
159
|
+
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
|
|
160
|
+
<script src="https://d3js.org/d3.v7.min.js"><\/script>
|
|
161
|
+
<script src="https://cdn.jsdelivr.net/npm/dompurify@3.0.6/dist/purify.min.js"><\/script>
|
|
162
|
+
<style>
|
|
163
|
+
:root {
|
|
164
|
+
--bg: #030308;
|
|
165
|
+
--bg-card: rgba(12, 12, 20, 0.9);
|
|
166
|
+
--border: rgba(255, 255, 255, 0.08);
|
|
167
|
+
--border-hover: rgba(255, 255, 255, 0.15);
|
|
168
|
+
--text: #fafafa;
|
|
169
|
+
--text-secondary: #a1a1aa;
|
|
170
|
+
--text-muted: #71717a;
|
|
171
|
+
--accent: #22d3ee;
|
|
172
|
+
--accent-secondary: #a855f7;
|
|
173
|
+
--accent-tertiary: #3b82f6;
|
|
174
|
+
--emerald: #10b981;
|
|
175
|
+
--amber: #f59e0b;
|
|
176
|
+
--gradient-primary: linear-gradient(135deg, #22d3ee 0%, #a855f7 100%);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
180
|
+
|
|
181
|
+
body {
|
|
182
|
+
font-family: 'Outfit', -apple-system, BlinkMacSystemFont, sans-serif;
|
|
183
|
+
background: var(--bg);
|
|
184
|
+
color: var(--text);
|
|
185
|
+
height: 100vh;
|
|
186
|
+
overflow: hidden;
|
|
187
|
+
-webkit-font-smoothing: antialiased;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/* Header */
|
|
191
|
+
header {
|
|
192
|
+
position: fixed;
|
|
193
|
+
top: 0; left: 0; right: 0;
|
|
194
|
+
height: 64px;
|
|
195
|
+
background: rgba(3, 3, 8, 0.85);
|
|
196
|
+
backdrop-filter: blur(20px) saturate(180%);
|
|
197
|
+
-webkit-backdrop-filter: blur(20px) saturate(180%);
|
|
198
|
+
border-bottom: 1px solid var(--border);
|
|
199
|
+
display: flex;
|
|
200
|
+
align-items: center;
|
|
201
|
+
padding: 0 28px;
|
|
202
|
+
z-index: 100;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
.logo-wrap {
|
|
206
|
+
display: flex;
|
|
207
|
+
align-items: center;
|
|
208
|
+
gap: 12px;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
.logo-icon {
|
|
212
|
+
width: 32px;
|
|
213
|
+
height: 32px;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
.logo-icon svg {
|
|
217
|
+
width: 100%;
|
|
218
|
+
height: 100%;
|
|
219
|
+
filter: drop-shadow(0 0 16px rgba(34, 211, 238, 0.4));
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
.logo {
|
|
223
|
+
font-size: 20px;
|
|
224
|
+
font-weight: 700;
|
|
225
|
+
background: var(--gradient-primary);
|
|
226
|
+
-webkit-background-clip: text;
|
|
227
|
+
-webkit-text-fill-color: transparent;
|
|
228
|
+
letter-spacing: -0.02em;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
.stats {
|
|
232
|
+
margin-left: 24px;
|
|
233
|
+
padding: 8px 16px;
|
|
234
|
+
background: rgba(255, 255, 255, 0.03);
|
|
235
|
+
border: 1px solid var(--border);
|
|
236
|
+
border-radius: 10px;
|
|
237
|
+
font-size: 13px;
|
|
238
|
+
font-weight: 500;
|
|
239
|
+
color: var(--text-secondary);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
.header-actions {
|
|
243
|
+
margin-left: auto;
|
|
244
|
+
display: flex;
|
|
245
|
+
gap: 12px;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
.btn {
|
|
249
|
+
background: rgba(255, 255, 255, 0.03);
|
|
250
|
+
border: 1px solid var(--border);
|
|
251
|
+
color: var(--text-secondary);
|
|
252
|
+
padding: 10px 20px;
|
|
253
|
+
border-radius: 12px;
|
|
254
|
+
font-family: inherit;
|
|
255
|
+
font-size: 14px;
|
|
256
|
+
font-weight: 500;
|
|
257
|
+
cursor: pointer;
|
|
258
|
+
transition: all 0.25s ease;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
.btn:hover {
|
|
262
|
+
background: rgba(255, 255, 255, 0.06);
|
|
263
|
+
border-color: var(--border-hover);
|
|
264
|
+
color: var(--text);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
.btn-primary {
|
|
268
|
+
background: var(--gradient-primary);
|
|
269
|
+
border: none;
|
|
270
|
+
color: #000;
|
|
271
|
+
box-shadow: 0 0 20px -4px rgba(34, 211, 238, 0.4);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
.btn-primary:hover {
|
|
275
|
+
box-shadow: 0 0 28px -4px rgba(34, 211, 238, 0.6);
|
|
276
|
+
transform: translateY(-1px);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/* Graph */
|
|
280
|
+
#graph {
|
|
281
|
+
width: 100%;
|
|
282
|
+
height: 100%;
|
|
283
|
+
cursor: grab;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
#graph:active { cursor: grabbing; }
|
|
287
|
+
|
|
288
|
+
/* Nodes */
|
|
289
|
+
.node { cursor: pointer; transition: transform 0.2s ease; }
|
|
290
|
+
|
|
291
|
+
.node-ober rect {
|
|
292
|
+
fill: url(#grad-cyan);
|
|
293
|
+
filter: drop-shadow(0 4px 20px rgba(34, 211, 238, 0.35));
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
.node-unter rect {
|
|
297
|
+
fill: url(#grad-emerald);
|
|
298
|
+
filter: drop-shadow(0 4px 20px rgba(16, 185, 129, 0.35));
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
.node-note circle {
|
|
302
|
+
fill: url(#grad-amber);
|
|
303
|
+
filter: drop-shadow(0 4px 16px rgba(245, 158, 11, 0.35));
|
|
304
|
+
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
.node-note:hover circle {
|
|
308
|
+
filter: drop-shadow(0 8px 28px rgba(245, 158, 11, 0.5));
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
.node text {
|
|
312
|
+
pointer-events: none;
|
|
313
|
+
user-select: none;
|
|
314
|
+
font-weight: 500;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
.node-label {
|
|
318
|
+
fill: var(--text);
|
|
319
|
+
font-size: 12px;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/* Links */
|
|
323
|
+
.link-hierarchy {
|
|
324
|
+
stroke: rgba(255, 255, 255, 0.12);
|
|
325
|
+
stroke-width: 2;
|
|
326
|
+
stroke-dasharray: 8, 6;
|
|
327
|
+
opacity: 0.7;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
.link-backlink {
|
|
331
|
+
stroke: url(#grad-link);
|
|
332
|
+
stroke-width: 3;
|
|
333
|
+
opacity: 0.85;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/* Legend */
|
|
337
|
+
.legend {
|
|
338
|
+
position: fixed;
|
|
339
|
+
bottom: 24px;
|
|
340
|
+
left: 24px;
|
|
341
|
+
background: var(--bg-card);
|
|
342
|
+
backdrop-filter: blur(20px);
|
|
343
|
+
-webkit-backdrop-filter: blur(20px);
|
|
344
|
+
border: 1px solid var(--border);
|
|
345
|
+
border-radius: 16px;
|
|
346
|
+
padding: 20px 24px;
|
|
347
|
+
min-width: 200px;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
.legend-title {
|
|
351
|
+
font-size: 11px;
|
|
352
|
+
font-weight: 700;
|
|
353
|
+
text-transform: uppercase;
|
|
354
|
+
letter-spacing: 0.1em;
|
|
355
|
+
color: var(--accent);
|
|
356
|
+
margin-bottom: 16px;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
.legend-item {
|
|
360
|
+
display: flex;
|
|
361
|
+
align-items: center;
|
|
362
|
+
gap: 14px;
|
|
363
|
+
margin: 10px 0;
|
|
364
|
+
font-size: 13px;
|
|
365
|
+
font-weight: 500;
|
|
366
|
+
color: var(--text-secondary);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
.legend-shape {
|
|
370
|
+
width: 24px;
|
|
371
|
+
height: 24px;
|
|
372
|
+
display: flex;
|
|
373
|
+
align-items: center;
|
|
374
|
+
justify-content: center;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
.legend-rect {
|
|
378
|
+
width: 20px;
|
|
379
|
+
height: 14px;
|
|
380
|
+
border-radius: 4px;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
.legend-circle {
|
|
384
|
+
width: 16px;
|
|
385
|
+
height: 16px;
|
|
386
|
+
border-radius: 50%;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
.legend-line {
|
|
390
|
+
width: 24px;
|
|
391
|
+
height: 3px;
|
|
392
|
+
border-radius: 2px;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
.legend-line-dashed {
|
|
396
|
+
background: repeating-linear-gradient(90deg, rgba(255,255,255,0.2), rgba(255,255,255,0.2) 5px, transparent 5px, transparent 9px);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/* Controls */
|
|
400
|
+
.controls {
|
|
401
|
+
position: fixed;
|
|
402
|
+
bottom: 24px;
|
|
403
|
+
right: 24px;
|
|
404
|
+
display: flex;
|
|
405
|
+
flex-direction: column;
|
|
406
|
+
gap: 10px;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
.control-btn {
|
|
410
|
+
width: 48px;
|
|
411
|
+
height: 48px;
|
|
412
|
+
background: var(--bg-card);
|
|
413
|
+
backdrop-filter: blur(20px);
|
|
414
|
+
-webkit-backdrop-filter: blur(20px);
|
|
415
|
+
border: 1px solid var(--border);
|
|
416
|
+
border-radius: 14px;
|
|
417
|
+
color: var(--text-secondary);
|
|
418
|
+
font-size: 20px;
|
|
419
|
+
font-weight: 500;
|
|
420
|
+
cursor: pointer;
|
|
421
|
+
display: flex;
|
|
422
|
+
align-items: center;
|
|
423
|
+
justify-content: center;
|
|
424
|
+
transition: all 0.25s ease;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
.control-btn:hover {
|
|
428
|
+
background: rgba(34, 211, 238, 0.1);
|
|
429
|
+
border-color: rgba(34, 211, 238, 0.3);
|
|
430
|
+
color: var(--accent);
|
|
431
|
+
transform: scale(1.05);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/* Modal */
|
|
435
|
+
.modal-overlay {
|
|
436
|
+
display: none;
|
|
437
|
+
position: fixed;
|
|
438
|
+
inset: 0;
|
|
439
|
+
background: rgba(3, 3, 8, 0.9);
|
|
440
|
+
backdrop-filter: blur(8px);
|
|
441
|
+
z-index: 200;
|
|
442
|
+
justify-content: center;
|
|
443
|
+
align-items: center;
|
|
444
|
+
padding: 40px;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
.modal-overlay.show { display: flex; }
|
|
448
|
+
|
|
449
|
+
.modal {
|
|
450
|
+
background: var(--bg-card);
|
|
451
|
+
backdrop-filter: blur(20px);
|
|
452
|
+
border: 1px solid var(--border);
|
|
453
|
+
border-radius: 20px;
|
|
454
|
+
max-width: 760px;
|
|
455
|
+
max-height: 85vh;
|
|
456
|
+
width: 100%;
|
|
457
|
+
overflow: hidden;
|
|
458
|
+
display: flex;
|
|
459
|
+
flex-direction: column;
|
|
460
|
+
box-shadow: 0 32px 64px rgba(0, 0, 0, 0.5), 0 0 80px -20px rgba(34, 211, 238, 0.15);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
.modal-header {
|
|
464
|
+
padding: 24px 28px;
|
|
465
|
+
border-bottom: 1px solid var(--border);
|
|
466
|
+
display: flex;
|
|
467
|
+
align-items: flex-start;
|
|
468
|
+
gap: 18px;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
.modal-icon {
|
|
472
|
+
width: 48px;
|
|
473
|
+
height: 48px;
|
|
474
|
+
background: linear-gradient(135deg, var(--amber), #ea580c);
|
|
475
|
+
border-radius: 14px;
|
|
476
|
+
display: flex;
|
|
477
|
+
align-items: center;
|
|
478
|
+
justify-content: center;
|
|
479
|
+
font-size: 22px;
|
|
480
|
+
flex-shrink: 0;
|
|
481
|
+
box-shadow: 0 8px 24px -8px rgba(245, 158, 11, 0.4);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
.modal-info { flex: 1; }
|
|
485
|
+
|
|
486
|
+
.modal-title {
|
|
487
|
+
font-size: 20px;
|
|
488
|
+
font-weight: 700;
|
|
489
|
+
color: var(--text);
|
|
490
|
+
margin-bottom: 6px;
|
|
491
|
+
letter-spacing: -0.01em;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
.modal-path {
|
|
495
|
+
font-size: 13px;
|
|
496
|
+
color: var(--text-muted);
|
|
497
|
+
font-weight: 500;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
.modal-close {
|
|
501
|
+
width: 36px;
|
|
502
|
+
height: 36px;
|
|
503
|
+
background: rgba(255, 255, 255, 0.03);
|
|
504
|
+
border: 1px solid var(--border);
|
|
505
|
+
color: var(--text-muted);
|
|
506
|
+
font-size: 22px;
|
|
507
|
+
cursor: pointer;
|
|
508
|
+
border-radius: 10px;
|
|
509
|
+
display: flex;
|
|
510
|
+
align-items: center;
|
|
511
|
+
justify-content: center;
|
|
512
|
+
transition: all 0.2s;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
.modal-close:hover {
|
|
516
|
+
background: rgba(255, 255, 255, 0.06);
|
|
517
|
+
border-color: var(--border-hover);
|
|
518
|
+
color: var(--text);
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
.modal-content {
|
|
522
|
+
padding: 28px;
|
|
523
|
+
overflow-y: auto;
|
|
524
|
+
line-height: 1.75;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
.modal-content h1 { font-size: 1.7em; margin-bottom: 18px; color: var(--text); font-weight: 700; letter-spacing: -0.02em; }
|
|
528
|
+
.modal-content h2 { font-size: 1.35em; margin: 28px 0 14px; color: var(--text); border-bottom: 1px solid var(--border); padding-bottom: 10px; font-weight: 600; }
|
|
529
|
+
.modal-content h3 { font-size: 1.15em; margin: 22px 0 10px; color: var(--text); font-weight: 600; }
|
|
530
|
+
.modal-content p { margin-bottom: 16px; color: var(--text-secondary); }
|
|
531
|
+
.modal-content strong { color: var(--text); font-weight: 600; }
|
|
532
|
+
.modal-content code { background: rgba(34, 211, 238, 0.1); padding: 3px 10px; border-radius: 8px; color: var(--accent); font-size: 13px; font-family: 'JetBrains Mono', monospace; }
|
|
533
|
+
.modal-content pre { background: rgba(0, 0, 0, 0.3); padding: 18px 20px; border-radius: 14px; overflow-x: auto; margin: 16px 0; border: 1px solid var(--border); }
|
|
534
|
+
.modal-content pre code { background: none; padding: 0; color: var(--text); }
|
|
535
|
+
.modal-content ul, .modal-content ol { margin: 16px 0; padding-left: 26px; color: var(--text-secondary); }
|
|
536
|
+
.modal-content li { margin: 8px 0; }
|
|
537
|
+
.modal-content blockquote { border-left: 3px solid var(--accent-secondary); padding-left: 18px; color: var(--text-muted); margin: 16px 0; font-style: italic; }
|
|
538
|
+
.modal-content a { color: var(--accent); text-decoration: none; border-bottom: 1px solid transparent; transition: border-color 0.2s; }
|
|
539
|
+
.modal-content a:hover { border-bottom-color: var(--accent); }
|
|
540
|
+
</style>
|
|
541
|
+
</head>
|
|
542
|
+
<body>
|
|
543
|
+
<header>
|
|
544
|
+
<div class="logo-wrap">
|
|
545
|
+
<div class="logo-icon">
|
|
546
|
+
<svg viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
547
|
+
<circle cx="16" cy="16" r="14" stroke="url(#header-grad)" stroke-width="2" fill="none"/>
|
|
548
|
+
<circle cx="16" cy="16" r="4" fill="url(#header-grad)"/>
|
|
549
|
+
<circle cx="8" cy="10" r="2.5" fill="url(#header-grad)" opacity="0.8"/>
|
|
550
|
+
<circle cx="24" cy="10" r="2.5" fill="url(#header-grad)" opacity="0.8"/>
|
|
551
|
+
<circle cx="8" cy="22" r="2.5" fill="url(#header-grad)" opacity="0.8"/>
|
|
552
|
+
<circle cx="24" cy="22" r="2.5" fill="url(#header-grad)" opacity="0.8"/>
|
|
553
|
+
<line x1="12.5" y1="14" x2="9.5" y2="11.5" stroke="url(#header-grad)" stroke-width="1.5" opacity="0.6"/>
|
|
554
|
+
<line x1="19.5" y1="14" x2="22.5" y2="11.5" stroke="url(#header-grad)" stroke-width="1.5" opacity="0.6"/>
|
|
555
|
+
<line x1="12.5" y1="18" x2="9.5" y2="20.5" stroke="url(#header-grad)" stroke-width="1.5" opacity="0.6"/>
|
|
556
|
+
<line x1="19.5" y1="18" x2="22.5" y2="20.5" stroke="url(#header-grad)" stroke-width="1.5" opacity="0.6"/>
|
|
557
|
+
<defs>
|
|
558
|
+
<linearGradient id="header-grad" x1="0" y1="0" x2="32" y2="32">
|
|
559
|
+
<stop offset="0%" stop-color="#22d3ee"/>
|
|
560
|
+
<stop offset="100%" stop-color="#a855f7"/>
|
|
561
|
+
</linearGradient>
|
|
562
|
+
</defs>
|
|
563
|
+
</svg>
|
|
564
|
+
</div>
|
|
565
|
+
<span class="logo">Wasibase</span>
|
|
566
|
+
</div>
|
|
567
|
+
<div class="stats" id="stats">Lade...</div>
|
|
568
|
+
<div class="header-actions">
|
|
569
|
+
<button class="btn" onclick="closeGraph()">Schliessen</button>
|
|
570
|
+
</div>
|
|
571
|
+
</header>
|
|
572
|
+
|
|
573
|
+
<svg id="graph">
|
|
574
|
+
<defs>
|
|
575
|
+
<linearGradient id="grad-cyan" x1="0%" y1="0%" x2="100%" y2="100%">
|
|
576
|
+
<stop offset="0%" stop-color="#22d3ee"/>
|
|
577
|
+
<stop offset="100%" stop-color="#0891b2"/>
|
|
578
|
+
</linearGradient>
|
|
579
|
+
<linearGradient id="grad-emerald" x1="0%" y1="0%" x2="100%" y2="100%">
|
|
580
|
+
<stop offset="0%" stop-color="#10b981"/>
|
|
581
|
+
<stop offset="100%" stop-color="#059669"/>
|
|
582
|
+
</linearGradient>
|
|
583
|
+
<linearGradient id="grad-amber" x1="0%" y1="0%" x2="100%" y2="100%">
|
|
584
|
+
<stop offset="0%" stop-color="#f59e0b"/>
|
|
585
|
+
<stop offset="100%" stop-color="#d97706"/>
|
|
586
|
+
</linearGradient>
|
|
587
|
+
<linearGradient id="grad-link" x1="0%" y1="0%" x2="100%" y2="0%">
|
|
588
|
+
<stop offset="0%" stop-color="#22d3ee"/>
|
|
589
|
+
<stop offset="100%" stop-color="#a855f7"/>
|
|
590
|
+
</linearGradient>
|
|
591
|
+
</defs>
|
|
592
|
+
</svg>
|
|
593
|
+
|
|
594
|
+
<div class="legend">
|
|
595
|
+
<div class="legend-title">Legende</div>
|
|
596
|
+
<div class="legend-item">
|
|
597
|
+
<div class="legend-shape"><div class="legend-rect" style="background: var(--accent);"></div></div>
|
|
598
|
+
<span>Oberkategorie</span>
|
|
599
|
+
</div>
|
|
600
|
+
<div class="legend-item">
|
|
601
|
+
<div class="legend-shape"><div class="legend-rect" style="background: var(--emerald);"></div></div>
|
|
602
|
+
<span>Unterkategorie</span>
|
|
603
|
+
</div>
|
|
604
|
+
<div class="legend-item">
|
|
605
|
+
<div class="legend-shape"><div class="legend-circle" style="background: var(--amber);"></div></div>
|
|
606
|
+
<span>Note</span>
|
|
607
|
+
</div>
|
|
608
|
+
<div class="legend-item">
|
|
609
|
+
<div class="legend-shape"><div class="legend-line legend-line-dashed"></div></div>
|
|
610
|
+
<span>Struktur</span>
|
|
611
|
+
</div>
|
|
612
|
+
<div class="legend-item">
|
|
613
|
+
<div class="legend-shape"><div class="legend-line" style="background: var(--gradient-primary);"></div></div>
|
|
614
|
+
<span>Backlink</span>
|
|
615
|
+
</div>
|
|
616
|
+
</div>
|
|
617
|
+
|
|
618
|
+
<div class="controls">
|
|
619
|
+
<button class="control-btn" onclick="zoomIn()" title="Zoom +">+</button>
|
|
620
|
+
<button class="control-btn" onclick="zoomOut()" title="Zoom -">−</button>
|
|
621
|
+
<button class="control-btn" onclick="resetView()" title="Reset">↺</button>
|
|
622
|
+
</div>
|
|
623
|
+
|
|
624
|
+
<div class="modal-overlay" id="modal" onclick="closeModal(event)">
|
|
625
|
+
<div class="modal" onclick="event.stopPropagation()">
|
|
626
|
+
<div class="modal-header">
|
|
627
|
+
<div class="modal-icon">📄</div>
|
|
628
|
+
<div class="modal-info">
|
|
629
|
+
<div class="modal-title" id="modalTitle"></div>
|
|
630
|
+
<div class="modal-path" id="modalPath"></div>
|
|
631
|
+
</div>
|
|
632
|
+
<button class="modal-close" onclick="closeModal()">×</button>
|
|
633
|
+
</div>
|
|
634
|
+
<div class="modal-content" id="modalContent"></div>
|
|
635
|
+
</div>
|
|
636
|
+
</div>
|
|
637
|
+
|
|
638
|
+
<script>
|
|
639
|
+
let simulation, svg, g, zoom;
|
|
640
|
+
let nodes = [], links = [];
|
|
641
|
+
|
|
642
|
+
const CONFIG = {
|
|
643
|
+
nodeSize: {
|
|
644
|
+
ober: { width: 140, height: 44, radius: 10 },
|
|
645
|
+
unter: { width: 120, height: 36, radius: 8 },
|
|
646
|
+
note: { radius: 22 }
|
|
647
|
+
},
|
|
648
|
+
force: {
|
|
649
|
+
linkDistance: { hierarchy: 140, backlink: 200 },
|
|
650
|
+
linkStrength: { hierarchy: 0.7, backlink: 0.2 },
|
|
651
|
+
charge: { ober: -800, unter: -500, note: -400 },
|
|
652
|
+
collision: { ober: 90, unter: 75, note: 50 }
|
|
653
|
+
}
|
|
654
|
+
};
|
|
655
|
+
|
|
656
|
+
async function init() {
|
|
657
|
+
const response = await fetch('/api/graph');
|
|
658
|
+
const data = await response.json();
|
|
659
|
+
nodes = data.nodes;
|
|
660
|
+
links = data.links;
|
|
661
|
+
|
|
662
|
+
updateStats();
|
|
663
|
+
createGraph();
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
function updateStats() {
|
|
667
|
+
const oberCount = nodes.filter(n => n.type === 'oberkategorie').length;
|
|
668
|
+
const unterCount = nodes.filter(n => n.type === 'unterkategorie').length;
|
|
669
|
+
const noteCount = nodes.filter(n => n.type === 'note').length;
|
|
670
|
+
const backlinkCount = links.filter(l => l.type === 'backlink').length;
|
|
671
|
+
|
|
672
|
+
document.getElementById('stats').textContent =
|
|
673
|
+
noteCount + ' Notes in ' + unterCount + ' Kategorien' +
|
|
674
|
+
(backlinkCount > 0 ? ' • ' + backlinkCount + ' Verknüpfungen' : '');
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
function createGraph() {
|
|
678
|
+
const width = window.innerWidth;
|
|
679
|
+
const height = window.innerHeight;
|
|
680
|
+
|
|
681
|
+
svg = d3.select('#graph').attr('width', width).attr('height', height);
|
|
682
|
+
|
|
683
|
+
// Zoom setup
|
|
684
|
+
zoom = d3.zoom()
|
|
685
|
+
.scaleExtent([0.2, 3])
|
|
686
|
+
.on('zoom', (event) => g.attr('transform', event.transform));
|
|
687
|
+
svg.call(zoom);
|
|
688
|
+
|
|
689
|
+
g = svg.append('g');
|
|
690
|
+
|
|
691
|
+
// Force simulation
|
|
692
|
+
simulation = d3.forceSimulation(nodes)
|
|
693
|
+
.force('link', d3.forceLink(links)
|
|
694
|
+
.id(d => d.id)
|
|
695
|
+
.distance(d => CONFIG.force.linkDistance[d.type])
|
|
696
|
+
.strength(d => CONFIG.force.linkStrength[d.type])
|
|
697
|
+
)
|
|
698
|
+
.force('charge', d3.forceManyBody()
|
|
699
|
+
.strength(d => CONFIG.force.charge[d.type.replace('kategorie', '')] || -400)
|
|
700
|
+
)
|
|
701
|
+
.force('center', d3.forceCenter(width / 2, height / 2))
|
|
702
|
+
.force('collision', d3.forceCollide()
|
|
703
|
+
.radius(d => CONFIG.force.collision[d.type.replace('kategorie', '')] || 50)
|
|
704
|
+
)
|
|
705
|
+
.force('x', d3.forceX(width / 2).strength(0.03))
|
|
706
|
+
.force('y', d3.forceY(height / 2).strength(0.03));
|
|
707
|
+
|
|
708
|
+
// Draw links
|
|
709
|
+
const link = g.append('g')
|
|
710
|
+
.attr('class', 'links')
|
|
711
|
+
.selectAll('line')
|
|
712
|
+
.data(links)
|
|
713
|
+
.enter()
|
|
714
|
+
.append('line')
|
|
715
|
+
.attr('class', d => 'link-' + d.type);
|
|
716
|
+
|
|
717
|
+
// Draw nodes
|
|
718
|
+
const node = g.append('g')
|
|
719
|
+
.attr('class', 'nodes')
|
|
720
|
+
.selectAll('g')
|
|
721
|
+
.data(nodes)
|
|
722
|
+
.enter()
|
|
723
|
+
.append('g')
|
|
724
|
+
.attr('class', d => 'node node-' + d.type.replace('kategorie', ''))
|
|
725
|
+
.call(d3.drag()
|
|
726
|
+
.on('start', dragStarted)
|
|
727
|
+
.on('drag', dragged)
|
|
728
|
+
.on('end', dragEnded)
|
|
729
|
+
)
|
|
730
|
+
.on('click', (event, d) => handleClick(d));
|
|
731
|
+
|
|
732
|
+
// Render node shapes
|
|
733
|
+
node.each(function(d) {
|
|
734
|
+
const el = d3.select(this);
|
|
735
|
+
const cfg = CONFIG.nodeSize;
|
|
736
|
+
|
|
737
|
+
if (d.type === 'oberkategorie') {
|
|
738
|
+
el.append('rect')
|
|
739
|
+
.attr('width', cfg.ober.width)
|
|
740
|
+
.attr('height', cfg.ober.height)
|
|
741
|
+
.attr('x', -cfg.ober.width / 2)
|
|
742
|
+
.attr('y', -cfg.ober.height / 2)
|
|
743
|
+
.attr('rx', cfg.ober.radius);
|
|
744
|
+
el.append('text')
|
|
745
|
+
.attr('class', 'node-label')
|
|
746
|
+
.attr('text-anchor', 'middle')
|
|
747
|
+
.attr('dy', 5)
|
|
748
|
+
.attr('fill', 'white')
|
|
749
|
+
.attr('font-size', '13px')
|
|
750
|
+
.attr('font-weight', '600')
|
|
751
|
+
.text(truncate(d.label, 16));
|
|
752
|
+
}
|
|
753
|
+
else if (d.type === 'unterkategorie') {
|
|
754
|
+
el.append('rect')
|
|
755
|
+
.attr('width', cfg.unter.width)
|
|
756
|
+
.attr('height', cfg.unter.height)
|
|
757
|
+
.attr('x', -cfg.unter.width / 2)
|
|
758
|
+
.attr('y', -cfg.unter.height / 2)
|
|
759
|
+
.attr('rx', cfg.unter.radius);
|
|
760
|
+
el.append('text')
|
|
761
|
+
.attr('class', 'node-label')
|
|
762
|
+
.attr('text-anchor', 'middle')
|
|
763
|
+
.attr('dy', 4)
|
|
764
|
+
.attr('fill', 'white')
|
|
765
|
+
.attr('font-size', '12px')
|
|
766
|
+
.attr('font-weight', '500')
|
|
767
|
+
.text(truncate(d.label, 14));
|
|
768
|
+
}
|
|
769
|
+
else {
|
|
770
|
+
el.append('circle')
|
|
771
|
+
.attr('r', cfg.note.radius);
|
|
772
|
+
el.append('text')
|
|
773
|
+
.attr('class', 'node-label')
|
|
774
|
+
.attr('x', cfg.note.radius + 8)
|
|
775
|
+
.attr('dy', 4)
|
|
776
|
+
.attr('fill', '#ccc')
|
|
777
|
+
.attr('font-size', '12px')
|
|
778
|
+
.text(truncate(d.label, 22));
|
|
779
|
+
}
|
|
780
|
+
});
|
|
781
|
+
|
|
782
|
+
// Simulation tick
|
|
783
|
+
simulation.on('tick', () => {
|
|
784
|
+
link
|
|
785
|
+
.attr('x1', d => d.source.x)
|
|
786
|
+
.attr('y1', d => d.source.y)
|
|
787
|
+
.attr('x2', d => d.target.x)
|
|
788
|
+
.attr('y2', d => d.target.y);
|
|
789
|
+
node.attr('transform', d => 'translate(' + d.x + ',' + d.y + ')');
|
|
790
|
+
});
|
|
791
|
+
|
|
792
|
+
// Initial zoom to fit
|
|
793
|
+
setTimeout(fitToScreen, 1200);
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
function truncate(str, len) {
|
|
797
|
+
return str.length > len ? str.substring(0, len - 1) + '…' : str;
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
function fitToScreen() {
|
|
801
|
+
const bounds = g.node().getBBox();
|
|
802
|
+
const width = window.innerWidth;
|
|
803
|
+
const height = window.innerHeight;
|
|
804
|
+
const scale = 0.85 / Math.max(bounds.width / width, bounds.height / height);
|
|
805
|
+
const tx = width / 2 - scale * (bounds.x + bounds.width / 2);
|
|
806
|
+
const ty = height / 2 - scale * (bounds.y + bounds.height / 2);
|
|
807
|
+
svg.transition().duration(750).call(
|
|
808
|
+
zoom.transform,
|
|
809
|
+
d3.zoomIdentity.translate(tx, ty).scale(Math.min(scale, 1.2))
|
|
810
|
+
);
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
function dragStarted(event, d) {
|
|
814
|
+
if (!event.active) simulation.alphaTarget(0.3).restart();
|
|
815
|
+
d.fx = d.x;
|
|
816
|
+
d.fy = d.y;
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
function dragged(event, d) {
|
|
820
|
+
d.fx = event.x;
|
|
821
|
+
d.fy = event.y;
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
function dragEnded(event, d) {
|
|
825
|
+
if (!event.active) simulation.alphaTarget(0);
|
|
826
|
+
d.fx = null;
|
|
827
|
+
d.fy = null;
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
function handleClick(node) {
|
|
831
|
+
if (node.type === 'note') openNote(node);
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
async function openNote(node) {
|
|
835
|
+
const url = '/api/note/' +
|
|
836
|
+
encodeURIComponent(node.oberkategorie) + '/' +
|
|
837
|
+
encodeURIComponent(node.unterkategorie) + '/' +
|
|
838
|
+
encodeURIComponent(node.thema);
|
|
839
|
+
|
|
840
|
+
try {
|
|
841
|
+
const response = await fetch(url);
|
|
842
|
+
const data = await response.json();
|
|
843
|
+
|
|
844
|
+
document.getElementById('modalTitle').textContent = data.thema;
|
|
845
|
+
document.getElementById('modalPath').textContent = data.oberkategorie + ' / ' + data.unterkategorie;
|
|
846
|
+
document.getElementById('modalContent').innerHTML = DOMPurify.sanitize(data.html);
|
|
847
|
+
document.getElementById('modal').classList.add('show');
|
|
848
|
+
} catch (e) {
|
|
849
|
+
console.error('Error loading note:', e);
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
function closeModal(event) {
|
|
854
|
+
if (!event || event.target.id === 'modal') {
|
|
855
|
+
document.getElementById('modal').classList.remove('show');
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
function zoomIn() { svg.transition().duration(300).call(zoom.scaleBy, 1.4); }
|
|
860
|
+
function zoomOut() { svg.transition().duration(300).call(zoom.scaleBy, 0.7); }
|
|
861
|
+
function resetView() { fitToScreen(); }
|
|
862
|
+
|
|
863
|
+
async function closeGraph() {
|
|
864
|
+
await fetch('/close', { method: 'POST' });
|
|
865
|
+
window.close();
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
// Keyboard shortcuts
|
|
869
|
+
document.addEventListener('keydown', (e) => {
|
|
870
|
+
if (e.key === 'Escape') {
|
|
871
|
+
if (document.getElementById('modal').classList.contains('show')) {
|
|
872
|
+
closeModal();
|
|
873
|
+
} else {
|
|
874
|
+
closeGraph();
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
if (e.key === '+' || e.key === '=') zoomIn();
|
|
878
|
+
if (e.key === '-') zoomOut();
|
|
879
|
+
if (e.key === '0') resetView();
|
|
880
|
+
});
|
|
881
|
+
|
|
882
|
+
// Resize handler
|
|
883
|
+
window.addEventListener('resize', () => {
|
|
884
|
+
const width = window.innerWidth;
|
|
885
|
+
const height = window.innerHeight;
|
|
886
|
+
svg.attr('width', width).attr('height', height);
|
|
887
|
+
simulation.force('center', d3.forceCenter(width / 2, height / 2));
|
|
888
|
+
simulation.force('x', d3.forceX(width / 2).strength(0.03));
|
|
889
|
+
simulation.force('y', d3.forceY(height / 2).strength(0.03));
|
|
890
|
+
simulation.alpha(0.3).restart();
|
|
891
|
+
});
|
|
892
|
+
|
|
893
|
+
init();
|
|
894
|
+
<\/script>
|
|
895
|
+
</body>
|
|
896
|
+
</html>`;
|
|
897
|
+
}
|