15 Commits

  • admin(review console): fix "Load older" order and the re-review follow-ups
    Maintainer repair for the #167 re-review of 25d9375f.
    
    Blocking: "Load older messages" prepended the older page to a newest-first
    list that MessagePreview reverses, so it showed under the newest message.
    It is appended now.
    
    Live media:
    - LiveMedia probes each candidate key with a one-byte read and returns
      ErrLiveMediaTooLarge for an object over 16 MiB before reading it. The
      admin API maps it to 413 and the panel answers 413 with a plain-text
      reason and the same nosniff / CSP sandbox headers.
    - Responses are Cache-Control: no-store again, at both layers.
    - parseMedia reads the top-level kind and the id of the right object, so
      documents get their download link. Number literals are quoted before
      JSON.parse, so 64-bit ids never become JS numbers.
    - Tests: LiveMedia over the real files service on LocalFS (sniffing,
      allowlist, key checks, the cap and the probe), /v1/media and panel proxy
      headers, a messages.read gate test for /api/live/*, and a DSN-gated
      dialog paging test with tied and zero dates.
    
    Messenger:
    - The dialog cursor is keyed on before_peer_id, since top_message_date is
      0 for dialogs created by a scheduled message, a TTL or a chat theme.
    - The dialog list has its own stale-response guard for owner switches and
      "Load more chats".
    - Bubbles show the box id that "Delete selected" takes.
    - Detail pages preview the 50 messages ending at the opened one and keep
      it in view.
    - ?focus=env jumps once, so Status can be opened afterwards.
    - The shell title and the lit tab use the longest matching tab path, so
      group pages say Groups and detail routes light their tab; /premium is
      an alias of the Premium tab.
    - The remaining hardcoded strings go through i18n (en, zh, ru).
    
    Docs: /api/live/* rows document the cursor, has_more, media types, status
    codes, headers and messages.read; "Adding a page" says the shell renders
    the tab bar, and the generator now fails on a <SectionTabs> in a page.
    AGENTS.md, .env.example and comments use the current section names.
    web/dist is rebuilt.
    
    Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
  • Translate Chinese/Russian comments, logs and diagnostics to English
    Translate all natural-language comments, log/diagnostic messages, SQL comments and config/doc prose from Chinese (and Russian) to English across the codebase (~840 files). Rebased on upstream main; upstream's newly added files are translated too.
    
    Locale resources and functional non-English data are left intact: language packs, admin/grammystore i18n bundles, localized docs, the officialBrandRE regex, seeded Russian premium-bot text, user-facing Russian bot strings and asserted Unicode test fixtures. A new internal/translation guard test fails if CJK/Cyrillic reappears in source outside that allowlist.
    
    No code, identifiers, struct tags or behavior changed (verified with an AST-level comment/string audit). go build ./..., go vet ./... and the guard pass.
  • feat(docker): add --no-pull to restart a locally built stack (#136)
    Sync private telesrv 8f8d15b6. start-docker always ran
    `docker compose pull` unless --build was given, so restarting a stack
    built locally under a custom TELESRV_IMAGE_TAG failed with "manifest
    unknown" or was silently replaced by an older published image.
    
    --no-pull (-NoPull in PowerShell) skips the pull and starts with
    `up --pull never`, so a missing image fails fast instead of being
    fetched. With --build it builds without refreshing base images. Both
    scripts keep their public "gramsrv" ready message.
    
    Refs #136
    
    Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
  • feat(admin): fold the star giveaway into the Stars & Premium page
    Giveaway was a tab of its own holding one field and one button, which is
    not a job a tab bar should carry. The button now sits at the bottom of the
    Stars & Premium page, next to the per-user grants it belongs with, inside
    a frame with a warned border: it is the only write on the page that
    touches every account at once, and it keeps the same reason -> dry run ->
    confirm flow as every other write.
    
    Commerce drops to three tabs -- Stars & Premium, Prices, Stars balances --
    and the last one is renamed, because "Stars" and "Stars & Premium" in the
    same tab bar read as the same thing. The old /star-issue URL is an alias
    of the Stars & Premium tab, so a bookmark opens the same place and the
    sidebar row stays lit.
    
    The doc generator now prints the alias paths instead of "the bare alias",
    which was written for /messages and said nothing about /star-issue.
  • fix(admin): put the section tabs back in the page, and stop everything stretching
    Seven pages rendered their section sub-menu into PageFrame's `actions` prop
    instead of among its children, and actions goes into the header's
    right-aligned action row. Three of them -- Storage, Stars and Account
    ratings -- nested the tab bar inside the Refresh <button>, which is not
    valid HTML and looked worse still. So the sub-menu selector appeared
    alongside the page's buttons rather than under its title, on all four
    Accounts tabs and both Stars pages. Moving the seven to be PageFrame's
    first child is the same one-line position the other seventeen already use.
    
    Nothing caught it because the component renders either way, so the check
    that already exists for the generated doc tables now also asserts that no
    <SectionTabs> sits inside an actions prop, and names the file and line.
    
    The pinned-shell layout is the other half. .content is a grid, and inside
    the viewport-height media query it gets a definite height from flex, so
    its single implicit row absorbed all the leftover space and every page --
    each one a single .page-frame -- was pulled to the full viewport with its
    own rows smeared out to match. align-content: start puts that back. The
    overview is unaffected: it already opts in with an explicit 1fr row.
    
    In the overview's top band, the three summary panels are equal thirds, and
    auto-fit resolves that width to three columns -- right for the three
    health figures, wrong for the other two. The Stars economy rendered its
    four figures 3+1 with a hole beside them, and Needs attention took two
    tiles across two thirds of its panel while the shared stretch rule
    inflated both to the full band height. Each panel now pins the count its
    own figures need: 2x2 for the economy, two centred columns for the
    queues, hugging their content instead of stretching.
    
    People and chats was the worst of them. Nine counters in the tall band's
    left cell, auto-fit found five columns and rendered 5+4, so the figures
    used two rows of the four the panel had -- half the panel empty and every
    tile a cramped 124px. It is a fixed 3x3 that fills the panel and makes
    each tile about twice as wide.
    
    Finally, the server icon in Server Settings > Server identity rendered
    through .avatar-photo-img, which is a 50% circle, while the sidebar and
    the login screen show the same uploaded file as a 30% squircle. Scoping
    the radius to the edit slot makes all three agree and leaves the account
    avatars in tables circular.
    
    Co-Authored-By: Claude Fable 5.5 (1M context) <noreply@anthropic.com>
  • docs(admin): generate the panel's route and permission tables
    The route and permission tables in docs/admin-panel-api.{en,ru}.md had
    drifted a long way out of date and nothing noticed: permissions.tsx
    exported a dozen section rights the tables never mentioned, the SPA table
    listed routes that no longer existed, and several pages and their rights
    were missing entirely. They are generated from nav.ts, permissions.tsx and
    security.go now, so editing the source and rerunning is the only way they
    move -- and CI runs --check so they cannot rot unnoticed again.
    
    The generator also documents what the panel does now, since the rules it
    follows were only ever in the diff: the sidebar, the tab bars and the
    route table are three views of one declaration in nav.ts, a sibling page
    is a tab rather than a second sidebar row, a right outside
    assignablePermissions() cannot be granted to a named operator, and the
    //go:embed web/dist bundle has to be rebuilt and committed or a src change
    does not ship. AGENTS.md and both API docs carry that, plus a section on
    adding a page in the order that avoids repeating this drift.
    
    docs/giftfetch.md is new, and local-setup.md points at it. Official
    Star-gifts are only reachable with an authorized user session, which is the
    part that actually stops people: the fetch is written up around the QR
    login, the snapshot layout, the validation it performs, and the fact that
    a .session file is a live account credential and belongs outside the
    repository.
    
    One fix to the generator itself. It read nav.ts's tab arrays with a
    non-greedy (?P<tabs>.*?)\]\s*\}, which stops at the first ] followed by a
    } -- the closing bracket of a tab's own aliases: [...] -- so the messages
    section parsed to zero tabs and dropped out of the table with no error
    where all the others were fine. The tabs array is now read by bracket
    matching, and a section that parses to zero tabs raises instead of
    vanishing, because a silently missing section is exactly the failure this
    commit exists to stop.
  • add
  • feat(backup): add gramsrv-backup tool, restore scripts and migration guide
    Operators had no supported way to move a deployment or to recover one. This
    adds cmd/gramsrv-backup, which captures the PostgreSQL databases, the media and
    key tree, Redis and the configuration into a single verifiable bundle, and
    restores it on another host.
    
    backup / restore / verify, with MANIFEST.json recording the source commit, the
    schema version and exact row counts, and SHA256SUMS in sha256sum(1) format.
    Components are captured independently (server, grammystore) since each has its
    own database and usually its own container; pg_dump and redis-cli can come from
    the host or from docker exec.
    
    Portability is the point of the dump format:
    
      - plain SQL rather than the custom format, so any psql can replay it and the
        client need not match the server
      - --clean --if-exists so a bundle can be replayed over a populated database
      - --no-owner --no-privileges, and no --create, so the target role and database
        name are not baked into the artifact
      - restored with ON_ERROR_STOP=1 and --single-transaction, so a failure cannot
        leave a half-applied schema
      - \restrict, \unrestrict and SET transaction_timeout are stripped on the way
        in; they are PostgreSQL 17 specific and break older clients and servers
      - the target major version is checked before the restore starts
    
    Freezing (--freeze-units, --freeze-containers) is optional because pg_dump is
    already MVCC-consistent; it exists so the media tree is byte consistent, and
    everything stopped is restarted from a defer even when the capture fails.
    
    Verification re-hashes every artifact, decompresses it, walks tar members, and
    cross-checks file_blobs against the media tree, reporting missing and corrupt
    objects. check-media.sh runs the live form of that check.
    
    Also included: shell and PowerShell wrappers, a migration runbook in
    docs/backup-restore.md, and internal/doclint, which fails the build when tracked
    markdown leaks a key, a token or a credential so documentation stays safe to
    publish.