Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
15 changes: 9 additions & 6 deletions .claude/skills/update-account-server/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,12 +56,15 @@ ssh root@69.63.206.178 "cat /root/Container/gamma-account/Caddyfile" | diff -
Show the user any difference and copy a file over (`git show <headSha>:cloud/deploy/<file> | ssh root@69.63.206.178 "cat > /root/Container/gamma-account/<file>"`)
only once they agree. `.env` is never copied — new variables from
`.env.example` (`git diff <old>..<headSha> -- cloud/deploy/.env.example`) are
named to the user to add by hand. The same `compose.yml` also pins the
`demo` service's image tag (demo.gammapdf.com, the `update-demo-server`
skill): a diff on that one line is the demo's own pin, not drift — keep the
host's tag when copying the file over (never let this skill move the demo
to an older image), and `up -d` restarts only the services whose image or
config changed.
named to the user to add by hand.

The public demo (demo.gammapdf.com) is NOT in this project: it is its own
compose project in `/root/Container/gamma-demo/` (the `update-demo-server`
skill; never touch it from here). This project holds only its way in: the
Caddyfile's `@demo` handle and Caddy on the external network `gamma-edge`,
where the demo answers as `gamma-demo`. `compose.yml` refuses to start while
that network is missing; on a new host create it first
(`docker network inspect gamma-edge >/dev/null 2>&1 || docker network create gamma-edge`).

## Update

Expand Down
260 changes: 111 additions & 149 deletions .claude/skills/update-demo-server/SKILL.md

Large diffs are not rendered by default.

8 changes: 4 additions & 4 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ Topic docs live in `docs/dev/` — **read the relevant one before working in tha
- [docs/dev/collab.md](docs/dev/collab.md) — real-time collaboration: the block-op write path and per-page op log (`gamma/ops.py`), the workspace change feed + page tombstones (`gamma/routers/sync.py`, `deleted_pages`), rooms + the page websocket (`gamma/collab.py`, presence), the client session (`src/collaboration/collabSession.js` + `src/collaboration/usePageCollab.js`, `src/shared/model/blockOps.js`), same-block reconciliation, undo rebasing. Read before touching any block writer or the tree's save path.
- [docs/dev/mirror.md](docs/dev/mirror.md) — offline copies: a workspace that mirrors a workspace on another Gamma server (`gamma/sync_engine.py`, `gamma/sync_tree.py`, `routers/sync.py` + `routers/mirrors.py`): the change feed and `whoami`, the per-page three-way reconciliation from a saved base tree, edit-beats-delete, files by hash, the conflict list, write-scope integration tokens as bearer credentials on the HTTP API, Settings → Workspaces → Clones, the desktop switcher's *clone* chip. Read before touching sync, tokens on `/api/*`, or anything a mirror pushes.
- [docs/dev/workspaces.md](docs/dev/workspaces.md) — workspaces: accounts vs libraries, the two kinds (personal: several per account, just you, metered; shared: admin-made, roles owner/editor/viewer, access private / public), the default workspace, what admins may do without membership, storage (personal only + per-workspace quotas), how a request names its workspace (`?ws=` / `X-Gamma-Workspace` / the personal default), shares inside workspaces, backups (the one zip format behind export/import, server-kept per-workspace snapshots, `gamma/ws_backup.py`), the API, the switcher + the Workspaces, Backups and Server panes. Read before touching any auth helper or data-path helper.
- [docs/dev/guests.md](docs/dev/guests.md) — guests keep nothing: every guest login mints a throwaway `guest-<random>` account with its own workspace, expired after `guest_ttl_hours` by the middleware and the sweeper (`gamma/guests.py`, `workspaces.delete_account`), the optional `GAMMA_GUEST_SEED` starter zip; the shared AI allowance (tokens per account per 24 h on the admin's shared entries, `ai_settings.ai_runtime` + one choke point in the transport); demo mode (`demo_mode` / `GAMMA_DEMO`: the login page's "Try the demo", the first tour offered on arrival, sessionStorage guide progress) and demo.gammapdf.com's deployment (`cloud/deploy/`, the `update-demo-server` skill). Read before touching guest handling, the shared providers or the login page.
- [docs/dev/guests.md](docs/dev/guests.md) — guests keep nothing: every guest login mints a throwaway `guest-<random>` account with its own workspace, expired after `guest_ttl_hours` by the middleware and the sweeper (`gamma/guests.py`, `workspaces.delete_account`), the optional `GAMMA_GUEST_SEED` starter zip; the shared AI allowance (tokens per account per 24 h on the admin's shared entries, `ai_settings.ai_runtime` + one choke point in the transport); demo mode (`demo_mode` / `GAMMA_DEMO`: the login page's "Try the demo", the first tour offered on arrival, sessionStorage guide progress) and demo.gammapdf.com's deployment (its own compose project, `cloud/deploy/demo/`, the `update-demo-server` skill). Read before touching guest handling, the shared providers or the login page.
- [docs/dev/migrations.md](docs/dev/migrations.md) — the versioned data-directory upgrade: `SCHEMA_VERSION`, numbered steps, snapshot-then-stamp, refusal of newer directories, the no-pile-up rules, how to write a step. Read before changing any stored shape.
- [docs/dev/user_db.md](docs/dev/user_db.md) — the data directory (`users.db` + per-workspace DBs), auth middleware, seeding/first-run admin, `manage.py` CLI, user-management GUI, storage limits, the server log.
- [docs/dev/ai.md](docs/dev/ai.md) — the AI stack: provider entries and protocols (incl. ChatGPT OAuth), the `/api/ai/chat` request/stream shape, the library agent (scopes, permissions, tool loop, replay, privacy), chat-history buckets.
Expand Down Expand Up @@ -86,7 +86,7 @@ Frontend has no linter. UI changes are verified by relevant flows in the browser

- All state is SQLite + files under a data directory (env `GAMMA_DATA_DIR`, defaults to the repo's `data/`): global `users.db` (accounts, sessions, workspaces + memberships, page shares, personal prefs incl. the AI provider entries, server settings; its `PRAGMA user_version` is the schema version) plus per-WORKSPACE `workspaces/<id>/pages.db` (the `unified_blocks` tree — everything is a block; root-level blocks are pages, highlights are blocks with `highlight_id`/`pdf_position` in `properties`), `data.db` (chats, page snapshots, search indexes) and `uploads/` (content-hash filenames, dedup). Accounts and workspaces are separate: every account owns one or more personal workspaces (one of them its default) and may be a member of admin-created shared ones under a role. Layout: [docs/dev/user_db.md](docs/dev/user_db.md); model: [docs/dev/workspaces.md](docs/dev/workspaces.md).
- Schema changes are numbered migration steps (`gamma/migrations.py`, run at startup with a snapshot first; `db.py` always creates the current shape — never add a lazy `ALTER TABLE` on connect). Rules: [docs/dev/migrations.md](docs/dev/migrations.md).
- Auth: `session` cookie → middleware resolves `request.state.user`; a guest is a throwaway `guest-<random>` account whose workspace is deleted after `guest_ttl_hours` ([docs/dev/guests.md](docs/dev/guests.md)). A cloud sign-in (`gamma/cloud_auth.py`) ends in the same session row — never add a second identity path; an account with an empty `password_hash` is cloud-only and the password login refuses it. The WORKSPACE a request works in is a separate decision: `require_ws(request)` (any member, or anyone signed in for a public workspace) / `require_ws(request, write=True)` (editor or owner) from `?ws=` / the `X-Gamma-Workspace` header / the account's default. Server admins pass the workspace API's checks for every workspace without membership, but read pages only as members. Share tokens are per PAGE of a workspace, Notion-style: invited people each with view/edit, plus general access (anyone / signed-in users / invite only) with its own role; members keep their workspace role on top. Shared-view endpoints resolve the workspace via `resolve_ws` + `share_scope_page`; only the block writers accept an edit share through `require_ws_writer`, still scoped to the page. Data helpers take the workspace id; `auth.actor_of(request)` is the actor — the account, or `link:<display name>` for a visitor editing through an anyone-with-the-link edit share (the `X-Gamma-Name` header / the socket's `?name=`; such visitors are the only writers rate limited per IP). Keep those distinctions when touching endpoints (details: [docs/dev/api.md](docs/dev/api.md)).
- Auth: `session` cookie → middleware resolves `request.state.user`; a guest is a throwaway `guest-<random>` account whose workspace is deleted after `guest_ttl_hours` ([docs/dev/guests.md](docs/dev/guests.md)). A cloud sign-in (`gamma/cloud_auth.py`) ends in the same session row — never add a second identity path; an account with an empty `password_hash` is cloud-only and the password login refuses it. The WORKSPACE a request works in is a separate decision: `require_ws(request)` (any member, or anyone signed in for a public workspace) / `require_ws(request, write=True)` (editor or owner) from `?ws=` / the `X-Gamma-Workspace` header / the account's default. Server admins pass the workspace API's checks for every workspace without membership, but read pages only as members. Share tokens are per PAGE or per FOLDER of a workspace (a folder share reaches the pages filed there or below it, read live), Notion-style: invited people each with view/edit, plus general access (anyone / signed-in users / invite only) with its own role; members keep their workspace role on top. Shared-view endpoints resolve the workspace via `resolve_ws` + `share_scope` (a `ShareScope`: `allows_page` / `allows_block`, the only place that knows a share's kind); only the block writers accept an edit share through `require_ws_writer`, still scoped to the share's pages. Data helpers take the workspace id; `auth.actor_of(request)` is the actor — the account, or `link:<display name>` for a visitor editing through an anyone-with-the-link edit share (the `X-Gamma-Name` header / the socket's `?name=`; such visitors are the only writers rate limited per IP). Keep those distinctions when touching endpoints (details: [docs/dev/api.md](docs/dev/api.md)).
- Route order matters for `/api/blocks/*`: static-prefix routes (`by-doc`, `children`, `subtree`) must be registered before `/{block_id}`.
- AI: providers are per-user GUI entries — there are NO env API keys; AI endpoints must build config through `ai_runtime(user)` (`gamma/ai_settings.py`), never module-level constants. Chat speaks Anthropic Messages, OpenAI Chat Completions, and the ChatGPT-OAuth Responses wire, each an adapter in `gamma/ai_protocols/` — provider differences go on the adapter, never as a protocol branch in a route; model facts (listings, context windows) are asked live, never tabled in code; the library agent's tool registry lives in `gamma/ai_tools.py`. All wiring: [docs/dev/ai.md](docs/dev/ai.md); long-paper context: [docs/dev/ai_context.md](docs/dev/ai_context.md).
- Paper metadata + PDF resolution (arXiv/DOI/Unpaywall chains, AI extraction fallback, BibTeX/citation caching): [docs/dev/paper_metadata.md](docs/dev/paper_metadata.md).
Expand Down Expand Up @@ -122,8 +122,8 @@ Frontend has no linter. UI changes are verified by relevant flows in the browser
- Notices (`src/app/notices.js` + `useNotices.js`, `gamma/notices.py`, `/api/notices`): the red dot on the account button for what wants a look once (a newer release and log errors for admins; a failed backup task, open clone conflicts, a failed cloud sync, storage nearly full for everyone); each notice names the Settings pane that resolves it, showing the pane records its fingerprint per account, and "Settings…" opens on the strongest one. New sources are `@source` functions in `gamma/notices.py`, cheap reads only. Details: [docs/dev/settings.md](docs/dev/settings.md).
- Interface language ([docs/dev/i18n.md](docs/dev/i18n.md)): every user-visible string in the frontend goes through `t("English sentence")` from `src/shared/i18n/i18n.js` (`tn` for counts, `T` to mark a string in a static table); the English text is the key, `locales/zh.json` holds the Chinese. A new string needs its catalog line in the same change: `npm run i18n -- --sync` adds the empty key, `node --test tests/i18n.test.mjs` fails until it is filled; `npm run i18n:audit` lists text that never reached `t()`. `t()` works in module-level constants (the catalog loads before `App`; a language change reloads the page). Never compare against translated text. The browser suite pins `en-US` on every context.
- Theme: Settings → Appearance — System plus the seven pinned themes in `THEMES` (`app/prefs.js`; `gamma-theme` in localStorage, an inline script in `index.html` applies a pinned theme before first paint) plus display-only "Flip page colors" (`gamma-pdf-dark`).
- Sharing a page: `src/sharing/SharePopover.jsx` (the header link button, a popover under it like the account menu) — link + Copy + Stop sharing, access as three audience tiles (anyone / signed in / invited only) plus a View / Edit toggle, invited people with their own access, the citation section; built from the settings kit like the workspace Manage dialog. There is no "reset link" — stop and share again. Data functions stay in App.jsx (`loadShareSettings`, `updateShareSettings`, …).
- View modes are derived from the URL: `/` home, `/?page=<id>` page (with PDF if it has `source_url`), `/?share=<token>` the share view (`shareMode`: no library/chat/prefs; `readOnly` is state — false once the link resolves with edit rights, and `utils.withShare` puts the token on every API call), `/?block=<id>` jump-to-block; every non-share URL also carries `ws=<workspace id>`.
- Sharing a page or a folder: `src/sharing/SharePopover.jsx` (the header link button, a popover under it like the account menu; for a folder the same popover with a folder `target` — only the words change — under the topbar's link button while a folder is open, or from the folder menu's Share…) — link + Copy + Stop sharing, access as three audience tiles (anyone / signed in / invited only) plus a View / Edit toggle, invited people with their own access, the citation section; built from the settings kit like the workspace Manage dialog. There is no "reset link" — stop and share again. Data functions stay in App.jsx (`loadShareSettings`, `updateShareSettings`, …).
- View modes are derived from the URL: `/` home, `/?page=<id>` page (with PDF if it has `source_url`), `/?share=<token>` the share view (`shareMode`: no library/chat/prefs; `readOnly` is state — false once the link resolves with edit rights, and `utils.withShare` puts the token on every API call; a folder share shows its listing, `sharing/SharedFolder.jsx`, and opens a page with `page=<id>` beside the token, each a history entry), `/?block=<id>` jump-to-block; every non-share URL also carries `ws=<workspace id>`.
- Reference links: a highlight block with `properties.link_url` / `link_page_id` is a clickable link region on the PDF; `link_highlight_id` additionally targets an exact highlight in that paper. Document links resolve against the library by DOI/arXiv id before offering fetch-vs-browser.
- Home library (folder labels, merged listing, the shared `PageCard`, recents strip + snapshots, context menu): [docs/dev/home_library.md](docs/dev/home_library.md).
- Menus (`src/shared/ui/Menus.jsx`): `ContextMenu` + row primitives (`MenuItem`/`MenuLabel`/`SubMenuItem`). A flyout renders INSIDE the parent menu's DOM (portalling would break the outside-pointerdown test) and opens on hover guarded by `src/shared/ui/menuAim.js` (the "safe triangle"; UI-agnostic, reuse for any hierarchical surface).
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
<img src="https://get.microsoft.com/images/en-us%20dark.svg" alt="Download Gamma from the Microsoft Store" width="240">
</a>

**[Install](#install)** · **[User guide](./docs/user_guide.md)** · [Website](https://gammapdf.com) · [Releases](https://github.com/tim4431/Gamma/releases)
**[Install](#install)** · **[User guide](./docs/user_guide.md)** · [Try the demo](https://demo.gammapdf.com) · [Website](https://gammapdf.com) · [Releases](https://github.com/tim4431/Gamma/releases)

Every picture below is clickable and opens the matching part of the [user guide](./docs/user_guide.md).

Expand Down Expand Up @@ -82,7 +82,7 @@ Highlights and free notes are the same kind of block, so a paper's notes and a p
<a href="./docs/user_guide.md#workspaces"><img alt="Personal workspaces next to a shared research library where an owner and an editor type into two blocks of the same page at the same time and a viewer reads along" src="./docs/assets/branding/gamma-workspaces-light.svg" width="100%"></a>

- **Workspaces** — keep separate personal libraries, or collaborate in a shared library created by a server administrator: owners manage members, editors change pages, viewers read.
- **Share a page** — send a link to an annotated paper; invite people with view or edit rights, or open it to anyone with the link.
- **Share a page or a folder** — send a link to an annotated paper, or to a whole folder of them; invite people with view or edit rights, or open it to anyone with the link.
- **Edit together** — changes and cursors appear live; edits to different blocks coexist, same-block edits merge.

→ Guide: [Sharing a page](./docs/user_guide.md#sharing-a-page) · [Workspaces](./docs/user_guide.md#workspaces)
Expand Down
Loading
Loading