commit c01def696c189e83da01931293c43eeb29f014ef Author: brutsalvadi Date: Wed Apr 22 22:54:19 2026 +0200 Initialize bincio_wiki project structure - Add brutsalvadi/astro-bloomz as git submodule at site/ - Add edit/ FastAPI sidecar (read/write markdown pages, trigger rebuild) - Add scripts/dev.sh and scripts/build.sh (symlink pages/, run Astro + Pagefind) - Add .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9c4adb3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,16 @@ +# Python +__pycache__/ +*.pyc +.venv/ +venv/ + +# Site build output (managed by submodule) +site/dist/ +site/node_modules/ +site/.astro/ + +# Claude Code local settings +.claude/ + +# OS +.DS_Store diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..4f77000 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "site"] + path = site + url = https://github.com/brutsalvadi/astro-bloomz diff --git a/edit/__init__.py b/edit/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/edit/requirements.txt b/edit/requirements.txt new file mode 100644 index 0000000..eca82c6 --- /dev/null +++ b/edit/requirements.txt @@ -0,0 +1,3 @@ +fastapi>=0.115.0 +uvicorn[standard]>=0.30.0 +pydantic>=2.0.0 diff --git a/edit/server.py b/edit/server.py new file mode 100644 index 0000000..b0b53d2 --- /dev/null +++ b/edit/server.py @@ -0,0 +1,106 @@ +"""FastAPI edit sidecar for BincioWiki — reads and writes markdown pages.""" + +from __future__ import annotations + +import os +import re +import subprocess +from pathlib import Path + +from fastapi import FastAPI, HTTPException +from fastapi.middleware.cors import CORSMiddleware +from fastapi.middleware.gzip import GZipMiddleware +from fastapi.responses import JSONResponse +from pydantic import BaseModel + +# Resolved at startup relative to the project root (one level above this file) +_ROOT = Path(__file__).parent.parent +pages_dir: Path = _ROOT / os.environ.get("WIKI_PAGES_DIR", "pages") +site_dir: Path = _ROOT / "site" + +_SAFE_SLUG = re.compile(r"^[a-zA-Z0-9][a-zA-Z0-9_\-/]*$") + +app = FastAPI(title="BincioWiki Edit Sidecar", docs_url=None, redoc_url=None) + +app.add_middleware(GZipMiddleware, minimum_size=1024) +app.add_middleware( + CORSMiddleware, + allow_origin_regex=r"https?://localhost(:\d+)?", + allow_methods=["GET", "POST", "DELETE"], + allow_headers=["Content-Type"], +) + + +def _slug_to_path(slug: str) -> Path: + if not _SAFE_SLUG.match(slug): + raise HTTPException(400, "Invalid page slug — only alphanumeric, hyphens, underscores, and slashes allowed") + resolved = (pages_dir / f"{slug}.md").resolve() + if not str(resolved).startswith(str(pages_dir.resolve())): + raise HTTPException(400, "Path traversal detected") + return resolved + + +class PageBody(BaseModel): + content: str + + +@app.get("/pages") +async def list_pages() -> JSONResponse: + """Return all wiki page slugs.""" + if not pages_dir.exists(): + return JSONResponse({"pages": []}) + slugs = [ + str(p.relative_to(pages_dir).with_suffix("")) + for p in sorted(pages_dir.rglob("*.md")) + ] + return JSONResponse({"pages": slugs}) + + +@app.get("/pages/{slug:path}") +async def get_page(slug: str) -> JSONResponse: + """Return raw markdown content for a page.""" + path = _slug_to_path(slug) + if not path.exists(): + raise HTTPException(404, "Page not found") + return JSONResponse({"slug": slug, "content": path.read_text(encoding="utf-8")}) + + +@app.post("/pages/{slug:path}") +async def save_page(slug: str, body: PageBody) -> JSONResponse: + """Create or update a wiki page.""" + path = _slug_to_path(slug) + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(body.content, encoding="utf-8") + return JSONResponse({"slug": slug, "saved": True}) + + +@app.delete("/pages/{slug:path}") +async def delete_page(slug: str) -> JSONResponse: + """Delete a wiki page.""" + path = _slug_to_path(slug) + if not path.exists(): + raise HTTPException(404, "Page not found") + path.unlink() + return JSONResponse({"slug": slug, "deleted": True}) + + +@app.post("/rebuild") +async def rebuild() -> JSONResponse: + """Trigger an astro build of the site.""" + try: + result = subprocess.run( + ["npm", "run", "build"], + cwd=site_dir, + capture_output=True, + text=True, + timeout=120, + ) + return JSONResponse({ + "success": result.returncode == 0, + "stdout": result.stdout[-3000:] if result.stdout else "", + "stderr": result.stderr[-3000:] if result.stderr else "", + }) + except subprocess.TimeoutExpired: + raise HTTPException(504, "Build timed out after 120s") + except Exception as e: + raise HTTPException(500, str(e)) diff --git a/scripts/build.sh b/scripts/build.sh new file mode 100755 index 0000000..1333959 --- /dev/null +++ b/scripts/build.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash +# Build the static wiki site and run Pagefind for search indexing. +# Usage: scripts/build.sh +set -e +cd "$(dirname "$0")/.." +ROOT="$(pwd)" + +# Symlink wiki pages into the site's content collection +mkdir -p pages +mkdir -p site/src/content/entries +LINK="$ROOT/site/src/content/entries/_wiki" +if [ ! -L "$LINK" ]; then + ln -sf "$ROOT/pages" "$LINK" + echo "Linked pages/ → site/src/content/entries/_wiki" +fi + +# Build Astro site +cd site && npm run build +cd "$ROOT" + +# Run Pagefind to index the static output +if command -v pagefind &>/dev/null; then + echo "Running Pagefind..." + pagefind --site site/dist +else + echo "Pagefind not installed — skipping search index (npm install -g pagefind)" +fi + +echo "Build complete: site/dist/" diff --git a/scripts/dev.sh b/scripts/dev.sh new file mode 100755 index 0000000..43f3688 --- /dev/null +++ b/scripts/dev.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash +# Start the Astro dev server and optionally the edit sidecar. +# Usage: scripts/dev.sh [--edit] +set -e +cd "$(dirname "$0")/.." +ROOT="$(pwd)" + +# Symlink wiki pages into the site's content collection +mkdir -p pages +mkdir -p site/src/content/entries +LINK="$ROOT/site/src/content/entries/_wiki" +if [ ! -L "$LINK" ]; then + ln -sf "$ROOT/pages" "$LINK" + echo "Linked pages/ → site/src/content/entries/_wiki" +fi + +# Start edit sidecar if requested +if [[ "$*" == *"--edit"* ]]; then + echo "Starting edit sidecar on :8001..." + (cd "$ROOT" && uvicorn edit.server:app --reload --port 8001) & + SIDECAR_PID=$! + trap "kill $SIDECAR_PID 2>/dev/null" EXIT +fi + +# Start Astro dev server +export PUBLIC_EDIT_URL="${WIKI_EDIT_URL:-}" +cd site && npm run dev diff --git a/site b/site new file mode 160000 index 0000000..2d51d17 --- /dev/null +++ b/site @@ -0,0 +1 @@ +Subproject commit 2d51d17f55d73f0ca0d2b74a21367784b4100974