Chris4K commited on
Commit
f4b47e7
Β·
verified Β·
1 Parent(s): f390f79

Upload 3 files

Browse files
Files changed (3) hide show
  1. Dockerfile +10 -0
  2. main.py +1353 -0
  3. requirements.txt +3 -0
Dockerfile ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.11-slim
2
+ WORKDIR /app
3
+ RUN useradd -m -u 1000 user
4
+ COPY requirements.txt .
5
+ RUN pip install --no-cache-dir -r requirements.txt
6
+ COPY . .
7
+ RUN mkdir -p store && chown -R user:user /app
8
+ USER user
9
+ EXPOSE 7860
10
+ CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7860"]
main.py ADDED
@@ -0,0 +1,1353 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ KNOWLEDGE STORE β€” Multi-Container Persistent Knowledge Base
3
+ Docker SDK / FastAPI β€” no Gradio, no CSP
4
+
5
+ Containers & their knowledge decay models:
6
+ medical β€” fast decay (outdated = dangerous). Half-life 180 days.
7
+ legal β€” slow decay (laws change rarely). Half-life 730 days.
8
+ company β€” mixed: SOPs stable (HL 365), market/people data volatile (HL 30).
9
+ research β€” citation boost on create, then slow decay. HL 540 days.
10
+ tech β€” very fast decay (versions). HL 90 days.
11
+ prompts β€” no decay (prompts are reusable).
12
+ history β€” ANTI-decay: value increases with age.
13
+ personal β€” moderate decay (preferences drift). HL 180 days.
14
+ finance β€” extreme decay (market data). HL 7 days.
15
+ operations β€” moderate. HL 180 days.
16
+
17
+ Knowledge Value Score = base_importance * time_factor(container) * access_bonus
18
+ Time factor varies per container and uses exponential decay / growth.
19
+
20
+ Search types:
21
+ keyword β€” simple full-text (TF-IDF-like scoring)
22
+ time β€” recency or historical filter
23
+ tag β€” exact/prefix tag match
24
+ container β€” container-scoped list
25
+ semantic β€” keyword with cosine-like tf scoring (no embeddings, pure Python)
26
+ value β€” sorted by current knowledge value score
27
+
28
+ MCP tools: ks_write, ks_read, ks_search, ks_list, ks_delete,
29
+ ks_containers, ks_stats, ks_top_value
30
+ """
31
+ import os, uuid, json, math, time, re, asyncio
32
+ from pathlib import Path
33
+ from datetime import datetime, timezone
34
+ from typing import Optional, List
35
+ from collections import defaultdict, Counter
36
+
37
+ from fastapi import FastAPI, HTTPException, Request
38
+ from fastapi.responses import JSONResponse, HTMLResponse, StreamingResponse
39
+
40
+ BASE = Path(__file__).parent
41
+ STORE = BASE / "store"
42
+ STORE.mkdir(exist_ok=True)
43
+
44
+ # ── Container definitions ─────────────────────────────────────────
45
+
46
+ CONTAINERS = {
47
+ "medical": {
48
+ "label": "Medical",
49
+ "icon": "⚕", # caduceus-ish
50
+ "color": "#ef4444",
51
+ "description": "Clinical guidelines, drug refs, protocols, case notes",
52
+ "decay_model": "exponential",
53
+ "half_life_days": 180,
54
+ "warn_after_days": 90,
55
+ "folders": ["guidelines", "drugs", "protocols", "cases", "research"],
56
+ "note": "Outdated medical info can be dangerous. Review regularly.",
57
+ "badge": "CRITICAL-DECAY",
58
+ },
59
+ "legal": {
60
+ "label": "Legal",
61
+ "icon": "⚖",
62
+ "color": "#8b5cf6",
63
+ "description": "Contracts, regulations, compliance, case law, GDPR",
64
+ "decay_model": "slow_exponential",
65
+ "half_life_days": 730,
66
+ "warn_after_days": 365,
67
+ "folders": ["contracts", "regulations", "gdpr", "caselaw", "templates"],
68
+ "note": "Laws change slowly but verify jurisdiction and amendment dates.",
69
+ "badge": "SLOW-DECAY",
70
+ },
71
+ "company": {
72
+ "label": "Company",
73
+ "icon": "🏢",
74
+ "color": "#0ea5e9",
75
+ "description": "SOPs, org charts, projects, market intel, people",
76
+ "decay_model": "tiered", # folder-dependent
77
+ "half_life_days": 180,
78
+ "warn_after_days": 90,
79
+ "folders": ["sop", "projects", "people", "market", "strategy"],
80
+ "folder_half_lives": {"sop":365, "projects":90, "people":60, "market":14, "strategy":180},
81
+ "note": "Market and people data decay fast. SOPs are more stable.",
82
+ "badge": "TIERED-DECAY",
83
+ },
84
+ "research": {
85
+ "label": "Research",
86
+ "icon": "🔬",
87
+ "color": "#06b6d4",
88
+ "description": "Papers, experiments, hypotheses, datasets, findings",
89
+ "decay_model": "citation_curve", # peaks at 30 days then slow decay
90
+ "half_life_days": 540,
91
+ "peak_days": 30,
92
+ "warn_after_days": 365,
93
+ "folders": ["papers", "experiments", "datasets", "hypotheses", "notes"],
94
+ "note": "New research has highest relevance. Classic papers retain value.",
95
+ "badge": "CITATION-CURVE",
96
+ },
97
+ "tech": {
98
+ "label": "Tech / Docs",
99
+ "icon": "💻",
100
+ "color": "#22d3ee",
101
+ "description": "API docs, code snippets, architecture, DevOps, configs",
102
+ "decay_model": "versioned_decay",
103
+ "half_life_days": 90,
104
+ "warn_after_days": 45,
105
+ "folders": ["api", "snippets", "architecture", "devops", "configs"],
106
+ "note": "Software versions change fast. Tag with version numbers.",
107
+ "badge": "FAST-DECAY",
108
+ },
109
+ "prompts": {
110
+ "label": "Prompts",
111
+ "icon": "⚡",
112
+ "color": "#f59e0b",
113
+ "description": "LLM prompts, system instructions, few-shot examples, chains",
114
+ "decay_model": "stable", # no decay
115
+ "half_life_days": None,
116
+ "warn_after_days": None,
117
+ "folders": ["system", "chains", "fewshot", "templates", "experiments"],
118
+ "note": "Prompts are reusable. Value does not decay.",
119
+ "badge": "STABLE",
120
+ },
121
+ "history": {
122
+ "label": "History / Archive",
123
+ "icon": "🕮",
124
+ "color": "#d97706",
125
+ "description": "Historical records, past decisions, retrospectives, logs",
126
+ "decay_model": "anti_decay", # increases in value with age
127
+ "half_life_days": None,
128
+ "warn_after_days": None,
129
+ "folders": ["decisions", "retrospectives", "logs", "milestones", "archive"],
130
+ "note": "Historical context becomes MORE valuable over time.",
131
+ "badge": "ANTI-DECAY",
132
+ },
133
+ "personal": {
134
+ "label": "Personal",
135
+ "icon": "👤",
136
+ "color": "#ec4899",
137
+ "description": "Goals, notes, preferences, journals, ideas",
138
+ "decay_model": "drift_decay",
139
+ "half_life_days": 180,
140
+ "warn_after_days": 120,
141
+ "folders": ["goals", "notes", "ideas", "journal", "preferences"],
142
+ "note": "Preferences and goals drift over time. Review periodically.",
143
+ "badge": "DRIFT-DECAY",
144
+ },
145
+ "finance": {
146
+ "label": "Finance",
147
+ "icon": "📈",
148
+ "color": "#10b981",
149
+ "description": "Market data, reports, forecasts, invoices, budgets",
150
+ "decay_model": "extreme_decay",
151
+ "half_life_days": 7,
152
+ "warn_after_days": 3,
153
+ "folders": ["market", "reports", "forecasts", "invoices", "budgets"],
154
+ "note": "Market data decays within hours. Financial reports within weeks.",
155
+ "badge": "EXTREME-DECAY",
156
+ },
157
+ "operations": {
158
+ "label": "Operations",
159
+ "icon": "⚙",
160
+ "color": "#84cc16",
161
+ "description": "Runbooks, incidents, on-call, monitoring, deployments",
162
+ "decay_model": "operational_decay",
163
+ "half_life_days": 180,
164
+ "warn_after_days": 60,
165
+ "folders": ["runbooks", "incidents", "oncall", "monitoring", "deployments"],
166
+ "note": "Runbooks age fast in fast-moving infra. Keep versioned.",
167
+ "badge": "MODERATE-DECAY",
168
+ },
169
+ }
170
+
171
+ # ── Knowledge value scoring ───────────────────────────────────────
172
+
173
+ def knowledge_value(doc: dict) -> float:
174
+ """Compute 0-100 current value score for a document."""
175
+ container = doc.get("container", "tech")
176
+ cfg = CONTAINERS.get(container, CONTAINERS["tech"])
177
+ base = float(doc.get("importance", 5)) / 10.0 # 0..1
178
+ access_bonus = min(1.0, math.log1p(doc.get("access_count", 0)) / 10)
179
+ age_days = (time.time() - doc.get("created_at", time.time())) / 86400
180
+
181
+ model = cfg.get("decay_model", "exponential")
182
+ hl = cfg.get("half_life_days") or 365
183
+
184
+ if model == "stable":
185
+ t_factor = 1.0
186
+ elif model == "anti_decay":
187
+ # value grows: tanh curve from 0 to 1 over ~2 years
188
+ t_factor = 0.5 + 0.5 * math.tanh(age_days / 365)
189
+ elif model == "citation_curve":
190
+ peak = cfg.get("peak_days", 30)
191
+ if age_days <= peak:
192
+ t_factor = 0.6 + 0.4 * (age_days / peak)
193
+ else:
194
+ t_factor = math.exp(-math.log(2) * (age_days - peak) / hl)
195
+ elif model == "tiered":
196
+ folder = doc.get("folder", "")
197
+ folder_hl = cfg.get("folder_half_lives", {}).get(folder, hl)
198
+ t_factor = math.exp(-math.log(2) * age_days / folder_hl)
199
+ elif model == "extreme_decay":
200
+ t_factor = math.exp(-math.log(2) * age_days / max(1, hl))
201
+ else:
202
+ # standard exponential decay
203
+ t_factor = math.exp(-math.log(2) * age_days / hl)
204
+
205
+ t_factor = max(0.0, min(1.0, t_factor))
206
+ score = (base * 0.5 + access_bonus * 0.1 + t_factor * 0.4) * 100
207
+ return round(score, 1)
208
+
209
+ def freshness_label(doc: dict) -> str:
210
+ container = doc.get("container", "tech")
211
+ cfg = CONTAINERS.get(container, {})
212
+ warn = cfg.get("warn_after_days")
213
+ model = cfg.get("decay_model", "exponential")
214
+ age_days = (time.time() - doc.get("created_at", time.time())) / 86400
215
+ if model == "stable": return "STABLE"
216
+ if model == "anti_decay": return "ARCHIVAL"
217
+ if not warn: return "OK"
218
+ if age_days > warn * 2: return "STALE"
219
+ if age_days > warn: return "AGING"
220
+ return "FRESH"
221
+
222
+ # ── Storage utils ─────────────────────────────────────────────────
223
+
224
+ def now_ts(): return int(time.time())
225
+
226
+ def doc_path(container: str, folder: str, did: str) -> Path:
227
+ d = STORE / container / folder
228
+ d.mkdir(parents=True, exist_ok=True)
229
+ return d / f"{did}.json"
230
+
231
+ def read_doc(container: str, folder: str, did: str) -> Optional[dict]:
232
+ p = doc_path(container, folder, did)
233
+ return json.loads(p.read_text()) if p.exists() else None
234
+
235
+ def write_doc(doc: dict):
236
+ doc["updated_at"] = now_ts()
237
+ doc_path(doc["container"], doc["folder"], doc["id"]).write_text(
238
+ json.dumps(doc, indent=2, ensure_ascii=False)
239
+ )
240
+
241
+ def all_docs(container: str = "", folder: str = "", limit: int = 500) -> List[dict]:
242
+ out = []
243
+ base = STORE / container if container else STORE
244
+ for p in sorted(base.rglob("*.json"), reverse=True):
245
+ try:
246
+ d = json.loads(p.read_text())
247
+ if folder and d.get("folder") != folder: continue
248
+ out.append(d)
249
+ except: pass
250
+ if len(out) >= limit: break
251
+ return out
252
+
253
+ def new_doc(data: dict) -> dict:
254
+ did = uuid.uuid4().hex[:10]
255
+ container = data.get("container", "tech")
256
+ cfg = CONTAINERS.get(container, {})
257
+ folders = cfg.get("folders", ["general"])
258
+ folder = data.get("folder", folders[0] if folders else "general")
259
+ doc = {
260
+ "id": did,
261
+ "container": container,
262
+ "folder": folder,
263
+ "title": (data.get("title") or "Untitled").strip(),
264
+ "body": (data.get("body") or data.get("content") or "").strip(),
265
+ "summary": (data.get("summary") or "").strip(),
266
+ "tags": [t.strip().lower() for t in data.get("tags", []) if str(t).strip()],
267
+ "importance": max(0, min(10, int(data.get("importance", 5)))),
268
+ "author": (data.get("author") or "").strip(),
269
+ "source": (data.get("source") or "").strip(),
270
+ "version": (data.get("version") or "").strip(),
271
+ "expires_hint": data.get("expires_hint"), # ISO date string, optional
272
+ "links": data.get("links", []), # related doc IDs
273
+ "metadata": data.get("metadata", {}),
274
+ "access_count": 0,
275
+ "created_at": now_ts(),
276
+ "updated_at": now_ts(),
277
+ "last_accessed": None,
278
+ }
279
+ write_doc(doc)
280
+ return doc
281
+
282
+ # ── Search engine ─────────────────────────────────────────────────
283
+
284
+ def tokenize(text: str) -> List[str]:
285
+ return re.findall(r"[a-zA-Z0-9\u00C0-\u024F]+", text.lower())
286
+
287
+ def tf_score(query_tokens: List[str], doc: dict) -> float:
288
+ text = " ".join([doc.get("title",""), doc.get("body",""),
289
+ doc.get("summary",""), " ".join(doc.get("tags",[]))]).lower()
290
+ doc_tokens = tokenize(text)
291
+ tf = Counter(doc_tokens)
292
+ total = len(doc_tokens) or 1
293
+ score = sum(tf.get(t, 0) / total for t in query_tokens)
294
+ # boost: title matches worth 3x
295
+ title_tokens = tokenize(doc.get("title","").lower())
296
+ title_tf = Counter(title_tokens)
297
+ score += sum(title_tf.get(t, 0) * 2 for t in query_tokens)
298
+ return score
299
+
300
+ def search_docs(query: str = "", container: str = "", folder: str = "",
301
+ tag: str = "", author: str = "", sort_by: str = "relevance",
302
+ freshness: str = "", limit: int = 20) -> List[dict]:
303
+ docs = all_docs(container, folder, 500)
304
+ query_tokens = tokenize(query) if query else []
305
+
306
+ results = []
307
+ for doc in docs:
308
+ # Tag filter
309
+ if tag and tag.lower() not in doc.get("tags", []): continue
310
+ # Author filter
311
+ if author and doc.get("author","").lower() != author.lower(): continue
312
+ # Freshness filter
313
+ if freshness:
314
+ fl = freshness_label(doc)
315
+ if freshness == "fresh" and fl != "FRESH": continue
316
+ if freshness == "stale" and fl not in ("STALE","AGING"): continue
317
+
318
+ score = tf_score(query_tokens, doc) if query_tokens else 1.0
319
+ if query_tokens and score == 0: continue
320
+
321
+ results.append((score, doc))
322
+
323
+ # Sort
324
+ if sort_by == "value":
325
+ results.sort(key=lambda x: -knowledge_value(x[1]))
326
+ elif sort_by == "newest":
327
+ results.sort(key=lambda x: -x[1].get("created_at", 0))
328
+ elif sort_by == "oldest":
329
+ results.sort(key=lambda x: x[1].get("created_at", 0))
330
+ elif sort_by == "importance":
331
+ results.sort(key=lambda x: (-x[1].get("importance", 5), -x[0]))
332
+ else:
333
+ results.sort(key=lambda x: (-x[0], -knowledge_value(x[1])))
334
+
335
+ return [d for _, d in results[:limit]]
336
+
337
+ # ── Seed data ─────────────────────────────────────────────────────
338
+
339
+ def seed():
340
+ if any(STORE.rglob("*.json")): return
341
+ seeds = [
342
+ # TECH
343
+ {"container":"tech","folder":"architecture","title":"ki-fusion-labs.de GPU Worker Architecture",
344
+ "body":"GPU workers use a polling architecture. Workers call GET /api/queue every 2 seconds to check for pending jobs. On job acquisition, worker POSTs result to /api/results/{job_id}. No inbound connections required β€” fully firewall-friendly. LM Studio listens on localhost:1234. Jobs include: model_id, prompt, max_tokens, temperature, stream flag.",
345
+ "summary":"Firewall-friendly polling design for GPU inference workers",
346
+ "tags":["ki-fusion-labs","gpu","architecture","llm","inference"],"importance":9,"author":"christof","version":"v2"},
347
+ {"container":"tech","folder":"api","title":"FORGE Skill Registry API Reference",
348
+ "body":"POST /api/v1/skills β€” create skill\nGET /api/v1/skills β€” list (filter: ?category=&tag=)\nGET /api/v1/skills/{id} β€” get\nPATCH /api/v1/skills/{id} β€” update\nDELETE /api/v1/skills/{id} β€” delete\nGET /mcp/sse β€” MCP SSE stream\nPOST /mcp β€” MCP JSON-RPC\n\nSkill schema: {id, name, description, category, code, input_schema, output_schema, tags, version, author}",
349
+ "summary":"FORGE MCP skill registry REST endpoints","tags":["forge","api","mcp","skills"],"importance":8,"author":"christof","version":"1.0"},
350
+ {"container":"tech","folder":"devops","title":"HF Spaces Docker SDK Deployment Guide",
351
+ "body":"CRITICAL: Use sdk: docker in README.md, NOT sdk: gradio.\nGradio SDK CSP blocks ALL <script> tags inside gr.HTML() and all iframes (frame-src: none).\nDockerfile pattern:\n FROM python:3.11-slim\n RUN useradd -m -u 1000 user\n USER user\n EXPOSE 7860\n CMD [\"uvicorn\", \"main:app\", \"--host\", \"0.0.0.0\", \"--port\", \"7860\"]\nNo Gradio dependency. Pure FastAPI serves HTML as string. No StaticFiles.\nSurrogate chars: never use \\uD83D in Python strings β€” use HTML entities &#128196;",
352
+ "summary":"How to deploy FastAPI on HF Spaces without CSP issues","tags":["hf-spaces","docker","fastapi","deployment"],"importance":10,"author":"christof"},
353
+ # LEGAL
354
+ {"container":"legal","folder":"gdpr","title":"bofrost* GDPR Deletion Architecture β€” Concept v3",
355
+ "body":"Decentralized pull-based deletion across 14+ systems in 6 countries (DE, AT, CH, IT, FR, NL).\nCore flow: DPO triggers deletion request -> orchestrator creates deletion ticket -> each system polls /api/deletions/pending -> system processes and POSTs Proof of Deletion (PoD) certificate -> orchestrator tracks completion.\nPoD schema: {request_id, system_id, subject_id, deleted_fields[], timestamp, checksum, processor_name}.\nArchitecture Board sign-off required. DPO must countersign each PoD batch.",
356
+ "summary":"Pull-based GDPR deletion design for bofrost* 14-system landscape","tags":["gdpr","bofrost","deletion","architecture","pod"],"importance":10,"author":"christof","version":"v3"},
357
+ {"container":"legal","folder":"regulations","title":"GDPR Article 17 β€” Right to Erasure (Key Points)",
358
+ "body":"Art. 17 GDPR: Data subject has right to erasure without undue delay when: (a) no longer necessary for original purpose, (b) consent withdrawn, (c) data subject objects under Art. 21, (d) unlawful processing, (e) legal obligation.\nExceptions: freedom of expression, legal obligation, public interest (Art. 17(3)).\nTimeline: respond within 1 month (extendable 2 months for complex cases).\nLogging: document all erasure requests and outcomes.",
359
+ "summary":"GDPR Art. 17 erasure right summary","tags":["gdpr","erasure","regulation","art17"],"importance":9,"author":"christof"},
360
+ # MEDICAL
361
+ {"container":"medical","folder":"protocols","title":"Burnout Prevention Protocol β€” Knowledge Worker",
362
+ "body":"Early indicators: decision fatigue after <2h deep work, >3 context switches/hour, sleep quality drop, emotional blunting.\nInterventions: (1) 90-min deep work blocks, no interruptions. (2) Hard stop at 18:00. (3) Single daily priority written before 09:00. (4) Weekly 30-min review: energy vs output. (5) Physical activity 3x/week minimum.\nEscalation: if indicators persist 3+ weeks, consult occupational health.\nFor AI project leaders: especially watch for 'always on' patterns with LLM tools.",
363
+ "summary":"Burnout prevention for knowledge workers managing AI projects","tags":["burnout","health","productivity","mental-health"],"importance":8,"author":"christof"},
364
+ # RESEARCH
365
+ {"container":"research","folder":"experiments","title":"BitNet 1.58-bit Trainer β€” Stability Fixes Log",
366
+ "body":"Problem history:\n- NaN loss: fixed with gradient clipping (max_norm=1.0) + LR warmup (500 steps)\n- Dead layers: fixed with initialization scale 0.02 instead of default\n- FlipRate spike: STE gradient scaling tuned to 0.3 β€” above 0.5 causes oscillation\n- Dataset distribution mismatch: balanced sampling per domain required\n- Quantization death spiral: add 1e-8 epsilon to weight norm denominator\n\nFinal config: LR=2e-4, warmup=500, clip=1.0, batch=32, accumulation=4\nHardware: RTX 5090 24GB VRAM, bfloat16",
367
+ "summary":"BitNet stable training recipe after systematic debugging","tags":["bitnet","training","rtx5090","quantization","stability"],"importance":9,"author":"christof"},
368
+ {"container":"research","folder":"hypotheses","title":"JARVIS TurnClassifier β€” Ambiguous Intent Routing",
369
+ "body":"Hypothesis: DST context window of 5 turns is insufficient for long multi-domain conversations.\nProposed fix: sliding window with semantic anchor β€” if slot confidence < 0.6, expand window to 10 turns and inject last confirmed intent as prior.\nTesting plan: 200 synthetic conversations, 3 ambiguity categories: topic-switch, implicit-reference, negation.\nExpected: +12% routing accuracy, +8ms latency overhead acceptable.",
370
+ "summary":"Hypothesis for improving JARVIS intent routing with adaptive DST window","tags":["jarvis","dst","nlp","routing","hypothesis"],"importance":8,"author":"christof"},
371
+ # PROMPTS
372
+ {"container":"prompts","folder":"system","title":"JARVIS TheCore System Prompt v3",
373
+ "body":"You are JARVIS, an advanced AI assistant operating within TheCore architecture. You have access to: (1) multi-tier memory system, (2) FORGE skill registry, (3) DISPATCH task board, (4) RELAY message bus, (5) this Knowledge Store.\n\nRouting rules:\n- Simple factual queries -> direct answer\n- Tasks requiring external data -> capability routing to appropriate tool\n- Ambiguous intent (confidence < 0.7) -> clarification before action\n- Urgent flags -> DISPATCH high-priority queue\n\nTone: precise, concise, no filler. Always show reasoning for non-trivial decisions.",
374
+ "summary":"Core system prompt for JARVIS TheCore v3","tags":["jarvis","system-prompt","thecore","routing"],"importance":10,"author":"christof","version":"v3"},
375
+ # COMPANY
376
+ {"container":"company","folder":"projects","title":"ki-fusion-labs.de β€” Active Project Status",
377
+ "body":"Status: ACTIVE development\nStack: PHP frontend + Python FastAPI backend + LM Studio local inference\nGPU: RTX 5090, CUDA 12.4\nActive components: LLM API queue, worker polling, result streaming\nRecent: SSL cert renewed (Let's Encrypt, 90-day auto-renew configured)\nPending: persistent queue (survive restarts), rate limiting per API key, usage dashboard\nHF Spaces deployed: FORGE, DISPATCH, RELAY, MEMORY, KNOWLEDGE",
378
+ "summary":"Current status of ki-fusion-labs.de platform","tags":["ki-fusion-labs","status","projects"],"importance":9,"author":"christof"},
379
+ # FINANCE
380
+ {"container":"finance","folder":"budgets","title":"AI Infrastructure Cost Baseline β€” 2026",
381
+ "body":"Monthly recurring:\n- HF Spaces (free tier): 0 EUR\n- OpenRouter free models: 0 EUR\n- Oracle Cloud Always Free: 0 EUR\n- Domain ki-fusion-labs.de: ~1.50 EUR/mo\n- Electricity RTX 5090 training (est. 10h/mo @ 400W): ~0.80 EUR\nTotal infra: ~2.30 EUR/month\n\nNote: HF Spaces may incur costs if Spaces upgraded to GPU. Budget 20 EUR/mo buffer.",
382
+ "summary":"AI infra cost breakdown β€” essentially free tier stack","tags":["budget","infrastructure","costs","2026"],"importance":6,"author":"christof"},
383
+ # OPERATIONS
384
+ {"container":"operations","folder":"runbooks","title":"HF Space Recovery Runbook",
385
+ "body":"When a Space goes red:\n1. Check logs: Space Settings -> Logs\n2. Common errors:\n - UnicodeEncodeError: surrogate chars in SPA string -> use HTML entities\n - ModuleNotFoundError: check requirements.txt, rebuild\n - Port error: ensure CMD uses --port 7860\n - Permission denied: ensure USER user in Dockerfile + chown\n3. Force rebuild: Settings -> Factory reset (loses state!)\n4. For persistent data: use HF Datasets API, not local filesystem\n5. SDK confusion: gradio SDK = CSP nightmare. Always use sdk: docker",
386
+ "summary":"Step-by-step HF Space debugging and recovery","tags":["hf-spaces","runbook","debugging","recovery"],"importance":10,"author":"christof"},
387
+ # HISTORY
388
+ {"container":"history","folder":"decisions","title":"ADR-001: Why Docker SDK over Gradio SDK",
389
+ "body":"Date: 2026-03\nContext: Building agent UI tools on HuggingFace Spaces.\nDecision: Use sdk: docker for all custom web UIs.\nRationale: Gradio SDK injects CSP headers that block ALL <script> tags in gr.HTML() components. Frame-src: none also blocks iframes. No workaround exists via custom_headers in README.\nConsequences: Pure FastAPI, HTML served as string, no Gradio dependency, full CSP control.\nStatus: ACCEPTED. Applied to FORGE, DISPATCH, RELAY, MEMORY, KNOWLEDGE.",
390
+ "summary":"Architecture decision: Docker over Gradio for HF Spaces UIs","tags":["adr","architecture","hf-spaces","docker","decision"],"importance":10,"author":"christof"},
391
+ ]
392
+ for s in seeds:
393
+ new_doc(s)
394
+
395
+ seed()
396
+
397
+ # ── FastAPI ───────────────────────────────────────────────────────
398
+
399
+ app = FastAPI(title="Knowledge Store")
400
+
401
+ def jresp(data, status=200): return JSONResponse(content=data, status_code=status)
402
+
403
+ @app.get("/api/containers")
404
+ async def get_containers():
405
+ result = {}
406
+ for k, v in CONTAINERS.items():
407
+ docs = all_docs(k, limit=500)
408
+ result[k] = {**v, "count": len(docs),
409
+ "avg_value": round(sum(knowledge_value(d) for d in docs)/len(docs), 1) if docs else 0}
410
+ return jresp(result)
411
+
412
+ @app.get("/api/docs")
413
+ async def list_docs(container: str = "", folder: str = "", tag: str = "",
414
+ author: str = "", sort: str = "newest", limit: int = 100):
415
+ docs = search_docs(container=container, folder=folder, tag=tag,
416
+ author=author, sort_by=sort, limit=limit)
417
+ for d in docs:
418
+ d["_value"] = knowledge_value(d)
419
+ d["_freshness"] = freshness_label(d)
420
+ return jresp(docs)
421
+
422
+ @app.get("/api/docs/search")
423
+ async def search(q: str = "", container: str = "", folder: str = "", tag: str = "",
424
+ sort: str = "relevance", freshness: str = "", limit: int = 20):
425
+ docs = search_docs(q, container, folder, tag, sort_by=sort, freshness=freshness, limit=limit)
426
+ for d in docs:
427
+ d["_value"] = knowledge_value(d)
428
+ d["_freshness"] = freshness_label(d)
429
+ return jresp(docs)
430
+
431
+ @app.get("/api/docs/top-value")
432
+ async def top_value(container: str = "", limit: int = 20):
433
+ docs = all_docs(container, limit=500)
434
+ scored = sorted(docs, key=lambda d: -knowledge_value(d))
435
+ for d in scored[:limit]:
436
+ d["_value"] = knowledge_value(d)
437
+ d["_freshness"] = freshness_label(d)
438
+ return jresp(scored[:limit])
439
+
440
+ @app.get("/api/docs/{container}/{folder}/{did}")
441
+ async def get_doc(container: str, folder: str, did: str):
442
+ d = read_doc(container, folder, did)
443
+ if not d: raise HTTPException(404)
444
+ d["access_count"] = d.get("access_count", 0) + 1
445
+ d["last_accessed"] = now_ts()
446
+ write_doc(d)
447
+ d["_value"] = knowledge_value(d)
448
+ d["_freshness"] = freshness_label(d)
449
+ return jresp(d)
450
+
451
+ @app.post("/api/docs")
452
+ async def create_doc(request: Request):
453
+ data = await request.json()
454
+ if not data.get("title","").strip(): raise HTTPException(400, "title required")
455
+ if not data.get("body","").strip() and not data.get("content","").strip():
456
+ raise HTTPException(400, "body required")
457
+ d = new_doc(data)
458
+ d["_value"] = knowledge_value(d)
459
+ d["_freshness"] = freshness_label(d)
460
+ return jresp({"status":"created","id":d["id"],"doc":d}, 201)
461
+
462
+ @app.patch("/api/docs/{container}/{folder}/{did}")
463
+ async def update_doc(container: str, folder: str, did: str, request: Request):
464
+ d = read_doc(container, folder, did)
465
+ if not d: raise HTTPException(404)
466
+ data = await request.json()
467
+ for k in ("title","body","summary","tags","importance","author","source","version","links","metadata"):
468
+ if k in data: d[k] = data[k]
469
+ write_doc(d)
470
+ d["_value"] = knowledge_value(d)
471
+ d["_freshness"] = freshness_label(d)
472
+ return jresp({"status":"updated","doc":d})
473
+
474
+ @app.delete("/api/docs/{container}/{folder}/{did}")
475
+ async def delete_doc(container: str, folder: str, did: str):
476
+ p = doc_path(container, folder, did)
477
+ if not p.exists(): raise HTTPException(404)
478
+ p.unlink()
479
+ return jresp({"status":"deleted"})
480
+
481
+ @app.get("/api/stats")
482
+ async def stats():
483
+ all_d = all_docs(limit=2000)
484
+ by_container: dict = {}
485
+ stale_count = 0
486
+ total_value = 0.0
487
+ by_freshness: dict = {"FRESH":0,"AGING":0,"STALE":0,"STABLE":0,"ARCHIVAL":0}
488
+ for d in all_d:
489
+ c = d.get("container","?")
490
+ by_container[c] = by_container.get(c,0) + 1
491
+ v = knowledge_value(d)
492
+ total_value += v
493
+ fl = freshness_label(d)
494
+ by_freshness[fl] = by_freshness.get(fl,0) + 1
495
+ if fl == "STALE": stale_count += 1
496
+ return jresp({
497
+ "total": len(all_d),
498
+ "by_container": by_container,
499
+ "by_freshness": by_freshness,
500
+ "stale_count": stale_count,
501
+ "avg_value": round(total_value/len(all_d), 1) if all_d else 0,
502
+ })
503
+
504
+ # ── MCP ───────────────────────────────────────────────────────────
505
+
506
+ MCP_TOOLS = [
507
+ {"name":"ks_write","description":"Write a knowledge document to a container/folder",
508
+ "inputSchema":{"type":"object","required":["container","title","body"],"properties":{
509
+ "container": {"type":"string","enum":list(CONTAINERS.keys())},
510
+ "folder": {"type":"string"},
511
+ "title": {"type":"string"},
512
+ "body": {"type":"string"},
513
+ "summary": {"type":"string"},
514
+ "tags": {"type":"array","items":{"type":"string"}},
515
+ "importance": {"type":"integer","minimum":0,"maximum":10},
516
+ "author": {"type":"string"},
517
+ "version": {"type":"string"},
518
+ }}},
519
+ {"name":"ks_read","description":"Read a document by container/folder/id",
520
+ "inputSchema":{"type":"object","required":["container","folder","id"],"properties":{
521
+ "container":{"type":"string"},"folder":{"type":"string"},"id":{"type":"string"}}}},
522
+ {"name":"ks_search","description":"Search knowledge base by query, tag, container, author",
523
+ "inputSchema":{"type":"object","properties":{
524
+ "query": {"type":"string"},
525
+ "container": {"type":"string"},
526
+ "folder": {"type":"string"},
527
+ "tag": {"type":"string"},
528
+ "sort": {"type":"string","enum":["relevance","value","newest","oldest","importance"]},
529
+ "freshness": {"type":"string","enum":["fresh","stale",""]},
530
+ "limit": {"type":"integer","default":10},
531
+ }}},
532
+ {"name":"ks_list","description":"List documents in a container/folder",
533
+ "inputSchema":{"type":"object","properties":{
534
+ "container":{"type":"string"},"folder":{"type":"string"},"limit":{"type":"integer"}}}},
535
+ {"name":"ks_delete","description":"Delete a knowledge document",
536
+ "inputSchema":{"type":"object","required":["container","folder","id"],"properties":{
537
+ "container":{"type":"string"},"folder":{"type":"string"},"id":{"type":"string"}}}},
538
+ {"name":"ks_containers","description":"List all containers with counts and avg value",
539
+ "inputSchema":{"type":"object","properties":{}}},
540
+ {"name":"ks_stats","description":"Overall knowledge base statistics",
541
+ "inputSchema":{"type":"object","properties":{}}},
542
+ {"name":"ks_top_value","description":"Get highest-value documents right now",
543
+ "inputSchema":{"type":"object","properties":{
544
+ "container":{"type":"string"},"limit":{"type":"integer","default":10}}}},
545
+ ]
546
+
547
+ async def mcp_call(name, args):
548
+ if name == "ks_write":
549
+ if not args.get("title") or not args.get("body"):
550
+ return json.dumps({"error":"title and body required"})
551
+ d = new_doc(args)
552
+ d["_value"] = knowledge_value(d)
553
+ return json.dumps({"created":d["id"],"container":d["container"],"folder":d["folder"],"value":d["_value"]})
554
+ if name == "ks_read":
555
+ d = read_doc(args["container"], args["folder"], args["id"])
556
+ if not d: return json.dumps({"error":"not found"})
557
+ d["access_count"] = d.get("access_count",0)+1
558
+ d["last_accessed"] = now_ts()
559
+ write_doc(d)
560
+ d["_value"] = knowledge_value(d); d["_freshness"] = freshness_label(d)
561
+ return json.dumps(d)
562
+ if name == "ks_search":
563
+ docs = search_docs(args.get("query",""), args.get("container",""),
564
+ args.get("folder",""), args.get("tag",""),
565
+ args.get("sort","relevance"), args.get("freshness",""), args.get("limit",10))
566
+ for d in docs: d["_value"]=knowledge_value(d); d["_freshness"]=freshness_label(d)
567
+ return json.dumps({"count":len(docs),"results":docs})
568
+ if name == "ks_list":
569
+ docs = all_docs(args.get("container",""), args.get("folder",""), args.get("limit",20))
570
+ for d in docs: d["_value"]=knowledge_value(d); d["_freshness"]=freshness_label(d)
571
+ return json.dumps({"count":len(docs),"docs":docs})
572
+ if name == "ks_delete":
573
+ p = doc_path(args["container"], args["folder"], args["id"])
574
+ if not p.exists(): return json.dumps({"error":"not found"})
575
+ p.unlink(); return json.dumps({"deleted":args["id"]})
576
+ if name == "ks_containers":
577
+ result = {}
578
+ for k, v in CONTAINERS.items():
579
+ docs = all_docs(k, limit=500)
580
+ result[k] = {"label":v["label"],"count":len(docs),"decay_model":v["decay_model"],
581
+ "badge":v["badge"],"avg_value":round(sum(knowledge_value(d) for d in docs)/len(docs),1) if docs else 0}
582
+ return json.dumps(result)
583
+ if name == "ks_stats":
584
+ all_d = all_docs(limit=2000)
585
+ by_c = {}
586
+ for d in all_d: by_c[d.get("container","?")] = by_c.get(d.get("container","?"),0)+1
587
+ return json.dumps({"total":len(all_d),"by_container":by_c})
588
+ if name == "ks_top_value":
589
+ docs = all_docs(args.get("container",""), limit=500)
590
+ scored = sorted(docs, key=lambda d:-knowledge_value(d))[:args.get("limit",10)]
591
+ for d in scored: d["_value"]=knowledge_value(d); d["_freshness"]=freshness_label(d)
592
+ return json.dumps({"count":len(scored),"docs":scored})
593
+ return json.dumps({"error":f"unknown: {name}"})
594
+
595
+ @app.get("/mcp/sse")
596
+ async def mcp_sse():
597
+ async def stream():
598
+ init = {"jsonrpc":"2.0","method":"notifications/initialized",
599
+ "params":{"serverInfo":{"name":"knowledge-store","version":"1.0"},"capabilities":{"tools":{}}}}
600
+ yield f"data: {json.dumps(init)}\n\n"
601
+ await asyncio.sleep(0.1)
602
+ yield f"data: {json.dumps({'jsonrpc':'2.0','method':'notifications/tools/list_changed','params':{}})}\n\n"
603
+ while True:
604
+ await asyncio.sleep(25)
605
+ yield f"data: {json.dumps({'jsonrpc':'2.0','method':'ping'})}\n\n"
606
+ return StreamingResponse(stream(), media_type="text/event-stream",
607
+ headers={"Cache-Control":"no-cache","X-Accel-Buffering":"no"})
608
+
609
+ @app.post("/mcp")
610
+ async def mcp_rpc(request: Request):
611
+ body = await request.json()
612
+ method = body.get("method",""); rid = body.get("id",1)
613
+ if method == "initialize":
614
+ return jresp({"jsonrpc":"2.0","id":rid,"result":{
615
+ "serverInfo":{"name":"knowledge-store","version":"1.0"},"capabilities":{"tools":{}}}})
616
+ if method == "tools/list":
617
+ return jresp({"jsonrpc":"2.0","id":rid,"result":{"tools":MCP_TOOLS}})
618
+ if method == "tools/call":
619
+ p = body.get("params",{})
620
+ res = await mcp_call(p.get("name",""), p.get("arguments",{}))
621
+ return jresp({"jsonrpc":"2.0","id":rid,"result":{"content":[{"type":"text","text":res}]}})
622
+ return jresp({"jsonrpc":"2.0","id":rid,"error":{"code":-32601,"message":"Method not found"}})
623
+
624
+ # ── SPA ───────────────────────────────────────────────────────────
625
+
626
+ @app.get("/", response_class=HTMLResponse)
627
+ async def ui():
628
+ return HTMLResponse(content=SPA, media_type="text/html; charset=utf-8")
629
+
630
+ SPA = r"""<!DOCTYPE html>
631
+ <html lang="en">
632
+ <head>
633
+ <meta charset="UTF-8">
634
+ <meta name="viewport" content="width=device-width,initial-scale=1">
635
+ <title>KNOWLEDGE STORE</title>
636
+ <link rel="preconnect" href="https://fonts.googleapis.com">
637
+ <link href="https://fonts.googleapis.com/css2?family=Space+Mono:wght@400;700&family=Inter:wght@400;500;600&display=swap" rel="stylesheet">
638
+ <style>
639
+ :root{
640
+ --bg:#08080f;--s1:#0f0f1a;--s2:#141428;--s3:#1a1a35;
641
+ --bd:#1e1e35;--bd2:#282850;--bd3:#323260;
642
+ --acc:#ff6b00;--acc2:#ff9500;--acc3:#ffb347;
643
+ --txt:#d8d8f0;--sub:#5a5a88;--dim:#282850;
644
+ --lo:#2ed573;--cr:#ff2244;--warn:#f59e0b;
645
+ --c-med:#ef4444;--c-leg:#8b5cf6;--c-com:#0ea5e9;
646
+ --c-res:#06b6d4;--c-tec:#22d3ee;--c-pro:#f59e0b;
647
+ --c-his:#d97706;--c-per:#ec4899;--c-fin:#10b981;--c-ops:#84cc16;
648
+ --font:'Space Mono',monospace;--body:'Inter',sans-serif;
649
+ }
650
+ *{box-sizing:border-box;margin:0;padding:0;}
651
+ html,body{height:100%;overflow:hidden;}
652
+ body{font-family:var(--body);background:var(--bg);color:var(--txt);display:flex;flex-direction:column;height:100vh;}
653
+ body::after{content:'';position:fixed;inset:0;pointer-events:none;
654
+ background-image:repeating-linear-gradient(0deg,transparent,transparent 2px,rgba(255,107,0,.004) 2px,rgba(255,107,0,.004) 3px);}
655
+
656
+ /* HEADER */
657
+ #hdr{flex-shrink:0;display:flex;align-items:center;padding:.8rem 1.6rem;gap:1.2rem;
658
+ border-bottom:1px solid var(--bd);background:linear-gradient(180deg,#0c0c1e,var(--bg));z-index:10;}
659
+ #logo{font-family:var(--font);font-size:1.2rem;font-weight:700;letter-spacing:2px;
660
+ background:linear-gradient(90deg,var(--acc),var(--acc3));
661
+ -webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text;}
662
+ #logo-sub{font-family:var(--font);font-size:.5rem;color:var(--sub);letter-spacing:.25em;text-transform:uppercase;margin-top:2px;}
663
+ #hdr-stats{display:flex;gap:.45rem;flex:1;flex-wrap:wrap;}
664
+ .hs{display:flex;align-items:center;gap:.35rem;background:var(--s1);border:1px solid var(--bd);
665
+ border-radius:5px;padding:.22rem .5rem;font-family:var(--font);font-size:.5rem;color:var(--sub);}
666
+ .hs-n{font-size:.82rem;font-weight:700;line-height:1;}
667
+ .freshbadge{font-size:.46rem;padding:1px 5px;border-radius:3px;font-family:var(--font);font-weight:700;letter-spacing:.06em;}
668
+ .fb-FRESH{background:#02130a;color:var(--lo);border:1px solid rgba(46,213,115,.15);}
669
+ .fb-AGING{background:#181400;color:var(--warn);border:1px solid rgba(245,158,11,.15);}
670
+ .fb-STALE{background:#1a0308;color:var(--cr);border:1px solid rgba(255,34,68,.15);}
671
+ .fb-STABLE{background:#0a0a18;color:var(--sub);border:1px solid var(--bd);}
672
+ .fb-ARCHIVAL{background:#1a0d00;color:var(--c-his);border:1px solid rgba(217,119,6,.15);}
673
+ #btn-new{background:var(--acc);color:#000;border:none;padding:.4rem 1rem;
674
+ font-family:var(--font);font-size:.65rem;font-weight:700;letter-spacing:.1em;
675
+ text-transform:uppercase;border-radius:4px;cursor:pointer;flex-shrink:0;
676
+ transition:background .12s,transform .1s;}
677
+ #btn-new:hover{background:var(--acc2);transform:translateY(-1px);}
678
+
679
+ /* 3-COLUMN LAYOUT */
680
+ #main{flex:1;display:flex;min-height:0;overflow:hidden;}
681
+
682
+ /* LEFT: container sidebar */
683
+ #sidebar{width:210px;flex-shrink:0;border-right:1px solid var(--bd);
684
+ overflow-y:auto;background:var(--s1);}
685
+ #sidebar::-webkit-scrollbar{width:3px;}
686
+ #sidebar::-webkit-scrollbar-thumb{background:var(--bd2);border-radius:2px;}
687
+ .sb-section{padding:.55rem .7rem .2rem;}
688
+ .sb-label{font-family:var(--font);font-size:.47rem;color:var(--sub);text-transform:uppercase;
689
+ letter-spacing:.15em;margin-bottom:.3rem;padding-bottom:.25rem;border-bottom:1px solid var(--bd);}
690
+ .ctr-item{display:flex;align-items:center;gap:.42rem;padding:.38rem .55rem;
691
+ border-radius:6px;cursor:pointer;margin-bottom:.12rem;transition:background .1s;}
692
+ .ctr-item:hover{background:var(--s2);}
693
+ .ctr-item.active{background:var(--s2);border-left:2px solid var(--acc);}
694
+ .ctr-icon{font-size:.85rem;width:1.4rem;text-align:center;flex-shrink:0;}
695
+ .ctr-info{flex:1;min-width:0;}
696
+ .ctr-name{font-size:.65rem;font-weight:600;color:var(--txt);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
697
+ .ctr-meta{display:flex;align-items:center;gap:.3rem;margin-top:.12rem;}
698
+ .ctr-count{font-family:var(--font);font-size:.5rem;color:var(--sub);}
699
+ .ctr-badge{font-family:var(--font);font-size:.42rem;padding:0 4px;border-radius:3px;
700
+ font-weight:700;letter-spacing:.06em;flex-shrink:0;}
701
+ .ctr-value{font-family:var(--font);font-size:.48rem;font-weight:700;margin-left:auto;}
702
+
703
+ /* folder sub-items */
704
+ .folder-item{display:flex;align-items:center;gap:.35rem;padding:.28rem .55rem .28rem 1.5rem;
705
+ border-radius:5px;cursor:pointer;margin-bottom:.06rem;transition:background .1s;font-size:.6rem;color:var(--sub);}
706
+ .folder-item:hover{background:var(--s2);color:var(--txt);}
707
+ .folder-item.active{color:var(--acc);background:var(--s2);}
708
+
709
+ /* CENTER: doc list */
710
+ #list-col{width:360px;flex-shrink:0;border-right:1px solid var(--bd);
711
+ display:flex;flex-direction:column;overflow:hidden;}
712
+ #list-toolbar{flex-shrink:0;padding:.42rem .7rem;border-bottom:1px solid var(--bd);
713
+ background:var(--s1);display:flex;gap:.38rem;flex-wrap:wrap;align-items:center;}
714
+ #search-inp{background:var(--s2);border:1px solid var(--bd2);border-radius:5px;
715
+ padding:.34rem .6rem;font-family:var(--font);font-size:.65rem;color:var(--txt);
716
+ outline:none;width:180px;transition:border-color .12s;}
717
+ #search-inp:focus{border-color:var(--acc);}
718
+ #search-btn{background:var(--acc);color:#000;border:none;padding:.34rem .6rem;
719
+ font-family:var(--font);font-size:.6rem;font-weight:700;border-radius:4px;cursor:pointer;}
720
+ .sort-sel{background:var(--s2);border:1px solid var(--bd2);border-radius:4px;
721
+ padding:.3rem .5rem;font-family:var(--font);font-size:.58rem;color:var(--txt);outline:none;}
722
+ #list-scroll{flex:1;overflow-y:auto;padding:.45rem;}
723
+ #list-scroll::-webkit-scrollbar{width:3px;}
724
+ #list-scroll::-webkit-scrollbar-thumb{background:var(--bd2);border-radius:2px;}
725
+
726
+ /* DOC CARD */
727
+ .dc{background:var(--s1);border:1px solid var(--bd);border-radius:8px;
728
+ padding:.58rem .75rem .58rem .95rem;margin-bottom:.35rem;cursor:pointer;
729
+ position:relative;animation:cin .14s ease;transition:border-color .1s,transform .08s;}
730
+ @keyframes cin{from{opacity:0;transform:translateY(3px)}to{opacity:1;transform:none}}
731
+ .dc:hover{border-color:var(--bd2);transform:translateY(-1px);}
732
+ .dc.active{border-color:var(--acc);background:var(--s2);}
733
+ .dc::before{content:'';position:absolute;left:0;top:0;bottom:0;width:3px;border-radius:8px 0 0 8px;}
734
+ .dc-top{display:flex;align-items:flex-start;gap:.35rem;margin-bottom:.22rem;}
735
+ .dc-title{flex:1;font-size:.7rem;font-weight:600;color:var(--txt);line-height:1.3;word-break:break-word;}
736
+ .dc-val{font-family:var(--font);font-size:.52rem;font-weight:700;flex-shrink:0;margin-top:1px;}
737
+ .dc-preview{font-size:.6rem;color:var(--sub);line-height:1.45;
738
+ max-height:36px;overflow:hidden;position:relative;margin-bottom:.28rem;}
739
+ .dc-preview::after{content:'';position:absolute;bottom:0;left:0;right:0;height:12px;
740
+ background:linear-gradient(transparent,var(--s1));}
741
+ .dc.active .dc-preview::after{background:linear-gradient(transparent,var(--s2));}
742
+ .dc-foot{display:flex;align-items:center;gap:.28rem;flex-wrap:wrap;}
743
+ .dc-tag{font-size:.47rem;background:var(--s2);border:1px solid var(--bd);
744
+ border-radius:3px;padding:0 4px;color:var(--sub);}
745
+ .dc-folder{font-size:.5rem;color:var(--sub);opacity:.6;}
746
+ .dc-date{font-size:.47rem;color:var(--dim);margin-left:auto;}
747
+
748
+ /* VALUE GAUGE */
749
+ .vg{display:inline-flex;align-items:center;gap:.25rem;}
750
+ .vg-bar{width:32px;height:3px;background:var(--bd2);border-radius:2px;overflow:hidden;}
751
+ .vg-fill{height:100%;border-radius:2px;transition:width .3s;}
752
+
753
+ /* RIGHT: detail */
754
+ #detail-col{flex:1;display:flex;flex-direction:column;overflow:hidden;}
755
+ #detail-scroll{flex:1;overflow-y:auto;padding:1.3rem 1.7rem;}
756
+ #detail-scroll::-webkit-scrollbar{width:4px;}
757
+ #detail-scroll::-webkit-scrollbar-thumb{background:var(--bd2);border-radius:2px;}
758
+ #d-empty{display:flex;flex-direction:column;align-items:center;justify-content:center;
759
+ height:100%;gap:.7rem;}
760
+ #d-empty .big{font-size:3rem;opacity:.12;}
761
+ #d-empty .msg{font-family:var(--font);font-size:.62rem;color:var(--sub);
762
+ letter-spacing:.12em;text-transform:uppercase;opacity:.4;}
763
+ #d-content{display:none;}
764
+
765
+ /* VALUE METER */
766
+ .value-meter{background:var(--s1);border:1px solid var(--bd);border-radius:8px;
767
+ padding:.7rem 1rem;margin-bottom:1rem;display:flex;gap:1.2rem;align-items:center;}
768
+ .vm-score{font-family:var(--font);font-size:2.2rem;font-weight:700;line-height:1;}
769
+ .vm-label{font-family:var(--font);font-size:.5rem;text-transform:uppercase;
770
+ letter-spacing:.15em;color:var(--sub);margin-top:.2rem;}
771
+ .vm-info{flex:1;}
772
+ .vm-model{font-family:var(--font);font-size:.55rem;color:var(--sub);margin-bottom:.35rem;}
773
+ .vm-bar-wrap{height:6px;background:var(--bd2);border-radius:3px;}
774
+ .vm-bar-fill{height:100%;border-radius:3px;transition:width .5s;}
775
+ .vm-meta{display:flex;justify-content:space-between;margin-top:.3rem;font-family:var(--font);font-size:.48rem;color:var(--sub);}
776
+ .decay-chips{display:flex;flex-wrap:wrap;gap:.25rem;margin-top:.5rem;}
777
+ .decay-chip{font-family:var(--font);font-size:.48rem;padding:1px 6px;border-radius:3px;background:var(--s2);color:var(--sub);border:1px solid var(--bd);}
778
+
779
+ .d-ctr-hdr{display:flex;align-items:center;gap:.55rem;margin-bottom:.65rem;}
780
+ .d-ctr-icon{font-size:1.2rem;}
781
+ .d-ctr-info .d-ctr-label{font-family:var(--font);font-size:.52rem;font-weight:700;
782
+ letter-spacing:.15em;text-transform:uppercase;margin-bottom:.1rem;}
783
+ .d-ctr-info .d-ctr-note{font-size:.58rem;color:var(--sub);}
784
+ #d-title{font-size:1.1rem;font-weight:600;color:var(--txt);line-height:1.4;margin-bottom:.55rem;word-break:break-word;}
785
+ #d-body{font-size:.76rem;color:var(--txt);line-height:1.72;
786
+ background:var(--s1);border:1px solid var(--bd);border-radius:7px;padding:.9rem 1rem;
787
+ white-space:pre-wrap;margin-bottom:.9rem;font-family:var(--body);}
788
+ .d-meta-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:.4rem .7rem;margin-bottom:.9rem;}
789
+ .dml{font-size:.48rem;font-family:var(--font);color:var(--sub);text-transform:uppercase;letter-spacing:.1em;margin-bottom:.15rem;}
790
+ .dmv{font-size:.62rem;color:var(--txt);}
791
+ .d-tags{display:flex;flex-wrap:wrap;gap:.28rem;margin-bottom:.9rem;}
792
+ .d-tag{background:var(--s2);border:1px solid var(--bd2);border-radius:4px;padding:1px 8px;font-size:.57rem;color:var(--sub);}
793
+ .d-acts{display:flex;gap:.42rem;}
794
+ .d-btn{background:var(--s2);border:1px solid var(--bd2);color:var(--sub);
795
+ padding:.34rem .68rem;font-family:var(--font);font-size:.6rem;border-radius:4px;cursor:pointer;transition:all .1s;}
796
+ .d-btn:hover{background:var(--bd2);color:var(--txt);}
797
+ .d-btn.danger:hover{background:#1e0508;color:var(--cr);}
798
+ .d-btn.acc{background:var(--acc);color:#000;border-color:var(--acc);}
799
+ .d-btn.acc:hover{background:var(--acc2);}
800
+
801
+ /* MODAL */
802
+ #modal{display:none;position:fixed;inset:0;background:rgba(0,0,0,.85);z-index:100;
803
+ backdrop-filter:blur(5px);align-items:center;justify-content:center;}
804
+ #modal.open{display:flex;}
805
+ .mdl{background:var(--s1);border:1px solid var(--bd2);border-top:2px solid var(--acc);
806
+ border-radius:12px;padding:1.4rem;width:640px;max-width:97vw;max-height:92vh;
807
+ overflow-y:auto;animation:mdin .17s ease;position:relative;}
808
+ @keyframes mdin{from{opacity:0;transform:scale(.96) translateY(-8px)}to{opacity:1;transform:none}}
809
+ #mdl-title{font-family:var(--font);font-size:.82rem;font-weight:700;letter-spacing:3px;
810
+ color:var(--acc);margin-bottom:.9rem;}
811
+ #mdl-close{position:absolute;top:.85rem;right:.85rem;background:none;border:none;color:var(--sub);
812
+ width:26px;height:26px;border-radius:4px;cursor:pointer;font-size:.85rem;
813
+ display:flex;align-items:center;justify-content:center;transition:all .1s;}
814
+ #mdl-close:hover{background:var(--bd2);color:var(--txt);}
815
+ .fg2{display:grid;grid-template-columns:1fr 1fr;gap:.6rem;}
816
+ .fg3{display:grid;grid-template-columns:1fr 1fr 1fr;gap:.6rem;}
817
+ .fl{margin-bottom:.6rem;}
818
+ .fl label{display:block;font-family:var(--font);font-size:.48rem;color:var(--sub);
819
+ text-transform:uppercase;letter-spacing:.12em;margin-bottom:.2rem;}
820
+ .fl input,.fl textarea,.fl select{width:100%;background:var(--s2);border:1px solid var(--bd2);
821
+ border-radius:5px;padding:.4rem .58rem;font-family:var(--body);font-size:.72rem;color:var(--txt);
822
+ outline:none;transition:border-color .12s;}
823
+ .fl input:focus,.fl textarea:focus,.fl select:focus{border-color:var(--acc);}
824
+ .fl textarea{min-height:130px;line-height:1.65;resize:vertical;}
825
+ .fl select option{background:var(--s2);}
826
+ #folder-sel option{background:var(--s2);}
827
+ #mdl-actions{display:flex;gap:.42rem;margin-top:.85rem;}
828
+ #btn-save{flex:1;background:var(--acc);color:#000;border:none;padding:.48rem 1rem;
829
+ font-family:var(--font);font-size:.65rem;font-weight:700;letter-spacing:.1em;
830
+ text-transform:uppercase;border-radius:5px;cursor:pointer;transition:background .1s;}
831
+ #btn-save:hover{background:var(--acc2);}
832
+ #btn-mcancel{background:var(--s2);color:var(--sub);border:1px solid var(--bd2);padding:.48rem .9rem;
833
+ font-family:var(--font);font-size:.65rem;letter-spacing:.1em;text-transform:uppercase;
834
+ border-radius:5px;cursor:pointer;transition:all .1s;}
835
+ #btn-mcancel:hover{background:var(--bd2);color:var(--txt);}
836
+ #decay-preview{background:var(--s2);border:1px solid var(--bd2);border-radius:6px;
837
+ padding:.55rem .75rem;margin-top:.6rem;font-family:var(--font);font-size:.58rem;color:var(--sub);}
838
+ #decay-preview strong{color:var(--acc);}
839
+
840
+ /* TOAST */
841
+ #toasts{position:fixed;bottom:1rem;right:1rem;z-index:200;display:flex;flex-direction:column;gap:.35rem;}
842
+ .tst{background:var(--s1);border:1px solid var(--bd2);border-left:3px solid var(--acc);
843
+ padding:.42rem .78rem;font-size:.62rem;border-radius:6px;animation:tin .15s ease;
844
+ color:var(--txt);max-width:280px;font-family:var(--font);}
845
+ .tst.ok{border-left-color:var(--lo);}.tst.err{border-left-color:var(--cr);}
846
+ @keyframes tin{from{opacity:0;transform:translateX(12px)}to{opacity:1;transform:none}}
847
+ #mcp-hint{position:fixed;bottom:1rem;left:.8rem;z-index:10;background:var(--s1);
848
+ border:1px solid var(--bd2);border-left:3px solid var(--sub);border-radius:6px;
849
+ padding:.38rem .72rem;font-family:var(--font);font-size:.52rem;color:var(--sub);}
850
+ </style>
851
+ </head>
852
+ <body>
853
+
854
+ <div id="hdr">
855
+ <div>
856
+ <div id="logo">KNOWLEDGE STORE</div>
857
+ <div id="logo-sub">10 Containers &middot; Temporal Value Engine &middot; MCP &middot; ki-fusion-labs.de</div>
858
+ </div>
859
+ <div id="hdr-stats">
860
+ <div class="hs"><span class="hs-n" id="s-total" style="color:var(--txt)">0</span>DOCS</div>
861
+ <div class="hs"><span class="hs-n" id="s-avg-val" style="color:var(--acc)">0</span>AVG VALUE</div>
862
+ <div class="hs"><span class="freshbadge fb-STALE" id="s-stale">0</span>STALE</div>
863
+ <div class="hs"><span class="freshbadge fb-FRESH" id="s-fresh">0</span>FRESH</div>
864
+ </div>
865
+ <button id="btn-new">+ New Document</button>
866
+ </div>
867
+
868
+ <div id="main">
869
+
870
+ <!-- SIDEBAR -->
871
+ <div id="sidebar">
872
+ <div class="sb-section">
873
+ <div class="sb-label">Containers</div>
874
+ <div class="ctr-item active" id="ctr-all" data-ctr="">
875
+ <div class="ctr-icon">&#128196;</div>
876
+ <div class="ctr-info">
877
+ <div class="ctr-name">All Documents</div>
878
+ <div class="ctr-meta"><span class="ctr-count" id="cnt-all">0 docs</span></div>
879
+ </div>
880
+ </div>
881
+ </div>
882
+ <div class="sb-section" id="ctr-list"></div>
883
+ <div class="sb-section" id="folder-list" style="display:none">
884
+ <div class="sb-label" id="folder-label">Folders</div>
885
+ <div id="folder-items"></div>
886
+ </div>
887
+ </div>
888
+
889
+ <!-- LIST -->
890
+ <div id="list-col">
891
+ <div id="list-toolbar">
892
+ <input type="text" id="search-inp" placeholder="Search...">
893
+ <button id="search-btn">&#128269;</button>
894
+ <select class="sort-sel" id="sort-sel">
895
+ <option value="relevance">Relevance</option>
896
+ <option value="value">Value Score</option>
897
+ <option value="newest" selected>Newest</option>
898
+ <option value="oldest">Oldest</option>
899
+ <option value="importance">Importance</option>
900
+ </select>
901
+ </div>
902
+ <div id="list-scroll"><div id="list-empty" style="font-size:.6rem;color:var(--dim);text-align:center;padding:2rem;">Loading...</div></div>
903
+ </div>
904
+
905
+ <!-- DETAIL -->
906
+ <div id="detail-col">
907
+ <div id="detail-scroll">
908
+ <div id="d-empty">
909
+ <div class="big">&#128196;</div>
910
+ <div class="msg">Select a document</div>
911
+ </div>
912
+ <div id="d-content"></div>
913
+ </div>
914
+ </div>
915
+
916
+ </div><!-- /main -->
917
+
918
+ <!-- COMPOSE MODAL -->
919
+ <div id="modal">
920
+ <div class="mdl">
921
+ <button id="mdl-close">&#x2715;</button>
922
+ <div id="mdl-title">NEW KNOWLEDGE DOCUMENT</div>
923
+ <div class="fg2">
924
+ <div class="fl"><label>Container *</label>
925
+ <select id="m-container"></select></div>
926
+ <div class="fl"><label>Folder</label>
927
+ <select id="m-folder"></select></div>
928
+ </div>
929
+ <div class="fl"><label>Title *</label>
930
+ <input type="text" id="m-title" placeholder="Document title"></div>
931
+ <div class="fl"><label>Body *</label>
932
+ <textarea id="m-body" placeholder="Knowledge content (markdown supported in display)..."></textarea></div>
933
+ <div class="fl"><label>Summary (one-liner)</label>
934
+ <input type="text" id="m-summary" placeholder="Brief description for search results"></div>
935
+ <div class="fg3">
936
+ <div class="fl"><label>Author</label>
937
+ <input type="text" id="m-author" placeholder="christof"></div>
938
+ <div class="fl"><label>Version</label>
939
+ <input type="text" id="m-version" placeholder="v1.0"></div>
940
+ <div class="fl"><label>Importance (0-10)</label>
941
+ <input type="number" id="m-importance" value="5" min="0" max="10"></div>
942
+ </div>
943
+ <div class="fl"><label>Tags (comma separated)</label>
944
+ <input type="text" id="m-tags" placeholder="gdpr, architecture, v3"></div>
945
+ <div id="decay-preview">Select a container to see its decay model...</div>
946
+ <div id="mdl-actions">
947
+ <button id="btn-save">&#9889; Save Document</button>
948
+ <button id="btn-mcancel">Cancel</button>
949
+ </div>
950
+ </div>
951
+ </div>
952
+
953
+ <div id="toasts"></div>
954
+ <div id="mcp-hint">MCP: <code>ks_write</code> &nbsp;|&nbsp; <code>ks_search</code> &nbsp;|&nbsp; <code>ks_top_value</code></div>
955
+
956
+ <script>
957
+ var CONTAINERS_META = {};
958
+ var ALL_DOCS = [];
959
+ var ACTIVE_CTR = '';
960
+ var ACTIVE_FOLDER = '';
961
+ var ACTIVE_ID = null;
962
+ var SORT = 'newest';
963
+ var SEARCH_Q = '';
964
+
965
+ function esc(s){return String(s||'').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');}
966
+ function tsDate(ts){if(!ts)return ''; return new Date(ts*1000).toLocaleDateString('de-DE',{day:'2-digit',month:'short',year:'2-digit'});}
967
+ function tsAgo(ts){
968
+ if(!ts)return '';
969
+ var d=Math.floor((Date.now()/1000)-ts);
970
+ if(d<60)return d+'s ago'; if(d<3600)return Math.floor(d/60)+'m ago';
971
+ if(d<86400)return Math.floor(d/3600)+'h ago';
972
+ if(d<86400*30)return Math.floor(d/86400)+'d ago';
973
+ return Math.floor(d/86400/30)+'mo ago';
974
+ }
975
+ function toast(msg,t){
976
+ var el=document.createElement('div');el.className='tst'+(t?' '+t:'');el.textContent=msg;
977
+ document.getElementById('toasts').appendChild(el);setTimeout(function(){el.remove();},2700);
978
+ }
979
+ function valueColor(v){
980
+ if(v>=70)return 'var(--lo)';
981
+ if(v>=40)return 'var(--warn)';
982
+ return 'var(--cr)';
983
+ }
984
+ function post(url,data){return fetch(url,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(data)});}
985
+
986
+ // ── Load ──────────────────────��───────────────────────────────────
987
+ function loadAll(){
988
+ Promise.all([
989
+ fetch('/api/containers').then(function(r){return r.json();}),
990
+ fetch('/api/stats').then(function(r){return r.json();}),
991
+ ]).then(function(res){
992
+ CONTAINERS_META = res[0];
993
+ var stats = res[1];
994
+ document.getElementById('s-total').textContent = stats.total;
995
+ document.getElementById('s-avg-val').textContent = stats.avg_value;
996
+ document.getElementById('s-stale').textContent = stats.by_freshness.STALE||0;
997
+ document.getElementById('s-fresh').textContent = stats.by_freshness.FRESH||0;
998
+ document.getElementById('cnt-all').textContent = stats.total+' docs';
999
+ buildSidebar();
1000
+ populateContainerSelect();
1001
+ loadDocs();
1002
+ });
1003
+ }
1004
+
1005
+ function buildSidebar(){
1006
+ var list = document.getElementById('ctr-list');
1007
+ list.innerHTML='';
1008
+ var order=['medical','legal','company','research','tech','prompts','history','personal','finance','operations'];
1009
+ order.forEach(function(key){
1010
+ var c = CONTAINERS_META[key]; if(!c) return;
1011
+ var avgV = c.avg_value||0;
1012
+ var vc = valueColor(avgV);
1013
+ var badgeCol = {'FAST-DECAY':'var(--cr)','EXTREME-DECAY':'var(--cr)','CRITICAL-DECAY':'var(--cr)',
1014
+ 'SLOW-DECAY':'var(--lo)','STABLE':'var(--lo)','ANTI-DECAY':'var(--c-his)',
1015
+ 'CITATION-CURVE':'var(--c-res)','TIERED-DECAY':'var(--c-com)',
1016
+ 'MODERATE-DECAY':'var(--warn)','DRIFT-DECAY':'var(--warn)','VERSIONED-DECAY':'var(--warn)'}[c.badge]||'var(--sub)';
1017
+ var item = document.createElement('div');
1018
+ item.className='ctr-item'+(ACTIVE_CTR==key?' active':'');
1019
+ item.dataset.ctr=key;
1020
+ item.innerHTML=
1021
+ '<div class="ctr-icon">'+c.icon+'</div>'
1022
+ +'<div class="ctr-info">'
1023
+ +'<div class="ctr-name">'+esc(c.label)+'</div>'
1024
+ +'<div class="ctr-meta">'
1025
+ +'<span class="ctr-count">'+c.count+' docs</span>'
1026
+ +'<span class="ctr-badge" style="background:'+badgeCol+'18;color:'+badgeCol+';border-color:'+badgeCol+'30">'+c.badge+'</span>'
1027
+ +'</div>'
1028
+ +'</div>'
1029
+ +'<span class="ctr-value" style="color:'+vc+'">'+avgV+'</span>';
1030
+ item.addEventListener('click',function(){selectContainer(key);});
1031
+ list.appendChild(item);
1032
+ });
1033
+ }
1034
+
1035
+ function selectContainer(key){
1036
+ ACTIVE_CTR=key; ACTIVE_FOLDER='';
1037
+ document.querySelectorAll('.ctr-item,.folder-item').forEach(function(el){
1038
+ el.classList.toggle('active',el.dataset.ctr==key||el.dataset.folder==key);
1039
+ });
1040
+ document.getElementById('ctr-all').classList.toggle('active',!key);
1041
+ // Show folders
1042
+ var flist=document.getElementById('folder-list');
1043
+ var fitems=document.getElementById('folder-items');
1044
+ var cfg=CONTAINERS_META[key];
1045
+ if(cfg&&cfg.folders&&cfg.folders.length){
1046
+ document.getElementById('folder-label').textContent=(cfg.label||key)+' folders';
1047
+ flist.style.display='block';
1048
+ fitems.innerHTML='';
1049
+ cfg.folders.forEach(function(f){
1050
+ var fi=document.createElement('div');
1051
+ fi.className='folder-item';fi.dataset.folder=f;
1052
+ fi.textContent='/'+f;
1053
+ fi.addEventListener('click',function(e){
1054
+ e.stopPropagation();
1055
+ ACTIVE_FOLDER=f;
1056
+ document.querySelectorAll('.folder-item').forEach(function(el){el.classList.toggle('active',el.dataset.folder==f);});
1057
+ loadDocs();
1058
+ });
1059
+ fitems.appendChild(fi);
1060
+ });
1061
+ } else {
1062
+ flist.style.display='none';
1063
+ }
1064
+ loadDocs();
1065
+ }
1066
+
1067
+ document.getElementById('ctr-all').addEventListener('click',function(){
1068
+ ACTIVE_CTR='';ACTIVE_FOLDER='';
1069
+ document.querySelectorAll('.ctr-item,.folder-item').forEach(function(el){el.classList.remove('active');});
1070
+ document.getElementById('ctr-all').classList.add('active');
1071
+ document.getElementById('folder-list').style.display='none';
1072
+ loadDocs();
1073
+ });
1074
+
1075
+ function loadDocs(){
1076
+ var url='/api/docs?sort='+SORT+'&limit=200'
1077
+ +(ACTIVE_CTR?'&container='+ACTIVE_CTR:'')
1078
+ +(ACTIVE_FOLDER?'&folder='+ACTIVE_FOLDER:'');
1079
+ if(SEARCH_Q) url='/api/docs/search?q='+encodeURIComponent(SEARCH_Q)+'&sort='+SORT
1080
+ +(ACTIVE_CTR?'&container='+ACTIVE_CTR:'')+(ACTIVE_FOLDER?'&folder='+ACTIVE_FOLDER:'');
1081
+ fetch(url).then(function(r){return r.json();}).then(function(docs){
1082
+ ALL_DOCS=docs;renderList();
1083
+ });
1084
+ }
1085
+
1086
+ // ── Render list ───────────────────────────────────────────────────
1087
+ function renderList(){
1088
+ var scroll=document.getElementById('list-scroll');
1089
+ scroll.innerHTML='';
1090
+ if(!ALL_DOCS.length){
1091
+ var e=document.createElement('div');
1092
+ e.style.cssText='font-size:.6rem;color:var(--dim);text-align:center;padding:2rem;';
1093
+ e.textContent='No documents';scroll.appendChild(e);return;
1094
+ }
1095
+ ALL_DOCS.forEach(function(d){scroll.appendChild(makeCard(d));});
1096
+ }
1097
+
1098
+ function makeCard(d){
1099
+ var ctr=CONTAINERS_META[d.container]||{};
1100
+ var col=ctr.color||'var(--acc)';
1101
+ var val=d._value||0;
1102
+ var vc=valueColor(val);
1103
+ var fl=d._freshness||'FRESH';
1104
+ var card=document.createElement('div');
1105
+ card.className='dc'+(ACTIVE_ID==d.id?' active':'');
1106
+ card.id='dc-'+d.id;
1107
+ card.style.setProperty('--ctr-color',col);
1108
+ card.style.borderLeft='3px solid '+col+'55';
1109
+ var tags=(d.tags||[]).slice(0,2).map(function(t){return '<span class="dc-tag">'+esc(t)+'</span>';}).join('');
1110
+ var valBar='<div class="vg"><div class="vg-bar"><div class="vg-fill" style="width:'+val+'%;background:'+vc+'"></div></div></div>';
1111
+ card.innerHTML=
1112
+ '<div class="dc-top">'
1113
+ +'<div class="dc-title">'+esc(d.title)+'</div>'
1114
+ +'<div class="dc-val" style="color:'+vc+'">'+val+'</div>'
1115
+ +'</div>'
1116
+ +(d.summary?'<div class="dc-preview">'+esc(d.summary)+'</div>':'<div class="dc-preview">'+esc((d.body||'').substring(0,90))+'</div>')
1117
+ +'<div class="dc-foot">'
1118
+ +'<span class="freshbadge fb-'+fl+'">'+fl+'</span>'
1119
+ +tags
1120
+ +'<span class="dc-folder">'+esc(d.folder)+'</span>'
1121
+ +'<span class="dc-date">'+tsAgo(d.created_at)+'</span>'
1122
+ +'</div>';
1123
+ card.addEventListener('click',function(){selectDoc(d);});
1124
+ return card;
1125
+ }
1126
+
1127
+ // ── Detail ────────────────────────────────────────────────────────
1128
+ function selectDoc(d){
1129
+ ACTIVE_ID=d.id;
1130
+ document.querySelectorAll('.dc').forEach(function(c){c.classList.toggle('active',c.id=='dc-'+d.id);});
1131
+ // Fetch fresh with access bump
1132
+ fetch('/api/docs/'+d.container+'/'+d.folder+'/'+d.id)
1133
+ .then(function(r){return r.json();}).then(function(doc){renderDetail(doc);})
1134
+ .catch(function(){renderDetail(d);});
1135
+ }
1136
+
1137
+ function renderDetail(d){
1138
+ document.getElementById('d-empty').style.display='none';
1139
+ var dc=document.getElementById('d-content');dc.style.display='block';
1140
+ var ctr=CONTAINERS_META[d.container]||{};
1141
+ var col=ctr.color||'var(--acc)';
1142
+ var val=d._value||0;
1143
+ var vc=valueColor(val);
1144
+ var fl=d._freshness||'FRESH';
1145
+ var hl=ctr.half_life_days;
1146
+ var model=ctr.decay_model||'exponential';
1147
+ var tags=(d.tags||[]).map(function(t){return '<span class="d-tag">'+esc(t)+'</span>';}).join('');
1148
+ var ageDays=Math.floor((Date.now()/1000-d.created_at)/86400);
1149
+
1150
+ // Value meter
1151
+ var modelDesc={
1152
+ 'stable':'No time decay &mdash; value remains constant.',
1153
+ 'anti_decay':'Value INCREASES with age (archival pattern).',
1154
+ 'citation_curve':'Peaks at day '+((ctr.peak_days||30))+', then slow decay.',
1155
+ 'extreme_decay':'Extreme decay. Half-life: '+(hl||7)+' days.',
1156
+ 'exponential':'Exponential decay. Half-life: '+(hl||180)+' days.',
1157
+ 'slow_exponential':'Slow decay. Half-life: '+(hl||730)+' days.',
1158
+ 'tiered':'Folder-dependent half-life (people 60d, market 14d, sop 365d).',
1159
+ 'versioned_decay':'Fast versioned decay. Half-life: '+(hl||90)+' days.',
1160
+ 'drift_decay':'Preference drift decay. Half-life: '+(hl||180)+' days.',
1161
+ 'operational_decay':'Operational decay. Half-life: '+(hl||180)+' days.',
1162
+ }[model]||'Standard decay model.';
1163
+
1164
+ var chips='<div class="decay-chips">'
1165
+ +'<div class="decay-chip">age: '+ageDays+'d</div>'
1166
+ +(hl?'<div class="decay-chip">half-life: '+hl+'d</div>':'')
1167
+ +'<div class="decay-chip">model: '+esc(model)+'</div>'
1168
+ +'<div class="decay-chip">accessed: '+(d.access_count||0)+'x</div>'
1169
+ +(d.version?'<div class="decay-chip">ver: '+esc(d.version)+'</div>':'')
1170
+ +'</div>';
1171
+
1172
+ dc.innerHTML=
1173
+ '<div class="d-ctr-hdr">'
1174
+ +'<div class="d-ctr-icon">'+ctr.icon+'</div>'
1175
+ +'<div class="d-ctr-info">'
1176
+ +'<div class="d-ctr-label" style="color:'+col+'">'+esc(ctr.label||d.container)+'</div>'
1177
+ +'<div class="d-ctr-note">'+esc(ctr.note||'')+'</div>'
1178
+ +'</div>'
1179
+ +'<span class="freshbadge fb-'+fl+'">'+fl+'</span>'
1180
+ +'</div>'
1181
+ +'<div class="value-meter">'
1182
+ +'<div><div class="vm-score" style="color:'+vc+'">'+val+'</div><div class="vm-label">value score</div></div>'
1183
+ +'<div class="vm-info">'
1184
+ +'<div class="vm-model">'+modelDesc+'</div>'
1185
+ +'<div class="vm-bar-wrap"><div class="vm-bar-fill" style="width:'+val+'%;background:'+vc+'"></div></div>'
1186
+ +'<div class="vm-meta"><span>0 (worthless)</span><span>50 (relevant)</span><span>100 (critical)</span></div>'
1187
+ +chips
1188
+ +'</div>'
1189
+ +'</div>'
1190
+ +'<div id="d-title">'+esc(d.title)+'</div>'
1191
+ +'<div id="d-body">'+esc(d.body||'')+'</div>'
1192
+ +(tags?'<div class="d-tags">'+tags+'</div>':'')
1193
+ +'<div class="d-meta-grid">'
1194
+ +'<div><div class="dml">Author</div><div class="dmv">'+(d.author||'&mdash;')+'</div></div>'
1195
+ +'<div><div class="dml">Folder</div><div class="dmv">'+esc(d.folder)+'</div></div>'
1196
+ +'<div><div class="dml">Importance</div><div class="dmv">'+d.importance+'/10</div></div>'
1197
+ +'<div><div class="dml">Created</div><div class="dmv">'+tsDate(d.created_at)+'</div></div>'
1198
+ +'<div><div class="dml">Updated</div><div class="dmv">'+tsDate(d.updated_at)+'</div></div>'
1199
+ +'<div><div class="dml">Source</div><div class="dmv">'+(d.source||'&mdash;')+'</div></div>'
1200
+ +'</div>'
1201
+ +'<div class="d-acts">'
1202
+ +'<button class="d-btn acc" id="d-edit">&#9998; Edit</button>'
1203
+ +'<button class="d-btn danger" id="d-del">&#128465; Delete</button>'
1204
+ +'</div>';
1205
+ document.getElementById('d-edit').addEventListener('click',function(){openModal(d);});
1206
+ document.getElementById('d-del').addEventListener('click',function(){deleteDoc(d);});
1207
+ }
1208
+
1209
+ function deleteDoc(d){
1210
+ if(!confirm('Delete "'+d.title+'"?'))return;
1211
+ fetch('/api/docs/'+d.container+'/'+d.folder+'/'+d.id,{method:'DELETE'}).then(function(){
1212
+ toast('Deleted','ok');ACTIVE_ID=null;
1213
+ document.getElementById('d-empty').style.display='flex';
1214
+ document.getElementById('d-content').style.display='none';
1215
+ loadAll();
1216
+ });
1217
+ }
1218
+
1219
+ // ── Search / sort ─────────────────────────────────────────────────
1220
+ document.getElementById('search-btn').addEventListener('click',function(){
1221
+ SEARCH_Q=document.getElementById('search-inp').value.trim();loadDocs();});
1222
+ document.getElementById('search-inp').addEventListener('keydown',function(e){
1223
+ if(e.key=='Enter'){SEARCH_Q=this.value.trim();loadDocs();}
1224
+ if(e.key=='Escape'){this.value='';SEARCH_Q='';loadDocs();}
1225
+ });
1226
+ document.getElementById('sort-sel').addEventListener('change',function(){SORT=this.value;loadDocs();});
1227
+
1228
+ // ── Modal ─────────────────────────────────────────────────────────
1229
+ function populateContainerSelect(){
1230
+ var sel=document.getElementById('m-container');
1231
+ sel.innerHTML='';
1232
+ var order=['medical','legal','company','research','tech','prompts','history','personal','finance','operations'];
1233
+ order.forEach(function(k){
1234
+ var c=CONTAINERS_META[k]||{};
1235
+ var o=document.createElement('option');o.value=k;o.textContent=c.label||k;sel.appendChild(o);
1236
+ });
1237
+ updateFolderSelect();
1238
+ updateDecayPreview();
1239
+ }
1240
+
1241
+ function updateFolderSelect(){
1242
+ var ctr=document.getElementById('m-container').value;
1243
+ var c=CONTAINERS_META[ctr]||{};
1244
+ var sel=document.getElementById('m-folder');
1245
+ sel.innerHTML='';
1246
+ (c.folders||['general']).forEach(function(f){
1247
+ var o=document.createElement('option');o.value=f;o.textContent=f;sel.appendChild(o);
1248
+ });
1249
+ }
1250
+
1251
+ function updateDecayPreview(){
1252
+ var ctr=document.getElementById('m-container').value;
1253
+ var c=CONTAINERS_META[ctr]||{};
1254
+ var model=c.decay_model||'exponential';
1255
+ var hl=c.half_life_days;
1256
+ var warn=c.warn_after_days;
1257
+ var msgs={
1258
+ 'stable':'<strong>STABLE</strong> &mdash; Value does not decay over time. Great for reference prompts and fixed facts.',
1259
+ 'anti_decay':'<strong>ANTI-DECAY</strong> &mdash; Value INCREASES over time. Historical records become more valuable as context.',
1260
+ 'citation_curve':'<strong>CITATION CURVE</strong> &mdash; Peaks at day '+(c.peak_days||30)+', then slow exponential decay (HL: '+hl+'d). Like academic papers.',
1261
+ 'extreme_decay':'<strong>EXTREME DECAY</strong> &mdash; Half-life '+(hl||7)+' days. Market data is near-worthless after a week.',
1262
+ 'tiered':'<strong>TIERED</strong> &mdash; Half-life depends on folder: market=14d, people=60d, projects=90d, sop=365d.',
1263
+ 'versioned_decay':'<strong>VERSIONED DECAY</strong> &mdash; Half-life '+(hl||90)+' days. Tag with version number to track relevance.',
1264
+ 'exponential':'<strong>EXPONENTIAL</strong> &mdash; Standard decay. Half-life '+(hl||180)+' days. Warning after '+(warn||90)+' days.',
1265
+ 'slow_exponential':'<strong>SLOW DECAY</strong> &mdash; Half-life '+(hl||730)+' days. Laws change slowly.',
1266
+ 'drift_decay':'<strong>DRIFT DECAY</strong> &mdash; Preferences drift. Half-life '+(hl||180)+' days.',
1267
+ 'operational_decay':'<strong>OPERATIONAL</strong> &mdash; Runbooks age with infra. Half-life '+(hl||180)+' days. Keep versioned.',
1268
+ };
1269
+ document.getElementById('decay-preview').innerHTML=
1270
+ (msgs[model]||'Standard decay model.')
1271
+ +'<br><span style="color:var(--acc)">Container: '+esc(c.label||ctr)+'</span> &mdash; '
1272
+ +esc(c.note||'');
1273
+ }
1274
+
1275
+ document.getElementById('m-container').addEventListener('change',function(){updateFolderSelect();updateDecayPreview();});
1276
+
1277
+ function openModal(doc){
1278
+ document.getElementById('mdl-title').textContent=doc?'EDIT DOCUMENT':'NEW KNOWLEDGE DOCUMENT';
1279
+ if(doc){
1280
+ document.getElementById('m-container').value=doc.container;
1281
+ updateFolderSelect();
1282
+ document.getElementById('m-folder').value=doc.folder;
1283
+ document.getElementById('m-title').value=doc.title;
1284
+ document.getElementById('m-body').value=doc.body||'';
1285
+ document.getElementById('m-summary').value=doc.summary||'';
1286
+ document.getElementById('m-author').value=doc.author||'';
1287
+ document.getElementById('m-version').value=doc.version||'';
1288
+ document.getElementById('m-importance').value=doc.importance||5;
1289
+ document.getElementById('m-tags').value=(doc.tags||[]).join(', ');
1290
+ document.getElementById('btn-save').dataset.editId=doc.container+'|'+doc.folder+'|'+doc.id;
1291
+ } else {
1292
+ document.getElementById('btn-save').dataset.editId='';
1293
+ ['m-title','m-body','m-summary','m-author','m-version','m-tags'].forEach(function(id){
1294
+ document.getElementById(id).value='';});
1295
+ document.getElementById('m-importance').value='5';
1296
+ }
1297
+ updateDecayPreview();
1298
+ document.getElementById('modal').classList.add('open');
1299
+ setTimeout(function(){document.getElementById('m-title').focus();},80);
1300
+ }
1301
+ function closeModal(){document.getElementById('modal').classList.remove('open');}
1302
+
1303
+ document.getElementById('btn-new').addEventListener('click',function(){openModal();});
1304
+ document.getElementById('mdl-close').addEventListener('click',closeModal);
1305
+ document.getElementById('btn-mcancel').addEventListener('click',closeModal);
1306
+ document.getElementById('modal').addEventListener('click',function(e){if(e.target===this)closeModal();});
1307
+
1308
+ document.getElementById('btn-save').addEventListener('click',function(){
1309
+ var title=document.getElementById('m-title').value.trim();
1310
+ var body=document.getElementById('m-body').value.trim();
1311
+ if(!title){document.getElementById('m-title').focus();toast('Title required','err');return;}
1312
+ if(!body){document.getElementById('m-body').focus();toast('Body required','err');return;}
1313
+ var tags=document.getElementById('m-tags').value.split(',').map(function(t){return t.trim();}).filter(Boolean);
1314
+ var pay={
1315
+ container:document.getElementById('m-container').value,
1316
+ folder:document.getElementById('m-folder').value,
1317
+ title:title,body:body,
1318
+ summary:document.getElementById('m-summary').value.trim(),
1319
+ author:document.getElementById('m-author').value.trim(),
1320
+ version:document.getElementById('m-version').value.trim(),
1321
+ importance:parseInt(document.getElementById('m-importance').value)||5,
1322
+ tags:tags
1323
+ };
1324
+ var editKey=this.dataset.editId;
1325
+ if(editKey){
1326
+ var parts=editKey.split('|');
1327
+ fetch('/api/docs/'+parts[0]+'/'+parts[1]+'/'+parts[2],
1328
+ {method:'PATCH',headers:{'Content-Type':'application/json'},body:JSON.stringify(pay)})
1329
+ .then(function(r){return r.json();}).then(function(d){
1330
+ toast('Updated','ok');closeModal();loadAll();
1331
+ setTimeout(function(){if(d.doc)renderDetail(d.doc);},300);
1332
+ }).catch(function(e){toast('Error: '+e.message,'err');});
1333
+ } else {
1334
+ post('/api/docs',pay).then(function(r){return r.json();}).then(function(d){
1335
+ toast('Saved to '+pay.container+'/'+pay.folder,'ok');closeModal();loadAll();
1336
+ }).catch(function(e){toast('Error: '+e.message,'err');});
1337
+ }
1338
+ });
1339
+
1340
+ document.addEventListener('keydown',function(e){
1341
+ if(e.key==='Escape')closeModal();
1342
+ var a=document.activeElement;
1343
+ var typing=a&&(a.tagName==='INPUT'||a.tagName==='TEXTAREA'||a.tagName==='SELECT');
1344
+ if(e.key==='n'&&!typing&&!e.ctrlKey&&!e.metaKey)openModal();
1345
+ if((e.ctrlKey||e.metaKey)&&e.key==='Enter'&&document.getElementById('modal').classList.contains('open'))
1346
+ document.getElementById('btn-save').click();
1347
+ if(e.key==='/'&&!typing){e.preventDefault();document.getElementById('search-inp').focus();}
1348
+ });
1349
+
1350
+ loadAll();
1351
+ </script>
1352
+ </body>
1353
+ </html>"""
requirements.txt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ fastapi>=0.111.0
2
+ uvicorn>=0.30.0
3
+ python-multipart>=0.0.9