Skip to content

feat(sync): Syncthing-based team session sharing#45

Open
JayantDevkar wants to merge 235 commits intomainfrom
worktree-syncthing-sync-design
Open

feat(sync): Syncthing-based team session sharing#45
JayantDevkar wants to merge 235 commits intomainfrom
worktree-syncthing-sync-design

Conversation

@JayantDevkar
Copy link
Owner

@JayantDevkar JayantDevkar commented Mar 4, 2026

Summary

Add real-time session sharing between team members using Syncthing as the sync layer. Team members share Claude Code sessions automatically — no manual sync commands needed.

How it works

  1. karma init — detects Syncthing automatically, saves device ID
  2. karma team create <name> — creates a team and generates a join code
  3. Share the join code — teammates run karma team join <code> to connect
  4. karma watch — watches for new sessions and packages them into shared Syncthing folders
  5. Sessions appear on teammates' dashboards via the /team page

What's in this PR (112 commits)

CLI (cli/)

  • init, team create/join/leave, project add/remove, watch, accept, status commands
  • Session packager that bundles JSONL sessions into manifests for sharing
  • Syncthing client wrapper for device/folder management
  • Watcher that auto-packages sessions on file changes
  • Worktree discovery so Desktop worktree sessions get included

API (api/)

  • SQLite tables for teams, members, projects, and sync events
  • Team CRUD endpoints under /sync/
  • Syncthing proxy service for device/folder/bandwidth queries
  • Remote session reader that serves synced sessions from other team members
  • Session filtering by source (local vs remote)

Frontend (frontend/)

  • /team page with setup wizard, team cards, and activity feed
  • /team/[name] detail page showing members, projects, and connection status
  • Sync status indicators and Syncthing detection UI

Cleanup (latest commit)

  • Removed all IPFS code (was an unused alternative backend) — 5,144 lines deleted
  • Simplified to Syncthing-only: no --backend flag, auto-detection instead
  • Removed ipns_key from DB, previous_cid/sync_backend from manifests

147 files changed, ~30k additions, ~500 deletions

Test plan

  • CLI tests pass (125/126, 1 pre-existing integration test)
  • API tests pass (1,481/1,491, 10 pre-existing proxy/path failures)
  • Verified IPFS references only remain in historical design docs
  • Manual test: full join flow between two machines
  • Manual test: watch + auto-sync sessions end-to-end

🤖 Generated with Claude Code

@JayantDevkar JayantDevkar changed the title feat: Syncthing session sync — pluggable backend for real-time team sharing feat: IPFS + Syncthing session sync — dual-backend team sharing Mar 4, 2026
@JayantDevkar JayantDevkar force-pushed the worktree-syncthing-sync-design branch 2 times, most recently from b111b2b to 2f26dc7 Compare March 5, 2026 19:56
JayantDevkar and others added 27 commits March 6, 2026 22:33
Design and plan for enabling cross-system Claude Code session sharing
via a private IPFS cluster. This lets project owners monitor freelancers'
Claude Code usage from a central Karma dashboard.

Key decisions:
- Private IPFS cluster (Kubo) with swarm key for access control
- New `karma` CLI (Python/click) for sync operations
- IPNS for mutable pointers to latest synced sessions
- Subprocess wrapper around ipfs binary (Python IPFS libs unmaintained)
- New /remote/* API endpoints + frontend Team section

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…m views

Add cross-system session sharing via private IPFS cluster so project owners
can monitor freelancers' Claude Code usage from a central Karma dashboard.

CLI package (cli/):
- `karma init` — user identity setup with machine_id auto-detection
- `karma project add/list/remove` — manage projects for syncing
- `karma sync [name] / --all` — package sessions as IPFS DAG, pin, publish to IPNS
- `karma pull` — fetch remote sessions from team members' IPNS keys
- `karma ls` — list pulled remote session data
- `karma team add/list/remove` — manage team members by IPNS key
- IPFSClient subprocess wrapper for Kubo CLI (add, get, pin, name publish/resolve)
- SessionPackager collects JSONL + subagents + tool-results into staging dir
- SyncManifest model with CID chaining for history
- Input validation: safe names, absolute paths, path escape guards
- 28 tests (config, IPFS wrapper, packager, CLI commands, integration flows)

API remote sessions router (api/routers/remote_sessions.py):
- GET /remote/users — list synced freelancers with project/session counts
- GET /remote/users/{user_id}/projects — user's synced projects
- GET /remote/users/{user_id}/projects/{project}/sessions — session list
- GET /remote/users/{user_id}/projects/{project}/manifest — full manifest
- Path traversal protection via regex validation on URL params
- Safe filesystem iteration for listing endpoints (no HTTPException on bad dir names)
- Malformed manifest handling returns 422 instead of 500
- 4 tests (manifest parsing, router helpers)

Frontend team section:
- "Team" nav link in Header (desktop + mobile)
- /team — user cards grid with session/project counts, empty state with CLI instructions
- /team/[user_id] — project list with sync timestamps and machine IDs
- Uses existing design system (PageHeader, design tokens, lucide icons)

Design doc: docs/plans/2026-03-03-ipfs-session-sync-design.md (committed previously)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Address code review findings across CLI, API, and frontend:

**CRITICAL security fixes:**
- Prevent command/flag injection in IPFS subprocess calls (ipfs.py):
  add `--` separator before positional args, validate CID format,
  validate key names, reject inputs starting with `-`
- Prevent path traversal via malicious IPFS content filenames (sync.py):
  validate filenames against safe regex, skip symlinks, verify
  dest.resolve() stays inside member_dir

**HIGH fixes:**
- Add input validation: user_id in `karma init`, ipns_key in `karma team add`
  with both CLI-level checks and Pydantic field_validators (config.py, main.py)
- Fix blocking I/O in async API handlers — change `async def` to `def` so
  FastAPI runs filesystem ops in threadpool (remote_sessions.py)
- Fix manifest leaking internal Claude dir path — pass original project
  path from config instead (packager.py, sync.py)

**MEDIUM fixes:**
- Allow dots in _SAFE_NAME regex for encoded paths with dots (remote_sessions.py)
- Unify _is_safe_dirname with _SAFE_NAME regex (remote_sessions.py)
- Switch frontend from fetchWithFallback to safeFetch pattern with
  error propagation to UI (team +page.server.ts files)
- Add breadcrumbs to team pages, remove unused FolderGit2 import (+page.svelte)
- Wrap multi-project sync loop in try/except per project (main.py)
- Set config file permissions to 0600 (config.py)

**LOW fixes:**
- Add 120s timeout to all subprocess calls (ipfs.py)
- Rename misleading test_incremental_skips_already_synced (test_packager.py)

All 32 tests passing (28 CLI + 4 API).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Covers sync health dashboard, per-machine IPNS keys, SQLite
sync_history table, new API endpoints, and CLI enhancements.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Covers SQLite schema v10, CLI enhancements (per-machine IPNS keys,
karma status, sync history recording), /sync API router, and
SvelteKit /sync page with four-zone dashboard.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Fixes across all 7 tasks to align plan with actual codebase patterns:

CLI (Tasks 2-4):
- Restructure tests to class-based (TestInitCommand, TestStatusCommand)
  using existing runner/init_config fixtures
- Replace __init__ override with @model_validator(mode="after") for
  Pydantic v2 frozen model compatibility
- Fix monkeypatch path: karma.config.KARMA_BASE (not karma.history)
- Add missing `from pathlib import Path` in pull command code
- Rename _get_db_path → get_db_path (public, matches test import)
- Record per-project pull events from manifest data (not project="all")

API (Task 5):
- Add `from __future__ import annotations` for Python 3.9 compat
  (bool | None union syntax requires 3.10+)
- Add _check_ipfs_health() function (was returning None for IPFS fields)
- Query sync_history for actual synced counts (not approximation)
- Rewrite main.py instructions with complete import block and correct
  line numbers

Frontend (Task 6):
- Complete truncated Header.svelte code — full desktop + mobile nav links
  with correct line numbers and matching class patterns
- Remove +layout.svelte from files list (no skeleton needed)
- Add 3-state IPFS display: running/degraded/not running
- Add CID display and "karma sync" hint for never-synced projects

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Fixes from Explorer/Architect/Critic triple review:
- Monkeypatch target: karma.history.KARMA_BASE (not karma.config)
- WAL pragma on all sqlite3.connect() in sync router
- Migration test for v9→v10 incremental path
- API endpoint tests for /sync/projects, /team, /history
- Accurate last_pull_at from sync_history (not manifest)
- Human-readable project names in team zone
- CLI karma status shows unpushed count matching design doc

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…h, 4 medium)

Security fixes:
- Fix ipfs get argument order: -o must come before -- separator
- Add -- end-of-options to pin_add and name_publish
- Block path traversal via ".." in API remote sessions router
- Use copytree(symlinks=False) to prevent nested symlink attacks in pull
- Filter unsafe directory names in list_remote_users output

Robustness fixes:
- Add error handling for corrupt config JSON in SyncConfig.load()
- Catch ValidationError in list_user_sessions endpoint
- Handle corrupt manifest.json gracefully in karma ls command
- Skip 0-byte JSONL files in session packager

Cleanup:
- Remove unused pydantic-settings dependency

Tests:
- Add 7 new tests for sync, pull, corrupt config, and ls commands

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
… IPFS

Syncthing provides real-time automatic sync for trusted teams as an alternative
to the on-demand IPFS approach. Both backends produce the same data format so
the dashboard API reads them identically. Supports per-team backend selection,
bidirectional sync, and Device ID-based onboarding.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
8-task plan covering manifest extension, per-team config, SyncthingClient,
SessionWatcher, CLI commands (init --backend, team create, watch, status),
and API sync status endpoints. All tasks follow TDD with exact file paths
and code.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Critic review found 6 critical and 4 non-critical issues. All addressed:
- Task 1 converted to verification-only (already implemented)
- Union type replaced with separate ipfs_members/syncthing_members dicts
- Added requests>=2.28 to pyproject.toml dependencies
- All str|None changed to Optional[str] for Python 3.9 compat
- project add --team moved before watch tests that depend on it
- API import style matched to existing codebase pattern
- RemoteManifest extended with sync_backend field
- Watcher exception logging instead of silent swallow

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Task 1: Add sync_backend field to SyncManifest
- Task 2: Add per-team config models (TeamConfig, SyncthingSettings,
  TeamMemberSyncthing) with separate ipfs_members/syncthing_members
  dicts and unified members property
- Task 3: Add SyncthingClient REST API wrapper (device/folder mgmt)
- Task 4: Add SessionWatcher with debounced filesystem monitoring

All 60 tests pass. Python 3.9 compatible (Optional[str] not str|None).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add init --backend, team create/add, project add --team, watch, and
status commands with full test coverage (14 tests, all passing).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add sync status router with config-based team/backend info.
Extend RemoteManifest with sync_backend field for Syncthing manifests.
5 new API tests, all passing.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Fix sync_status.py member count using ipfs_members/syncthing_members
  instead of nonexistent "members" key
- Add --team flag to project remove and team remove commands
- Remove dead isinstance check in watcher.py on_modified
- Remove redundant Path import in main.py project_add
- Update API tests to use actual serialized config format
- Add 5 new CLI tests for remove commands

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Router tests importing fastapi were in tests/ (non-API job) causing
ModuleNotFoundError in CI. Moved to tests/api/ and removed unused
pytest import from test_sync_status.py.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…-outbox/

The karma watch command was writing to sync-outbox/{team}/{user}/{project}/
but the API reads from remote-sessions/{user}/{project}/. This meant
Syncthing-synced data never appeared in the dashboard. Changed the output
path to remote-sessions/ so both IPFS and Syncthing converge to the same
directory the API reads from. Updated all docs to reflect the change.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…sion

Code review of dc9d7eb found two issues:

1. Design doc had 6 stale sync-outbox/ path references (lines 39, 75,
   115, 181, 208, 330) that contradicted the update notice at line 427.
   An operator following the Syncthing folder table would configure
   Syncthing to point at a directory karma watch no longer writes to.
   Updated all references to remote-sessions/{user-id}/ and consolidated
   the duplicate directory tree.

2. cli/karma/main.py watch command accepts --team but team_name was
   silently dropped from the output path with no explanation. Added
   comment documenting this is intentional (user_id provides namespace
   isolation, both backends converge on same path).

Also updated the plan doc's code snippet to match current implementation.

IPFS backend verified clean — no equivalent bug found.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add full-stack support for displaying Syncthing-synced remote sessions
alongside local sessions with visual distinction and filtering.

API:
- New remote_sessions.py service with project mapping, session lookup,
  and metadata iteration for synced sessions
- SessionSummary schema extended with source, remote_user_id,
  remote_machine_id fields
- SessionFilter gains SessionSource enum for all/local/remote filtering
- Session lookup falls back to remote-sessions/ when local not found
- SQLite schema v11: source columns + index on sessions table
- Indexer walks remote-sessions dirs; queries support source filter
- Source param wired through /sessions/all endpoint (SQLite + JSONL paths)
- Project detail deduplicates remote sessions against SQLite-indexed ones

Frontend:
- Team member color system (8-color palette, hash-based assignment)
- SessionCard/GlobalSessionCard: Globe badge + colored left border
  for remote sessions
- Source filter (All/Local/Remote) in FilterControls, search.ts,
  and both sessions/project pages
- CSS custom properties for team colors (light + dark mode)

Tests: 22 new tests covering service functions, filtering, schema migration

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- `karma init --backend syncthing` now saves api_key and device_id
  to sync-config.json (previously discarded after display)
- `karma team add` auto-pairs the remote device in Syncthing and
  creates shared folders for all team projects
- `karma project add` auto-creates shared folder when team already
  has Syncthing members
- Zero Syncthing UI interaction needed — one Device ID exchange
  between users is the only manual step

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Each user gets their own send-only outbox and receive-only inbox
with unique folder IDs (karma-out-{user_id}-{project}). This
prevents Syncthing from merging sessions from different users
into the same directory.

- Outbox: send-only, my sessions → teammates
- Inbox: receive-only, teammate sessions → my machine
- Same folder ID on both machines = Syncthing knows to sync them
- Different folder types = one-way flow, no mixing

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Main branch is at v10 but live DB was at v16 from other branches.
Migration guard `< 11` was never triggered. Updated to `< 17`
so the remote session columns (source, remote_user_id,
remote_machine_id) are correctly added on existing databases.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
JayantDevkar and others added 30 commits March 10, 2026 22:24
…bers

reconcile_introduced_devices was extracting the username from receiveonly
inbox folder IDs (e.g. karma-out--jay-mac-mini--suffix), but that name
belongs to the folder OWNER, not the introduced device. This caused new
members to be stored with the wrong name (duplicate of existing member).

- Use Syncthing device name for introduced devices instead of folder-parsed
  username in receiveonly outbox folders
- add_member and upsert_member now remove stale rows where the same name
  maps to a different device_id (handles Syncthing identity changes)
- Frontend {#each} key changed from member.name to member.device_id to
  prevent duplicate key errors when names collide

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
T0.1: Call auto_share_folders() after upsert_member() in
reconcile_introduced_devices. Introduced peers were getting DB records
but no Syncthing folders.

T0.2: Add own_device_id parameter to ensure_leader_introducers and
skip self. Prevents wasteful API call that always fails silently on
the leader's own machine. Moved config loading earlier in
sync_pending_devices.

T0.3: Add user_id collision check in sync_accept_pending_device.
Returns 409 if extracted member name matches an existing member with
a different device_id (prevents silent folder ID collisions).

T0.4: Clean up orphaned sync_settings rows (scope team:X and
member:X:%) in delete_team() before CASCADE delete. sync_settings
has no FK to sync_teams.

T0.5: Add cleanup_data_for_project() and call it from
sync_remove_team_project. Deletes remote-session files on disk and
DB rows, matching remove_member's cleanup behavior.

T0.6: Remove `continue` after add_device exception in
auto_accept_pending_peers. Device may already be configured via
introducer — proceed with upsert_member + auto_share_folders.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Each device now has a member_tag (user_id.machine_tag) that uniquely
identifies it. machine_tag is auto-derived from hostname (sanitized:
lowercase, alphanumeric + hyphens). user_id no longer allows dots
(dot is the member_tag separator).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Adds machine_id, machine_tag, member_tag columns to sync_members.
Creates sync_rejected_folders table for persistent folder rejection.
All new columns are nullable for backward compatibility during migration.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Parses 'user_id.machine_tag' format used in folder IDs.
Existing build/parse functions already handle dots in the
username component — no changes needed.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…r_tag

T1.4: sync_folders.py — use config.member_tag for outbox/handshake folder
IDs and paths; use m.get("member_tag") or m["name"] fallback for inboxes;
update cleanup functions for member_tag matching.

T1.5: sync_reconciliation.py — parse member_tag from handshake folder
usernames; pass machine_tag/member_tag to upsert_member calls; populate
own identity columns in reconcile_pending_handshakes.

T1.6: pending.py — use config.member_tag for own outbox path; parse
member_tag from handshake candidates; add member_tag to own_prefixes
check for self-detection.

Also updates sync_queries.py upsert_member, list_members, and
get_member_by_device_id to accept and return new identity columns
with COALESCE-based backward compat.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
sync_teams: pass machine_id/machine_tag/member_tag on create/join;
parse leader member_tag from join code.

sync_devices: parse member_tag when accepting pending devices;
pass identity columns to upsert_member.

sync_projects: use config.member_tag for outbox folder IDs and paths;
use m.get("member_tag") fallback for inbox folder IDs;
skip own member_tag directory in received_counts scan.

sync_members: include member_tag in list/profile queries;
expose member_tag in member list response.

sync_pending: add config.member_tag to own_names for self-detection;
display "user (machine)" format in pending descriptions.

Update test_sync_project_status outbox path to use member_tag.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
T1.8: remote_sessions.py — add _is_local_user() helper to match both
bare user_id and member_tag directories; update _resolve_user_id() to
extract user_id from member_tag dirs (with hostname suffix guard);
use _is_local_user() in project mapping scan.

T1.9: packager.py — add member_tag param to SessionPackager and include
in manifest; manifest.py — add member_tag field to SyncManifest;
sync_operations.py — use config.member_tag for outbox folder ID/path
and pass to SessionPackager; watcher_manager.py — use member_tag for
outbox path and pass to SessionPackager.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Three fixes from Phase 1 code review:

1. _resolve_user_id: DB member name is now authoritative when device_id
   matches (handshake healing keeps it current). Removed destructive
   heal that overwrote DB names with stale manifest user_id. Fixes
   test_received_counts_resolves_via_device_id.

2. schema.py: removed redundant column-guard block that duplicated
   the v17 migration logic (fresh installs get columns from CREATE TABLE).

3. pending.py: replaced unnecessary hasattr(config, 'member_tag') with
   truthiness check (member_tag is always a computed property).

Also fixed: missing `import re` in remote_sessions.py, stale schema
version assertion in test_remote_sessions.py, and corrected test
expectation for multi-dot hostnames (not valid member_tags).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Provides read/write helpers for the karma-meta--{team} folder:
member state files, removal signals, team info. Creator-only
removal authority enforced via team.json.created_by check.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Metadata folder is sendreceive type shared by all members. Team
creator writes team.json with created_by. Each member writes their
own state file on join. auto_share_folders adds new devices to the
metadata folder. cleanup_syncthing_for_team removes it.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- sync_metadata_writer.py: reads DB state (subscriptions, settings)
  and writes own member JSON to karma-meta--{team}/members/
- Calls update_own_metadata after: join, share/unshare project,
  change team settings, change member settings
- Removal signal written to karma-meta--{team}/removals/ when
  removing a member (creator-only authority enforced via team.json)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Reads member states and removal signals from karma-meta--{team}.
Discovers new members, updates identity columns, detects self-removal.
reconcile_all_teams_metadata iterates all teams.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- reconcile_all_teams_metadata gains auto_leave flag: when set, teams
  where we've been removed get Syncthing cleanup + local DB deletion
- WatcherManager runs MetadataReconciliationTimer every 60s to
  discover new members, update identities, and handle auto-leave
- Timer uses _ConfigProxy to avoid async _load_identity in thread

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…zation, authority fallback

Review fixes for the team metadata folder implementation:
- Atomic JSON writes (temp+rename) prevent Syncthing sync conflicts
- Path traversal guard (_safe_member_path) rejects unsafe member_tag filenames
- validate_removal_authority falls back to DB join_code when team.json missing
- _auto_leave_team handles async context via run_coroutine_threadsafe
- Delete copy-paste bug (sync_direction queried twice in metadata_writer)
- Remove unused import, add 4 tests (corrupt JSON, path traversal, DB fallback)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ocally

Root cause: "not running" tests set proxy._client = None but
_require_client() calls _try_connect() which connects to the real
local Syncthing daemon. Fix: patch _try_connect in not-running tests
and properly mock the REST connections response for get_devices.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…invite, per-device limits

Implements 5 tasks from the Sync v2 Phase 3 plan:

T3.1: Persistent folder rejection
  - Add reject_folder, unreject_folder, is_folder_rejected, list_rejected_folders
    to sync_queries.py (uses existing sync_rejected_folders table from v17)
  - Wire into sync_pending router: rejected folders filtered from pending list,
    rejection persisted on dismiss, unreject on explicit accept
  - Wire into CLI pending.py: skip rejected folders during acceptance

T3.2: Rejection updates metadata subscriptions
  - After rejecting a folder, call update_own_metadata() to write
    subscription=false to metadata file for other members to read
  - After accepting (unreject), update metadata to restore subscription

T3.3: auto_share_folders checks subscriptions
  - Read member metadata states before creating inbox folders
  - Add member_subscriptions param to ensure_inbox_folders()
  - Skip inbox creation when member has subscription=false for project

T3.4: Any-member invite endpoint
  - POST /sync/teams/{team}/invite generates invite code using caller's
    device as entry point (not just team creator)
  - Validates team membership before generating code
  - Invite code format: {team}:{member_tag}:{device_id}

T3.5: Per-device session limit via metadata
  - get_session_limit() checks metadata file for session_limit override
    before falling back to team-level setting
  - Wire into SessionPackager.package() with team_name and member_tag

Tests: 17 new tests (6 rejection, 2 subscription, 5 invite, 4 session limit)
Verified: 1663 API tests pass, 168 CLI tests pass, no regressions.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Two CLI code paths (main.py, project_resolution.py) were still using
config.user_id for build_outbox_id, which would cause folder ID
collisions when the same user has multiple devices. Now uses
config.member_tag consistently, matching the API layer pattern.

Also updates inbox folder construction to use m.get("member_tag")
with fallback to m["name"] for legacy member compatibility.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The "nuke everything" reset was missing v2 additions: handshakes/,
metadata-folders/, sync_rejected_folders table, and stale v1 DB files
(index.db, karma.db, sessions.db, workflow.db). Updated frontend
description to match. Includes v2 implementation plan docs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
CREATE TABLE IF NOT EXISTS is a no-op when sync_members already exists
from an older schema version (pre-v17, without machine_id/machine_tag/
member_tag columns). The idx_sync_members_tag index creation then fails
with "no such column: member_tag".

Moved the index creation to after a new column-patching block that
ALTERs sync_members to add missing v2 identity columns, mirroring the
existing sync_teams column patching pattern.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…er offers

Bug 1 — v1 folder IDs: reconcile_introduced_devices and auto_accept_pending_peers
derived usernames from Syncthing device names or folder ID parsing, producing
v1-style names (e.g. "ayushs-mac-mini") instead of proper member_tags
("ayush-mini.ayushs-mac-mini-local"). Added resolve_member_tag_from_metadata()
that looks up the authoritative member_tag from the team metadata folder's
state files by device_id. Both reconciliation paths now prefer this over
fallback sources.

Bug 2 — inbox device isolation: ensure_inbox_folders created inbox folders
with only [sender, self] in the device list. When the leader created an inbox
for a new member, other team devices were excluded — they could only get
access via the Syncthing introducer mechanism. Changed to include ALL team
members in inbox device lists (matching ensure_outbox_folder behavior), so
folder propagation works regardless of introducer state.

Bug 3 — metadata reconciler missing auto-share: reconcile_metadata_folder
was the only reconciliation path that added new members to the DB without
calling auto_share_folders. When the metadata folder synced faster than the
Syncthing introducer (race condition), the member was marked as "known" in
the DB but no folders were shared. Added _auto_share_with_new_members() to
trigger folder sharing for newly discovered members.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
… crash

API: Pending folders endpoint now filters out folders already configured
in Syncthing. Remote devices re-offer shared folders as pending, which
caused already-configured outbox folders to appear as ghost offers.

Frontend: Use composite key (folder_id + from_device) in the pending
folders {#each} block. Multiple devices can offer the same folder ID,
causing Svelte's each_key_duplicate error.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Unindexed remote sessions were appended to every page response in the
SQLite fast path, causing duplicate UUIDs when the frontend concatenated
pages. Restrict remote session merging to page 1 (offset==0) and add
frontend dedup as a safety net.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The previous fix only checked the current page's UUIDs, so a remote
session whose UUID existed on a different DB page would still be added
as "unindexed" to page 1. Now queries ALL project UUIDs from the DB
(fast UUID-only query) before comparing with remote sessions.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- OverviewTab: use member_count from DB instead of Syncthing device count
  (was excluding self, showing 3 instead of 4)
- OverviewTab: include self in connected count (+1, always online)
- sync_teams API: enrich member responses with live Syncthing connection
  data instead of hardcoding connected=False — fixes TeamOverviewTab and
  TeamCard always showing 0 online

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Overview tab: dual-line sent/received chart, proportional project bars
  with rank indicators, mini activity feed with "View all" link
- Activity tab: group events by day (Today/Yesterday/This Week/Older)
- Teams tab: sort project pills by session_count descending
- Header: slim down stat chips to metadata row, add counts to tab triggers
- Member color theme: set --member-color CSS variable at page level,
  child components reference var(--member-color) without prop drilling
- Link remote user badges to /members/{user_id} across SessionCard,
  GlobalSessionCard, TeamMemberCard, and ProjectTeamTab

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Compute actual received counts in the overview chart instead of
hardcoding 0. For remote members, counts sessions the local user
packaged in shared teams. For self, counts session_received events
from others. Also fix Sessions tab count to show sessions_sent.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Chart.js renders on <canvas> via the Canvas 2D API, which cannot parse
CSS variable strings like 'var(--text-muted)'. The shared chartConfig.ts
was passing these raw strings for tick labels, legend text, and default
font color, causing Chart.js to fall back to black — making chart text
invisible in dark mode.

Core fix in chartConfig.ts:
- registerChartDefaults() now resolves CSS vars via getComputedStyle()
- createResponsiveConfig() resolves legend/tooltip colors at call time
- createCommonScaleConfig() resolves tick colors at call time
- Added getThemeColors().textFaint for additional theme coverage
- Added onThemeChange() utility using MutationObserver on data-theme
  attribute to detect live theme toggles

Theme reactivity added to all 10 chart components:
- SessionsChart, ToolsChart: extracted inline onMount into createChart()
  functions with theme listener for full chart recreation
- UsageAnalytics, McpTrendChart, AgentTrendChart, PluginUsageStats:
  added onThemeChange(() => createChart()) in onMount with cleanup
- TeamOverviewTab, TeamActivityTab, MemberOverviewTab: added reactive
  themeVersion $state trigger to force $effect re-runs on theme change
- analytics/+page.svelte: cached dynamic Chart constructor, extracted
  createAnalyticsChart() async function, added onDestroy cleanup

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Fix local plans incorrectly appearing as remote by porting the
member_tag-aware _is_local_user() pattern from remote_sessions.py.
Add _find_user_dir() to resolve clean user_id back to filesystem
directory (handles both bare user_id and member_tag dir names).

Frontend changes for remote plan cards:
- Color FileText icon with member color instead of border override
- Convert badge from <div> to <a> linking to /members/ (consistency
  with session cards)
- Plan detail page: PageHeader icon colored with member theme,
  subtle background tint on content card, header badge is now a link
- Linked sessions are now clickable <a> tags pointing to the synced
  remote session (with ?remote=1 param)

Also adds proper TypeScript types for remote plan fields on PlanDetail
(removes two `as any` casts) and exposes project_encoded_name in the
API response for linked session URL construction.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants