Spaces:
Sleeping
Sleeping
Upload 3 files
Browse files- Dockerfile +10 -0
- main.py +1353 -0
- 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 📄",
|
| 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 · Temporal Value Engine · MCP · 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">📄</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">🔍</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">📄</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">✕</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">⚡ 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> | <code>ks_search</code> | <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,'&').replace(/</g,'<').replace(/>/g,'>');}
|
| 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 — 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||'—')+'</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||'—')+'</div></div>'
|
| 1200 |
+
+'</div>'
|
| 1201 |
+
+'<div class="d-acts">'
|
| 1202 |
+
+'<button class="d-btn acc" id="d-edit">✎ Edit</button>'
|
| 1203 |
+
+'<button class="d-btn danger" id="d-del">🗑 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> — Value does not decay over time. Great for reference prompts and fixed facts.',
|
| 1259 |
+
'anti_decay':'<strong>ANTI-DECAY</strong> — Value INCREASES over time. Historical records become more valuable as context.',
|
| 1260 |
+
'citation_curve':'<strong>CITATION CURVE</strong> — Peaks at day '+(c.peak_days||30)+', then slow exponential decay (HL: '+hl+'d). Like academic papers.',
|
| 1261 |
+
'extreme_decay':'<strong>EXTREME DECAY</strong> — Half-life '+(hl||7)+' days. Market data is near-worthless after a week.',
|
| 1262 |
+
'tiered':'<strong>TIERED</strong> — Half-life depends on folder: market=14d, people=60d, projects=90d, sop=365d.',
|
| 1263 |
+
'versioned_decay':'<strong>VERSIONED DECAY</strong> — Half-life '+(hl||90)+' days. Tag with version number to track relevance.',
|
| 1264 |
+
'exponential':'<strong>EXPONENTIAL</strong> — Standard decay. Half-life '+(hl||180)+' days. Warning after '+(warn||90)+' days.',
|
| 1265 |
+
'slow_exponential':'<strong>SLOW DECAY</strong> — Half-life '+(hl||730)+' days. Laws change slowly.',
|
| 1266 |
+
'drift_decay':'<strong>DRIFT DECAY</strong> — Preferences drift. Half-life '+(hl||180)+' days.',
|
| 1267 |
+
'operational_decay':'<strong>OPERATIONAL</strong> — 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> — '
|
| 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
|