Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
82 commits
Select commit Hold shift + click to select a range
0e0d28c
chore: bump root tooling metadata
martian56 Apr 19, 2026
53b315c
chore: expand gitignore for memory and agent folders
martian56 Apr 19, 2026
3ffd8c9
ci(api): stop swallowing pytest exit 5
martian56 Apr 19, 2026
5692008
ci(ui): run vitest in the UI pipeline
martian56 Apr 19, 2026
a9906b1
build: add MinIO service to dev docker-compose
martian56 Apr 19, 2026
5d10c72
build: add production docker-compose and env template
martian56 Apr 19, 2026
375b004
build(api): harden Dockerfile with multi-stage and non-root runtime
martian56 Apr 19, 2026
1087e5e
feat(ui): add production Dockerfile and nginx SPA config
martian56 Apr 19, 2026
0c31399
build(api): add boto3, python-multipart, boto3-stubs
martian56 Apr 19, 2026
3eec06b
docs(api): expand .env.example with auth and S3 settings
martian56 Apr 19, 2026
fe65ad1
feat(config): enforce production invariants and add S3 settings
martian56 Apr 19, 2026
5676ca9
feat(db): add UUID, CreatedAt, and Timestamped model mixins
martian56 Apr 19, 2026
6c360e7
feat(core): structured JSON logging with request context and Sentry hook
martian56 Apr 19, 2026
f2debeb
feat(middleware): add X-Request-ID middleware
martian56 Apr 19, 2026
6d2926c
refactor(core): frame Redis pub/sub messages with type-byte prefix
martian56 Apr 19, 2026
7740555
feat(core): add S3/MinIO storage client with presigned URLs
martian56 Apr 19, 2026
a2a031d
feat(core): pluggable email backend (console and SMTP)
martian56 Apr 19, 2026
dc8ff2e
feat(core): per-IP login rate limiter
martian56 Apr 19, 2026
6495bc6
feat(core): Redis-backed refresh token registry
martian56 Apr 19, 2026
8ee1931
feat(auth): split access and refresh JWT helpers
martian56 Apr 19, 2026
166803d
refactor(users): adopt model mixins, nullable names, avatar_key
martian56 Apr 19, 2026
4dcc231
refactor(workspaces): adopt mixins and set-null invited_by
martian56 Apr 19, 2026
428d359
refactor(boards): consolidate board models and adopt mixins
martian56 Apr 19, 2026
1eeb5f8
feat(users): presenter for display_name and avatar resolution
martian56 Apr 19, 2026
d90d234
feat(users): avatar upload/delete endpoints and display_name response
martian56 Apr 19, 2026
5425eca
feat(auth): password reset token flow
martian56 Apr 19, 2026
ee534db
feat(auth): email verification flow
martian56 Apr 19, 2026
a58b031
feat(auth): refresh, logout, password-reset, email-verify endpoints
martian56 Apr 19, 2026
6f2b1e6
refactor(workspaces): default workspace handles null user names
martian56 Apr 19, 2026
7414bdf
feat(workspaces): expose owner_display_name and avatar
martian56 Apr 19, 2026
a130b12
feat(workspaces): include member display_name and avatar
martian56 Apr 19, 2026
6de277b
feat(boards): repository for threaded comments
martian56 Apr 19, 2026
4c89a8e
feat(boards): repository for share tokens
martian56 Apr 19, 2026
1e04a8d
feat(boards): architecture-focused built-in templates
martian56 Apr 19, 2026
12cb979
feat(boards): share links, comments, templates, and search endpoints
martian56 Apr 19, 2026
e57a33d
feat(ws): first-frame auth handshake, binary relay, payload caps
martian56 Apr 19, 2026
9c5b4f4
feat(api): mount public board and templates routers
martian56 Apr 19, 2026
38e504d
feat(api): real /health, request-ID middleware, template seeding
martian56 Apr 19, 2026
ec6075f
build(alembic): register consolidated board and new auth models
martian56 Apr 19, 2026
7883b41
build(alembic): reset migrations and generate clean initial schema
martian56 Apr 19, 2026
37d626f
test(api): update health test for dependency-check payload
martian56 Apr 19, 2026
36f1bd8
test(config): production invariant tests
martian56 Apr 19, 2026
af77c35
test(auth): JWT round-trip and rejection paths
martian56 Apr 19, 2026
3ba51f8
test(security): bcrypt hash/verify edge cases
martian56 Apr 19, 2026
a1c5b96
test(ws): auth handshake rejection paths
martian56 Apr 19, 2026
4be9acd
build(ui): add yjs, vitest, and test config
martian56 Apr 19, 2026
8330133
feat(ui): add display_name and initials helpers
martian56 Apr 19, 2026
278b4ad
feat(ui): track refresh token and full profile in auth store
martian56 Apr 19, 2026
6143baa
feat(ui): add logoutAndClear, auto-refresh on 401, owner display fields
martian56 Apr 19, 2026
e0b4d2b
feat(ui): Yjs element bridge with clone-on-set
martian56 Apr 19, 2026
29de9ec
feat(ui): base64 Yjs state persistence helpers
martian56 Apr 19, 2026
5cad61b
refactor(ws): binary frames, event bus, client_id self-filter
martian56 Apr 19, 2026
7db3531
feat(ui): useBoardCollab for Yjs sync with undo manager
martian56 Apr 19, 2026
4f5841b
refactor(ui): extract useBoardInit hook
martian56 Apr 19, 2026
3516fd5
refactor(ui): scheduleSave accepts a lazy payload getter
martian56 Apr 19, 2026
bf45cec
feat(ui): useTemplates fetches built-in templates
martian56 Apr 19, 2026
8ec430c
feat(ui): useBoards.createBoardFromTemplate
martian56 Apr 19, 2026
87cb174
feat(ui): Avatar component with initial-bubble fallback
martian56 Apr 19, 2026
14124a4
feat(ui): Modal closes on Escape and focuses first input
martian56 Apr 19, 2026
df9f737
feat(ui): Dropdown trigger is a real button with Escape dismiss
martian56 Apr 19, 2026
a68a00b
feat(ui): forgot password page
martian56 Apr 19, 2026
29d7cd4
feat(ui): reset password page
martian56 Apr 19, 2026
e80966a
feat(ui): verify email page
martian56 Apr 19, 2026
5a73181
feat(ui): OAuth callback captures refresh token
martian56 Apr 19, 2026
e506e34
feat(ui): login page uses setTokens and forgot-password link
martian56 Apr 19, 2026
5d0eeea
feat(ui): export new auth pages
martian56 Apr 19, 2026
7aa16b9
feat(ui): board collab, native Excalidraw cursors, export, undo
martian56 Apr 19, 2026
0285f1e
feat(ui): public shared-board viewer page
martian56 Apr 19, 2026
de31bd2
feat(ui): share link dialog for boards
martian56 Apr 19, 2026
b00e060
feat(ui): threaded comments pane with live updates
martian56 Apr 19, 2026
b1400e6
refactor(ui): drop custom cursor overlay in favor of Excalidraw colla…
martian56 Apr 19, 2026
2de560c
feat(ui): account settings modal with avatar upload
martian56 Apr 19, 2026
137635b
feat(ui): global board search with Ctrl+K shortcut
martian56 Apr 19, 2026
576f84a
feat(ui): shell header uses avatar, display name, account modal
martian56 Apr 19, 2026
a2d2751
feat(ui): layout hydrates full user profile from /auth/me
martian56 Apr 19, 2026
b931f13
feat(ui): inline templates next to blank board and display-name swap
martian56 Apr 19, 2026
f6db2ab
feat(ui): recent and starred pages display owner_display_name
martian56 Apr 19, 2026
df253ac
feat(ui): workspace settings uses Avatar and display_name
martian56 Apr 19, 2026
039d89e
feat(ui): routes for shared board, verify email, forgot/reset password
martian56 Apr 19, 2026
58acc37
feat(i18n): add account settings profile key
martian56 Apr 19, 2026
38fce5a
fix: resolve github issues
martian56 Apr 19, 2026
83e6b01
fix: add vitest to the lock file
martian56 Apr 19, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 63 additions & 0 deletions .env.prod.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# Production environment template for docker-compose.prod.yml.
# Copy to `.env.prod`, fill in every required value, and make sure
# the real file never leaves your infrastructure.

# --- Postgres ---------------------------------------------------------
POSTGRES_USER=loomy
POSTGRES_PASSWORD=change-me-to-a-strong-random-password
POSTGRES_DB=loomy

# --- Redis ------------------------------------------------------------
REDIS_PASSWORD=change-me-to-another-strong-random-password

# --- API --------------------------------------------------------------
# >= 32 chars, generated with `openssl rand -hex 32` or similar.
SECRET_KEY=replace-with-32-plus-char-random-secret
# Public URLs (no trailing slash).
FRONTEND_URL=https://app.example.com
# Ports published on the host; adjust if you terminate TLS elsewhere.
API_PORT=8000
UI_PORT=80

# Token lifetimes (defaults shown).
ACCESS_TOKEN_EXPIRE_MINUTES=60
REFRESH_TOKEN_EXPIRE_DAYS=30

# --- OAuth (optional) -------------------------------------------------
GITHUB_CLIENT_ID=
GITHUB_CLIENT_SECRET=
GITHUB_REDIRECT_URI=https://api.example.com/api/auth/github/callback
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=
GOOGLE_REDIRECT_URI=https://api.example.com/api/auth/google/callback

# --- Email (required for password reset in production) ---------------
# Set EMAIL_BACKEND=smtp and fill the rest to actually deliver mail.
EMAIL_BACKEND=smtp
SMTP_HOST=smtp.example.com
SMTP_PORT=587
SMTP_USER=
SMTP_PASSWORD=
SMTP_USE_TLS=true
EMAIL_FROM_ADDRESS=no-reply@example.com
EMAIL_FROM_NAME=Loomy

# --- Object storage (required for user uploads) ---------------------
# Point at any S3-compatible endpoint (AWS S3, MinIO, Backblaze B2, etc.).
# Leave blank to disable user uploads (avatars, board logos, exports).
S3_ENDPOINT_URL=
S3_PUBLIC_ENDPOINT_URL=
S3_REGION=us-east-1
S3_ACCESS_KEY=
S3_SECRET_KEY=
S3_BUCKET=loomy-assets
S3_FORCE_PATH_STYLE=true
S3_PRESIGNED_URL_EXPIRE_SECONDS=3600

# --- Frontend build --------------------------------------------------
# Baked into the bundle at build time. Must be the public URL of the API.
VITE_API_URL=https://api.example.com

# --- Observability (optional) ----------------------------------------
LOG_LEVEL=INFO
SENTRY_DSN=
2 changes: 1 addition & 1 deletion .github/workflows/api-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,4 @@ jobs:
run: uv run mypy .

- name: Pytest
run: uv run pytest --tb=short -q || [ $? -eq 5 ]
run: uv run pytest --tb=short -q
3 changes: 3 additions & 0 deletions .github/workflows/ui-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,5 +38,8 @@ jobs:
- name: Format check
run: npm run format:check

- name: Test
run: npm run test

- name: Build
run: npm run build
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ scripts/
.cursor/
.vscode/
.claude/
.agents/
CLAUDE.md
memory/
.idea/
*.sublime-*

Expand Down
15 changes: 15 additions & 0 deletions api/app/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,18 @@ REDIS_URL=redis://localhost:6379/0

# Frontend URL
FRONTEND_URL=http://localhost:5173

# Object storage (MinIO in dev). Leave blank to disable uploads entirely.
# One bucket holds every app asset; keys are prefixed per kind:
# avatars/{user_id}/... (user profile photos)
# boards/{board_id}/logo/... (board logos, planned)
# boards/{board_id}/exports/... (rendered exports, planned)
# templates/{slug}/thumbnail.png (template previews, planned)
S3_ENDPOINT_URL=http://localhost:9000
S3_PUBLIC_ENDPOINT_URL=http://localhost:9000
S3_REGION=us-east-1
S3_ACCESS_KEY=minioadmin
S3_SECRET_KEY=minioadmin
S3_BUCKET=loomy-assets
S3_FORCE_PATH_STYLE=true
S3_PRESIGNED_URL_EXPIRE_SECONDS=3600
38 changes: 35 additions & 3 deletions api/app/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,18 +1,50 @@
FROM python:3.12-slim
# syntax=docker/dockerfile:1.7

# --- Stage 1: dependencies + build -----------------------------------
FROM python:3.12-slim AS builder

ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
UV_LINK_MODE=copy

WORKDIR /app

COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv

# Install system build deps only for the build stage; they're not
# copied into the runtime image.
RUN apt-get update \
&& apt-get install -y --no-install-recommends build-essential \
&& rm -rf /var/lib/apt/lists/*

COPY pyproject.toml uv.lock ./
RUN uv sync --frozen --no-dev --no-install-project

COPY . .

RUN uv sync --frozen --no-dev

ENV PATH="/app/.venv/bin:$PATH"
# --- Stage 2: lean runtime -------------------------------------------
FROM python:3.12-slim AS runtime

ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
PATH="/app/.venv/bin:$PATH"

# Non-root user so a broken process can't rewrite the image at runtime.
RUN groupadd --system --gid 1001 loomy \
&& useradd --system --uid 1001 --gid loomy --home /app loomy

WORKDIR /app

# Only copy what runtime needs — source + prebuilt venv.
COPY --from=builder --chown=loomy:loomy /app /app

USER loomy

EXPOSE 8000

HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \
CMD python -c "import urllib.request,sys;sys.exit(0 if urllib.request.urlopen('http://127.0.0.1:8000/health',timeout=3).status==200 else 1)" \
|| exit 1

CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
9 changes: 8 additions & 1 deletion api/app/alembic/env.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,14 @@

from app.config import settings
from app.db.base import Base
from app.modules.boards.model import Board, BoardStar, BoardView # noqa: F401 - metadata
from app.modules.boards.model import ( # noqa: F401 - metadata
Board,
BoardComment,
BoardShareToken,
BoardStar,
BoardTemplate,
BoardView,
)
from app.modules.elements.model import Element # noqa: F401 - metadata
from app.modules.users.model import OAuthAccount, User # noqa: F401 - metadata
from app.modules.workspaces.model import ( # noqa: F401 - metadata
Expand Down
Loading
Loading