diff --git a/.dockerignore b/.dockerignore index eca6c8fe8..32cca48f9 100644 --- a/.dockerignore +++ b/.dockerignore @@ -30,6 +30,8 @@ secrets.env~ .idea/ dev-docs/ docs/ +website/ +assets/branding/ *.md *.db *.sqlite diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index fc7545ace..26ddcd642 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -6,4 +6,4 @@ # A per-area ownership map (security/auth, CI, frontend, agent internals, with # multiple named owners per line) is being worked out in issue #593; once # agreed it replaces this file. Until then, required reviews and the security -# CI gate (docs/security-ci.md) remain in force via branch protection. +# CI gate (website/security-ci.md) remain in force via branch protection. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 38044e158..a276fdb1d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -110,7 +110,7 @@ jobs: fetch-depth: 0 persist-credentials: false - # Detect whether this PR only touches documentation files. + # Detect whether this PR only touches repository prose outside the Pages site. # If so, skip the expensive pytest run while still reporting a passing check. - name: Check for docs-only changes id: docs-check @@ -122,9 +122,10 @@ jobs: BASE="${{ github.event.before }}" HEAD="${{ github.sha }}" fi - # List all changed files; if every file matches docs/markdown patterns, skip pytest. + # Keep website/ and assets/branding/ out of this bypass: pytest owns + # regression guards for their published-file and orphan-asset contracts. changed=$(git diff --name-only "$BASE" "$HEAD" 2>/dev/null || git diff --name-only HEAD~1 HEAD) - non_docs=$(echo "$changed" | grep -Ev '^(docs/|.*\.md$|\.github/[^/]+\.md$)' || true) + non_docs=$(echo "$changed" | grep -Ev '^(docs/|[^/]+\.md$|\.github/[^/]+\.md$)' || true) if [ -z "$non_docs" ]; then echo "docs_only=true" >> "$GITHUB_OUTPUT" echo "Docs-only change detected — skipping pytest." diff --git a/.github/workflows/container-trivy.yml b/.github/workflows/container-trivy.yml index 8fabaae93..ad5674f18 100644 --- a/.github/workflows/container-trivy.yml +++ b/.github/workflows/container-trivy.yml @@ -23,12 +23,16 @@ on: paths-ignore: - '**.md' - 'docs/**' + - 'website/**' + - 'assets/branding/**' - '.github/ISSUE_TEMPLATE/**' push: branches: [main] paths-ignore: - '**.md' - 'docs/**' + - 'website/**' + - 'assets/branding/**' - '.github/ISSUE_TEMPLATE/**' workflow_dispatch: diff --git a/.github/workflows/deploy-pages.yml b/.github/workflows/deploy-pages.yml new file mode 100644 index 000000000..83f071e1c --- /dev/null +++ b/.github/workflows/deploy-pages.yml @@ -0,0 +1,50 @@ +name: Deploy GitHub Pages + +on: + push: + branches: [main] + paths: + - 'website/**' + - '.github/workflows/deploy-pages.yml' + workflow_dispatch: + +permissions: {} + +concurrency: + group: pages + cancel-in-progress: false + +jobs: + build: + name: Package static site + runs-on: ubuntu-latest + permissions: + contents: read + pages: read + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - uses: actions/configure-pages@45bfe0192ca1faeb007ade9deae92b16b8254a0d # v6.0.0 + - uses: actions/jekyll-build-pages@44a6e6beabd48582f863aeeb6cb2151cc1716697 # v1.0.13 + with: + source: website + destination: _site + - uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5.0.0 + with: + path: _site + + deploy: + name: Deploy static site + needs: build + runs-on: ubuntu-latest + permissions: + pages: write + id-token: write + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@cd2ce8fcbc39b97be8ca5fce6e763baed58fa128 # v5.0.0 diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index 8ce733b5a..9a5dd47cd 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -14,6 +14,8 @@ on: paths-ignore: - '**.md' - 'docs/**' + - 'website/**' + - 'assets/branding/**' - '.github/ISSUE_TEMPLATE/**' concurrency: diff --git a/.gitignore b/.gitignore index 77c364b8f..c50ba634d 100644 --- a/.gitignore +++ b/.gitignore @@ -85,6 +85,24 @@ output.txt.txt !docs/**/*.gif !docs/**/*.webp +# …except shipped website and branding media. +!website/**/*.jpg +!website/**/*.jpeg +!website/**/*.png +!website/**/*.gif +!website/**/*.bmp +!website/**/*.webp +!website/**/*.tiff +!website/**/*.pdf +!assets/branding/**/*.jpg +!assets/branding/**/*.jpeg +!assets/branding/**/*.png +!assets/branding/**/*.gif +!assets/branding/**/*.bmp +!assets/branding/**/*.webp +!assets/branding/**/*.tiff +!assets/branding/**/*.pdf + # Reports and temp files reports/ tasks/ diff --git a/README.md b/README.md index 4cc48f0d4..72bd1303c 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@

- Odysseus + Odysseus

@@ -8,7 +8,7 @@

Quick Start · - Setup Guide · + Setup Guide · Contributing · Roadmap

@@ -18,7 +18,7 @@

- Odysseus interface + Odysseus interface

--- @@ -36,7 +36,7 @@ docker compose up -d --build Open `http://localhost:7000` when the containers are healthy. The first admin password is printed in `docker compose logs odysseus`. -Native installs, GPU notes, Windows/macOS instructions, HTTPS, and configuration live in the [setup guide](docs/setup.md). +Native installs, GPU notes, Windows/macOS instructions, HTTPS, and configuration live in the [setup guide](website/setup.md). ## Features @@ -51,7 +51,7 @@ Native installs, GPU notes, Windows/macOS instructions, HTTPS, and configuration ## Demo -A full hover-to-play tour lives on the landing page: [`docs/index.html`](docs/index.html). +A full hover-to-play tour lives on the [Odysseus landing page](https://odysseus-dev.github.io/odysseus/). Its source lives under [`website/`](website/). ## Contributing @@ -64,7 +64,7 @@ Odysseus is a self-hosted workspace with powerful local tools. Keep auth enabled - Keep `AUTH_ENABLED=true` for any network-accessible deployment. - Keep `LOCALHOST_BYPASS=false` outside local development. -Deployment details are in the [setup guide](docs/setup.md#security-notes). +Deployment details are in the [setup guide](website/setup.md#security-notes). ## Star History diff --git a/docs/odysseus-browser.jpg b/assets/branding/odysseus-browser.jpg similarity index 100% rename from docs/odysseus-browser.jpg rename to assets/branding/odysseus-browser.jpg diff --git a/docs/odysseus-wordmark.png b/assets/branding/odysseus-wordmark.png similarity index 100% rename from docs/odysseus-wordmark.png rename to assets/branding/odysseus-wordmark.png diff --git a/docs/odysseus.jpg b/assets/branding/odysseus.jpg similarity index 100% rename from docs/odysseus.jpg rename to assets/branding/odysseus.jpg diff --git a/build-macos-app.sh b/build-macos-app.sh index c76075cac..7ea2c4b7f 100755 --- a/build-macos-app.sh +++ b/build-macos-app.sh @@ -27,13 +27,13 @@ echo " port: $PORT" rm -rf "$APP" mkdir -p "$APP/Contents/MacOS" "$APP/Contents/Resources" -# ── Icon (best effort) — center-crop docs/odysseus.jpg to a square .icns ── -if [ -f "$REPO_DIR/docs/odysseus.jpg" ] && command -v sips >/dev/null 2>&1; then +# ── Icon (best effort) — center-crop the branding image to a square .icns ── +if [ -f "$REPO_DIR/assets/branding/odysseus.jpg" ] && command -v sips >/dev/null 2>&1; then TMPIMG="$(mktemp -d)" # Center-crop to a square, scale to 512 (sips' icns encoder caps at 512), and # let sips emit the .icns directly — more robust across macOS versions than # building an .iconset by hand. - sips -c 720 720 "$REPO_DIR/docs/odysseus.jpg" --out "$TMPIMG/sq.png" >/dev/null 2>&1 || cp "$REPO_DIR/docs/odysseus.jpg" "$TMPIMG/sq.png" + sips -c 720 720 "$REPO_DIR/assets/branding/odysseus.jpg" --out "$TMPIMG/sq.png" >/dev/null 2>&1 || cp "$REPO_DIR/assets/branding/odysseus.jpg" "$TMPIMG/sq.png" sips -z 512 512 "$TMPIMG/sq.png" --out "$TMPIMG/icon.png" >/dev/null 2>&1 if sips -s format icns "$TMPIMG/icon.png" --out "$APP/Contents/Resources/odysseus.icns" >/dev/null 2>&1; then echo " icon: odysseus.icns" @@ -42,7 +42,7 @@ if [ -f "$REPO_DIR/docs/odysseus.jpg" ] && command -v sips >/dev/null 2>&1; then fi rm -rf "$TMPIMG" else - echo " icon: (skipped — no docs/odysseus.jpg)" + echo " icon: (skipped — no assets/branding/odysseus.jpg)" fi # ── Info.plist ── diff --git a/scripts/encode_previews.sh b/scripts/encode_previews.sh index 1d8a51466..47cb47b75 100755 --- a/scripts/encode_previews.sh +++ b/scripts/encode_previews.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash # Encode a source screen-recording (.mkv) into web-optimized preview clips for -# the landing page: docs/.webm (VP9) + docs/.mp4 (H.264). +# the landing page: website/.webm (VP9) + website/.mp4 (H.264). # # ./encode_previews.sh [max_secs] # @@ -13,7 +13,7 @@ set -euo pipefail IN="${1:?input file}" NAME="${2:?output basename}" MAX="${3:-30}" -OUT_DIR="$(cd "$(dirname "$0")/../docs" && pwd)" +OUT_DIR="$(cd "$(dirname "$0")/../website" && pwd)" dur=$(ffprobe -v error -show_entries format=duration -of csv=p=0 "$IN" | cut -d. -f1) dur=${dur:-0} diff --git a/tests/test_docs_no_orphan_images.py b/tests/test_docs_no_orphan_images.py index a8f8a4331..f6ed43560 100644 --- a/tests/test_docs_no_orphan_images.py +++ b/tests/test_docs_no_orphan_images.py @@ -1,30 +1,39 @@ -"""Regression guard for issue #1335 — PR review screenshots were committed into -docs/ (docs/a11y/*.png from #738, docs/gallery-314-*.png from #644) where they -served no purpose: nothing in the repo referenced them, so they just showed up -as "random images" in the doc folder. +"""Repository asset ownership guards for issues #1335 and #6175. -This test fails if any image under docs/ is orphaned — present in the tree but -referenced by no tracked text file. The intended doc assets (the README hero -image and the feature preview clips) are referenced, so they pass; a stray -screenshot dropped in by a future PR would not. +Public Markdown and landing-page media belong in website/, while shared +README/packaging imagery belongs in assets/branding/. Images in either managed +root must be referenced by tracked text, and every tracked website video must +be referenced by the site's entry point. """ +import re import subprocess from pathlib import Path +from urllib.parse import urlsplit import pytest REPO = Path(__file__).resolve().parent.parent IMAGE_EXTS = {".png", ".jpg", ".jpeg", ".gif", ".webp", ".bmp"} +VIDEO_EXTS = {".webm", ".mp4", ".mov", ".m4v"} +PUBLIC_GUIDES = { + "agent-migration.md", + "attachments.md", + "backup-restore.md", + "email-outlook.md", + "pr-blocker-audit.md", + "security-ci.md", + "setup.md", +} # Files a referenced image name could legitimately appear in. TEXT_EXTS = {".md", ".html", ".htm", ".js", ".ts", ".css", ".py", ".sh", ".json", ".yml", ".yaml", ".txt"} -def _tracked(paths_under): - """Git-tracked files under a path, or None if git isn't available.""" +def _tracked(*paths_under): + """Git-tracked files under paths, or None if git isn't available.""" try: out = subprocess.run( - ["git", "ls-files", paths_under], + ["git", "ls-files", "--", *paths_under], cwd=REPO, capture_output=True, text=True, timeout=30, ) except (OSError, subprocess.SubprocessError): @@ -34,12 +43,14 @@ def _tracked(paths_under): return [REPO / line for line in out.stdout.splitlines() if line.strip()] -def test_no_orphan_images_in_docs(): - docs_images = _tracked("docs") - if docs_images is None: +def test_no_orphan_documentation_or_branding_images(): + managed_files = _tracked("website", "assets/branding") + if managed_files is None: pytest.skip("not a git checkout") - docs_images = [p for p in docs_images if p.suffix.lower() in IMAGE_EXTS] - assert docs_images, "expected docs/ to still contain referenced doc assets" + managed_images = [p for p in managed_files if p.suffix.lower() in IMAGE_EXTS] + assert any("assets/branding" in p.as_posix() for p in managed_images), ( + "expected assets/branding/ to contain the shared project imagery" + ) # All tracked text we might reference an image from. all_tracked = _tracked(".") or [] @@ -55,10 +66,117 @@ def test_no_orphan_images_in_docs(): orphans = [ str(img.relative_to(REPO)) - for img in docs_images + for img in managed_images if img.name not in blob ] assert not orphans, ( - "unreferenced image(s) committed under docs/ — likely PR screenshots " - f"added by accident (see #1335): {orphans}" + "unreferenced image(s) committed under website/ or assets/branding/ " + f"(see #1335 and #6175): {orphans}" ) + + +def test_pages_site_owns_its_entrypoint_and_media(): + docs_files = _tracked("docs") + website_files = _tracked("website") + if docs_files is None or website_files is None: + pytest.skip("not a git checkout") + + assert REPO / "website/index.html" in website_files + assert REPO / "docs/index.html" not in docs_files + assert not [p for p in docs_files if p.suffix.lower() in VIDEO_EXTS | {".md"}] + + website_paths = {p.relative_to(REPO / "website").as_posix() for p in website_files} + assert PUBLIC_GUIDES <= website_paths + for guide in PUBLIC_GUIDES: + text = (REPO / "website" / guide).read_text(encoding="utf-8") + assert text.startswith("---\nlayout: default\n---\n"), guide + + website_videos = [p for p in website_files if p.suffix.lower() in VIDEO_EXTS] + assert website_videos, "expected website/ to contain the landing-page videos" + + entrypoint = (REPO / "website/index.html").read_text(encoding="utf-8") + unreferenced = [ + str(video.relative_to(REPO)) + for video in website_videos + if video.name not in entrypoint + ] + assert not unreferenced, f"unreferenced website video(s): {unreferenced}" + + workflow = (REPO / ".github/workflows/deploy-pages.yml").read_text(encoding="utf-8") + assert "actions/jekyll-build-pages@" in workflow + assert "source: website" in workflow + assert "destination: _site" in workflow + assert "path: _site" in workflow + assert "cancel-in-progress: false" in workflow + + +def test_pages_guides_keep_relative_links_inside_site(): + site_root = (REPO / "website").resolve() + + for guide in sorted(PUBLIC_GUIDES): + guide_path = REPO / "website" / guide + text = guide_path.read_text(encoding="utf-8") + for target in re.findall(r"\]\(([^)]+)\)", text): + parsed = urlsplit(target) + if parsed.scheme or parsed.netloc or not parsed.path: + continue + + resolved = (guide_path.parent / parsed.path).resolve() + assert resolved.is_relative_to(site_root), ( + f"{guide} links outside the Pages source: {target}" + ) + assert resolved.exists(), f"{guide} has a missing local link: {target}" + + +def test_setup_preserves_docker_go_template_literal(): + setup = (REPO / "website/setup.md").read_text(encoding="utf-8") + guarded_command = """ +```bash +docker info --format '{{.DockerRootDir}}' +``` +""" + + assert guarded_command in setup + + +def test_preview_encoder_targets_pages_source(): + encoder = (REPO / "scripts/encode_previews.sh").read_text(encoding="utf-8") + + assert "landing page: website/.webm" in encoder + assert 'OUT_DIR="$(cd "$(dirname "$0")/../website" && pwd)"' in encoder + + +def test_ci_runs_asset_ownership_guards_for_managed_roots(): + workflow = (REPO / ".github/workflows/ci.yml").read_text(encoding="utf-8") + match = re.search(r"grep -Ev '([^']+)'", workflow) + assert match, "expected the docs-only path classifier in CI" + docs_only = re.compile(match.group(1)) + + assert docs_only.match("README.md") + assert docs_only.match("docs/example.md") + assert not docs_only.match("website/setup.md") + assert not docs_only.match("website/new-preview.webm") + assert not docs_only.match("assets/branding/new-logo.png") + + +@pytest.mark.parametrize( + "path", + [ + "website/favicon.png", + "website/media/social-card.jpg", + "website/guides/reference.pdf", + "assets/branding/new-logo.gif", + "assets/branding/print/logo.tiff", + ], +) +def test_managed_site_media_is_not_ignored(path): + result = subprocess.run( + ["git", "check-ignore", "--no-index", "--quiet", path], + cwd=REPO, + capture_output=True, + text=True, + timeout=30, + ) + if result.returncode == 128: + pytest.skip("not a git checkout") + assert result.returncode == 1, f"{path} is unexpectedly ignored" diff --git a/tests/test_kokoro_optional_requirements.py b/tests/test_kokoro_optional_requirements.py index 53e670a03..8e9c70ce3 100644 --- a/tests/test_kokoro_optional_requirements.py +++ b/tests/test_kokoro_optional_requirements.py @@ -30,7 +30,7 @@ def test_kokoro_feature_markers_match_supported_python_range(python_version, sel def test_setup_documents_container_constraint_and_install_command(): - setup = (ROOT / "docs" / "setup.md").read_text(encoding="utf-8") + setup = (ROOT / "website" / "setup.md").read_text(encoding="utf-8") assert "pip install -r requirements-optional.txt" in setup assert "default Docker image currently uses Python 3.14" in setup diff --git a/tests/test_security_regressions.py b/tests/test_security_regressions.py index be8d3b8a3..1b467c6b4 100644 --- a/tests/test_security_regressions.py +++ b/tests/test_security_regressions.py @@ -123,10 +123,10 @@ def test_docker_compose_binds_web_ui_to_loopback_by_default(): def test_readme_native_quickstart_uses_loopback(): - # The README refresh (#4306) moved the native quickstart into docs/setup.md, + # The Pages source split (#6175) moved the native quickstart into website/setup.md, # so accept the loopback guidance from either the README or the setup guide. docs = Path("README.md").read_text(encoding="utf-8") - docs += "\n" + Path("docs/setup.md").read_text(encoding="utf-8") + docs += "\n" + Path("website/setup.md").read_text(encoding="utf-8") assert "python -m uvicorn app:app --host 127.0.0.1 --port 7000" in docs assert "0.0.0.0` only when you intentionally want" in docs diff --git a/tests/test_setup_admin_user.py b/tests/test_setup_admin_user.py index b0fde4d75..7e6959577 100644 --- a/tests/test_setup_admin_user.py +++ b/tests/test_setup_admin_user.py @@ -29,7 +29,7 @@ def test_create_default_admin_normalizes_env_username(tmp_path, monkeypatch): def test_main_loads_admin_password_from_env_file(tmp_path, monkeypatch): """Regression: setup.py must honor an admin password pre-seeded in .env on native installs, even when the var is not exported into the shell - (docs/setup.md documents this). Previously setup.py never called + (website/setup.md documents this). Previously setup.py never called load_dotenv(), so os.getenv() saw nothing and a random password was generated instead.""" import bcrypt diff --git a/website/_config.yml b/website/_config.yml new file mode 100644 index 000000000..f383681ec --- /dev/null +++ b/website/_config.yml @@ -0,0 +1,10 @@ +title: Odysseus +description: A self-hosted AI workspace for chat, agents, tools, and local models. +theme: jekyll-theme-primer + +plugins: + - jekyll-relative-links + +relative_links: + enabled: true + collections: true diff --git a/docs/agent-migration.md b/website/agent-migration.md similarity index 99% rename from docs/agent-migration.md rename to website/agent-migration.md index ff082159e..7abf776ab 100644 --- a/docs/agent-migration.md +++ b/website/agent-migration.md @@ -1,3 +1,7 @@ +--- +layout: default +--- + # Agent migration manifests Odysseus should be able to learn from another agent without blindly trusting diff --git a/docs/attachments.md b/website/attachments.md similarity index 99% rename from docs/attachments.md rename to website/attachments.md index 93f9e0ffe..d622f9050 100644 --- a/docs/attachments.md +++ b/website/attachments.md @@ -1,3 +1,7 @@ +--- +layout: default +--- + # Attachment References and Upload Storage Odysseus stores uploaded bytes once under the configured upload directory and diff --git a/docs/backup-restore.md b/website/backup-restore.md similarity index 99% rename from docs/backup-restore.md rename to website/backup-restore.md index 902c9e683..424b8bd59 100644 --- a/docs/backup-restore.md +++ b/website/backup-restore.md @@ -1,3 +1,7 @@ +--- +layout: default +--- + # Backup & Restore Odysseus keeps all of your state in the `data/` directory — the SQLite database diff --git a/docs/bg.webm b/website/bg.webm similarity index 100% rename from docs/bg.webm rename to website/bg.webm diff --git a/docs/chat.webm b/website/chat.webm similarity index 100% rename from docs/chat.webm rename to website/chat.webm diff --git a/docs/compare.webm b/website/compare.webm similarity index 100% rename from docs/compare.webm rename to website/compare.webm diff --git a/docs/document.webm b/website/document.webm similarity index 100% rename from docs/document.webm rename to website/document.webm diff --git a/docs/email-outlook.md b/website/email-outlook.md similarity index 97% rename from docs/email-outlook.md rename to website/email-outlook.md index 1f8b97d5d..0646d39ba 100644 --- a/docs/email-outlook.md +++ b/website/email-outlook.md @@ -1,3 +1,7 @@ +--- +layout: default +--- + # Outlook / Office 365 email accounts Odysseus email accounts currently use IMAP and SMTP with username/password diff --git a/docs/gallery.webm b/website/gallery.webm similarity index 100% rename from docs/gallery.webm rename to website/gallery.webm diff --git a/docs/index.html b/website/index.html similarity index 100% rename from docs/index.html rename to website/index.html diff --git a/docs/notes.webm b/website/notes.webm similarity index 100% rename from docs/notes.webm rename to website/notes.webm diff --git a/docs/pr-blocker-audit.md b/website/pr-blocker-audit.md similarity index 99% rename from docs/pr-blocker-audit.md rename to website/pr-blocker-audit.md index b56f28cb3..0a9258f15 100644 --- a/docs/pr-blocker-audit.md +++ b/website/pr-blocker-audit.md @@ -1,3 +1,7 @@ +--- +layout: default +--- + # PR Blocker Audit `scripts/pr_blocker_audit.py` is a small, read-only triage helper for maintainers who need to inspect open pull request overlap before reviewing or starting related work. diff --git a/docs/research.webm b/website/research.webm similarity index 100% rename from docs/research.webm rename to website/research.webm diff --git a/docs/security-ci.md b/website/security-ci.md similarity index 99% rename from docs/security-ci.md rename to website/security-ci.md index 8cceea258..1e68e7db7 100644 --- a/docs/security-ci.md +++ b/website/security-ci.md @@ -1,3 +1,7 @@ +--- +layout: default +--- + # Security CI guide This project runs a set of automated security checks on pull requests and diff --git a/docs/setup.md b/website/setup.md similarity index 99% rename from docs/setup.md rename to website/setup.md index 523dd41d7..aeaf6baf6 100644 --- a/docs/setup.md +++ b/website/setup.md @@ -1,3 +1,7 @@ +--- +layout: default +--- + # Odysseus Setup Guide This page keeps the detailed install, deployment, troubleshooting, and configuration notes out of the front README. @@ -15,8 +19,7 @@ On first setup, Odysseus creates an admin account (`admin` unless For Docker installs, the same line is in `docker compose logs odysseus`. Use that for the first login, then change it in **Settings**. -Contributing? See [CONTRIBUTING.md](../CONTRIBUTING.md) for setup, testing, and -pull request guidelines. +Contributing? See [CONTRIBUTING.md](https://github.com/odysseus-dev/odysseus/blob/dev/CONTRIBUTING.md) for setup, testing, and pull request guidelines. ### Docker (recommended) ```bash @@ -205,9 +208,11 @@ failed to fulfil mount request: open /usr/lib/wsl/lib/libdxcore.so: no such file Check with `snap list docker` or: + ```bash docker info --format '{{.DockerRootDir}}' ``` + A Docker root under `/var/snap/docker/` means snap confinement can prevent Docker from seeing WSL2's `/usr/lib/wsl/lib` GPU libraries even when the files @@ -476,7 +481,7 @@ uv pip sync requirements.lock # reproduce it exactly la ### Outlook / Office 365 email Odysseus email accounts currently use IMAP/SMTP username-password auth. Outlook and Microsoft 365 generally require OAuth instead, so normal Microsoft mailbox -passwords will fail. See [docs/email-outlook.md](docs/email-outlook.md) for the +passwords will fail. See [the Outlook email guide](email-outlook.md) for the current limitation and the planned integration direction. ## Security Notes @@ -733,7 +738,7 @@ src/ llm_core, agent_loop, agent_tools, chat_processor, search/ routes/ chat, session, document, memory, model … endpoints services/ docs, memory, search, hwfit (Cookbook) … static/ index.html + app.js + style.css + js/ (modular front-end) -docs/ landing page (index.html) + preview clips +website/ landing page (index.html) + preview clips ``` ## Data diff --git a/docs/theme.webm b/website/theme.webm similarity index 100% rename from docs/theme.webm rename to website/theme.webm