diff --git a/.env.example b/.env.example index 8867ceb..3e7a5bc 100644 --- a/.env.example +++ b/.env.example @@ -6,8 +6,9 @@ PORT=8787 PUBLIC_BASE_URL=http://localhost:8787 # Include both the host browser origin and PUBLIC_BASE_URL when they differ. ALLOWED_ORIGINS=http://localhost:8787 -# Production-required site access secret. Local development/tests may leave it -# empty. A non-empty value must contain 8-128 visible ASCII bytes (0x21-0x7e). +# Optional site access password in every environment. Unset or empty allows +# entry without a site password; room ownership and invitations still apply. +# A non-empty value must contain 8-128 visible ASCII bytes (0x21-0x7e). SITE_ACCESS_PASSWORD= # Rooms persist in rooms.sqlite under the working directory by default (also # when blank). Set :memory: explicitly for process-memory rooms. Deployments @@ -23,7 +24,8 @@ ENDPOINT_MEDIA_COPY_CAPACITY=2 # PIIK_DEBUG=route # Optional in-process automatic SFU fallback. Set SFU_UDP_PORT=7882 to enable; -# unset or empty keeps it disabled. Local Client modes never create this listener. +# unset or empty disables it and fixes Privacy mode on for every room. +# STUN/signaling/P2P remain available. Local App modes never create this listener. # Ingress uses the 9000-room code capacity; egress uses that capacity times the # configured per-room Viewer admission. SFU_UDP_PORT= @@ -38,11 +40,12 @@ SFU_PUBLIC_IP= STUN_URLS= # Hosted STUN is in-process. With STUN_URLS configured, bind this IPv4 address # on UDP 3478 (also 3479/3480 below). It is independent of HTTP LISTEN_HOST. -# Local Client modes never create these listeners, even with public discovery. +# Local App modes never create these listeners, even with public discovery. STUN_LISTEN_HOST=0.0.0.0 -# Optional bounded NAT traversal support. When enabled, Hosts see a per-share -# switch that defaults on after all three local STUN listeners bind. Browser +# Optional bounded NAT traversal support. The per-share switch stays visible; +# it is locked off when disabled, and defaults on when enabled after all three +# local STUN listeners bind. Browser # P2P derives UDP 3479/3480 from the first STUN_URLS endpoint on UDP 3478. # Cloud security-group and host firewall rules must also allow these UDP ports. NAT_PREDICTION_ENABLED=false @@ -53,9 +56,11 @@ NAT_PREDICTION_ENABLED=false # PIIK_ENV=development # PORT=8788 -# Minimal production example (use independent untracked secrets): +# Production example (keep any chosen password in an untracked environment): # PIIK_ENV=production -# SITE_ACCESS_PASSWORD=replace-with-an-independent-access-key +# PUBLIC_BASE_URL=https://share.example.com +# ALLOWED_ORIGINS=https://share.example.com +# SITE_ACCESS_PASSWORD= # STUN_URLS=stun:stun.example.com:3478 # NAT_PREDICTION_ENABLED=true # Automatic SFU fallback (does not replace STUN above): diff --git a/.gitattributes b/.gitattributes index 77c86f4..446e711 100644 --- a/.gitattributes +++ b/.gitattributes @@ -13,6 +13,7 @@ *.ico binary *.jpg binary *.jpeg binary +*.mp3 binary *.png binary *.webp binary *.woff binary diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..127eeb6 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,23 @@ +--- +name: Bug report / 问题反馈 +about: Report a reproducible problem / 反馈可复现的问题 +--- + +## Environment / 环境 + +- Piik version and build SHA / Piik 版本与构建 SHA: +- Browser, App, or Server / 使用端: +- OS and browser versions / 系统与浏览器版本: + +## Steps to reproduce / 复现步骤 + +1. + +## Expected and actual behavior / 预期与实际结果 + +- Expected / 预期: +- Actual / 实际: + +## Optional evidence / 可选证据 + + diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..f0ada69 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,12 @@ +--- +name: Feature request / 功能建议 +about: Describe a use case and desired outcome / 说明使用场景与期望结果 +--- + +## Problem or use case / 问题或场景 + + + +## Desired outcome / 期望结果 + + diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 362d163..2b81a7d 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,20 +1,17 @@ -## Summary + -Describe the problem and the outcome of this change. +## Problem and outcome / 问题与结果 -## Verification + -List automated tests, manual checks, environments, and meaningful measurements. +## Release notes -## Research And Decisions + -Link the relevant product owner, research note, issue, and ADR, or explain why none is needed. +## Verification / 验证 -## Checklist + -- [ ] The branch contains one coherent change and no unrelated churn. -- [ ] Required local tests and repository hygiene checks pass, and their exact evidence is listed above. -- [ ] Security, privacy, compatibility, and licensing effects were considered. -- [ ] Product owners, operational docs, ADRs, project memory, and status were updated only where their facts changed. -- [ ] Stale or conflicting memory and documentation were removed rather than preserved as a diary. -- [ ] No credentials, personal data, machine-specific paths, or required untracked artifacts are included. +## Impact / 影响 + + diff --git a/.github/workflows/repository-hygiene.yml b/.github/workflows/repository-hygiene.yml index 9b1cd4f..86cfeb2 100644 --- a/.github/workflows/repository-hygiene.yml +++ b/.github/workflows/repository-hygiene.yml @@ -11,13 +11,25 @@ on: required: false default: false type: boolean + candidate_version: + description: Optional candidate version (vMAJOR.MINOR.PATCH); never publishes + required: false + type: string permissions: contents: read +concurrency: + group: ci-${{ github.ref }} + cancel-in-progress: false + queue: max + jobs: validate: runs-on: ubuntu-latest + outputs: + version: ${{ steps.release.outputs.version }} + publish: ${{ steps.release.outputs.publish }} steps: - name: Check out repository uses: actions/checkout@v6 @@ -34,9 +46,26 @@ jobs: PIIK_BASE_SHA: ${{ github.event.pull_request.base.sha || github.event.before }} - name: Install dependencies run: npm ci + - name: Determine release identity + id: release + env: + PIIK_BUILD_VERSION: ${{ github.event_name == 'workflow_dispatch' && inputs.candidate_version || '' }} + run: >- + node scripts/release-version.mjs + ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' && vars.PIIK_RELEASES_ENABLED == 'true' && 'plan' || 'build' }} + --github-output + - name: Preview release notes + if: steps.release.outputs.publish == 'true' || inputs.candidate_version != '' + run: node scripts/release-notes.mjs "$PIIK_RELEASE_VERSION" "$GITHUB_SHA" --github-summary + env: + PIIK_RELEASE_VERSION: ${{ steps.release.outputs.version }} # The Go build embeds internal/server/webassets/dist, so Vite runs first. - name: Build the Browser client run: npm run build:client + env: + PIIK_BUILD_VERSION: ${{ steps.release.outputs.version }} + VITE_PIIK_VERSION: ${{ steps.release.outputs.version }} + VITE_PIIK_REVISION: ${{ github.sha }} - name: Cache Go toolchain uses: actions/cache@v4 with: @@ -49,15 +78,22 @@ jobs: cache-dependency-path: go.sum - name: Check Go core run: npm run check:client-core - - name: Type-check, test, and build - run: npm run check + - name: Type-check and test + run: npm run typecheck && npm test + env: + PIIK_BUILD_VERSION: ${{ steps.release.outputs.version }} + VITE_PIIK_VERSION: ${{ steps.release.outputs.version }} + VITE_PIIK_REVISION: ${{ github.sha }} release-application: needs: validate if: >- - github.event_name == 'workflow_dispatch' && inputs.client_checks == true + needs.validate.outputs.publish == 'true' || + (github.event_name == 'workflow_dispatch' && inputs.client_checks == true) runs-on: ubuntu-latest timeout-minutes: 15 + env: + PIIK_BUILD_VERSION: ${{ needs.validate.outputs.version }} steps: - name: Check out repository uses: actions/checkout@v6 @@ -93,7 +129,8 @@ jobs: - validate - release-application if: >- - github.event_name == 'workflow_dispatch' && inputs.client_checks == true + needs.validate.outputs.publish == 'true' || + (github.event_name == 'workflow_dispatch' && inputs.client_checks == true) strategy: fail-fast: false matrix: @@ -106,6 +143,8 @@ jobs: runner: macos-15 runs-on: ${{ matrix.runner }} timeout-minutes: 20 + env: + PIIK_BUILD_VERSION: ${{ needs.validate.outputs.version }} steps: - name: Check out repository uses: actions/checkout@v6 @@ -158,3 +197,52 @@ jobs: if-no-files-found: error compression-level: 0 retention-days: 14 + + publish: + needs: + - validate + - release-application + - release-clients + if: needs.validate.outputs.publish == 'true' + runs-on: ubuntu-latest + timeout-minutes: 30 + permissions: + contents: write + steps: + - name: Check out repository + uses: actions/checkout@v6 + with: + fetch-depth: 0 + - name: Set up Node.js + uses: actions/setup-node@v6 + with: + node-version-file: .node-version + cache: npm + - name: Install dependencies + run: npm ci --ignore-scripts + - name: Download application release + uses: actions/download-artifact@v8 + with: + name: piik-server-${{ github.sha }} + path: ${{ runner.temp }}/publish-release + - name: Download App release candidates + uses: actions/download-artifact@v8 + with: + pattern: piik-app-*-${{ github.sha }} + path: ${{ runner.temp }}/publish-release + merge-multiple: true + - name: Publish verified release + run: >- + node scripts/publish-release.mjs + "$RUNNER_TEMP/publish-release" "$PIIK_RELEASE_VERSION" "$GITHUB_SHA" + env: + GH_TOKEN: ${{ github.token }} + PIIK_RELEASE_VERSION: ${{ needs.validate.outputs.version }} + - name: Mirror verified release to Gitee + run: >- + node scripts/mirror-release.mjs + "$RUNNER_TEMP/publish-release" "$PIIK_RELEASE_VERSION" "$GITHUB_SHA" + env: + GH_TOKEN: ${{ github.token }} + GITEE_TOKEN: ${{ secrets.GITEE_TOKEN }} + PIIK_RELEASE_VERSION: ${{ needs.validate.outputs.version }} diff --git a/.github/workflows/website.yml b/.github/workflows/website.yml new file mode 100644 index 0000000..7373416 --- /dev/null +++ b/.github/workflows/website.yml @@ -0,0 +1,39 @@ +name: Website + +on: + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: github-pages + cancel-in-progress: false + +jobs: + deploy: + if: github.ref == 'refs/heads/main' + runs-on: ubuntu-latest + timeout-minutes: 5 + permissions: + contents: read + pages: write + id-token: write + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - uses: actions/checkout@v6 + - uses: actions/setup-node@v6 + with: + node-version-file: .node-version + cache: npm + - run: npm ci + - run: npm run build:website + - uses: actions/configure-pages@v5 + - uses: actions/upload-pages-artifact@v4 + with: + path: build/site + retention-days: 1 + - uses: actions/deploy-pages@v4 + id: deployment diff --git a/AGENTS.md b/AGENTS.md index 62ff490..08a8ae0 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -23,7 +23,7 @@ ## Product Contract -- Piik is private game screen sharing for one Host and up to 20 authenticated +- Piik is private screen sharing for one Host and up to 20 authenticated Viewers, with Web Host/Viewer/relay as the current product surface. - Current product truth is split by owner: [rooms/access](./docs/product/rooms-access.md), @@ -45,10 +45,9 @@ [status](./docs/status.md), [TODO](./docs/todo.md), and only the relevant product module, ADR, research, or operations document. Then inspect branch, HEAD, status, staged diff, and worktrees. -- Product modules own current behavior; ADRs own non-obvious decisions and - consequences; research owns evidence and license boundaries; deployment owns - operational reference. Status is the current execution/deployment index, TODO - is the only work ledger, and Git/PRs own completed history. +- Use the [documentation owner map](./docs/maintenance.md#owners): product + behavior, engineering, visual language, versions, evidence and operations each + have one owner. TODO is the only work ledger; Git/PRs own completed history. - Give every durable fact one owner. Update memory or status only when its compact snapshot materially changes. Ordinary UI detail, protocol field listings, self-evident code, test inventories, routine validation, and agent process do @@ -71,40 +70,28 @@ - Canonical root must be clean current `main` at audit/integration boundaries. Create branches/worktrees from that exact commit; old branches and worktrees are never truth sources. -- `main` is a low-frequency integration branch, never a workbench. Keep work on - branches/worktrees and merge only one complete, user-accepted phase per PR, - after its related implementation, tests, documentation, and acceptance are - complete. Use one squash commit and leave `main` unchanged between phases. - Do not merge intermediate checkpoints, release records, metadata-only edits, - or follow-up cleanup; fold them into the next phase. A P0/P1 emergency may - bypass batching only when the user explicitly authorizes it, still through - one coherent PR. +- `main` remains low-frequency: one complete, user-accepted phase per squash PR, + with no intermediate release-record or cleanup commits. Keep implementation + on worktrees; [CONTRIBUTING.md](./CONTRIBUTING.md#full-integration-and-release-workflow) + owns the full workflow and explicit emergency exception. - Preserve user work. Branch, truth-checkpoint, PR, release, recovery, and cleanup rules are owned by [CONTRIBUTING.md](./CONTRIBUTING.md); do not mirror that workflow here. ## Engineering Defaults -- Prefer the simplest cohesive design that is easy to use and maintain. Reuse - mature components, keep ownership explicit, and remove superseded paths in - the same change instead of retaining speculative flexibility or technical debt. -- Apply good coding practice to every change: split modules around clear - responsibilities and state/lifecycle ownership, with cohesive internals and - narrow interfaces. Reuse proven shared behavior and abstract stable common - responsibilities; avoid both duplicate implementations and needless wrappers. - Review coupling and readability, not only file size or line count. -- Judge a change by evidenced user value against its full implementation, - maintenance, compatibility, and failure cost. A large measured gain may - justify broad or breaking work; a small or speculative gain does not justify - material complexity or risk. -- After completing a material module, run one ablation pass and remove every new - component, state, dependency, or branch that is not needed for the verified - result or an accepted contract. Its primary goal is lower total system - complexity, not perfect performance; marginal gains do not justify permanent - mechanisms or a materially larger maintenance surface. +- UI changes must follow the shared [visual language](./docs/design/visual-language.md): + cast, semantic colours, result panels and motion have one owner. Extend the + existing preview when adding a new meaning; do not invent scene-local rules. +- Follow [engineering and interface boundaries](./docs/reference/engineering.md): + simple cohesive modules, clear state/resource owners, mature reuse and narrow + contracts. After a material module, remove additions that the accepted result + does not need; total complexity matters more than marginal performance gains. +- [Versioning](./docs/reference/versioning.md) separates build identity, wire, + storage and live generations, and owns the public compatibility design. + Its pending public-release policy does not replace today's private contract. - Use primary sources for non-trivial design and bugs. Add focused tests in proportion to risk; batch full browser/network/endurance checks at acceptance. -- Keep scripts deterministic, fast, cross-platform, and CI-runnable. Support - Windows, macOS, and Linux; use LF and ASCII filenames for new files. +- Keep scripts deterministic, fast, cross-platform and CI-runnable. - Before finishing material work, inspect status, report untracked artifacts, and stage requested files unless told otherwise. diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index 4a7a4a6..0000000 --- a/CLAUDE.md +++ /dev/null @@ -1,5 +0,0 @@ -@AGENTS.md - -# Claude Code - -This file intentionally imports the repository-wide agent instructions instead of duplicating them. Keep durable project guidance in `AGENTS.md` and the referenced bounded project documents. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 883a3e7..2a4d9af 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -23,7 +23,7 @@ leave `main` alone until the next material phase. Only a user-authorized P0/P1 emergency may use a separate PR, and that PR must still contain one coherent fix. -1. Create one short-lived branch/worktree from exact canonical `main`; use `feat/`, `fix/`, `docs/`, `refactor/`, `test/`, `chore/`, or `spike/` followed by a short description. +1. Create one short-lived branch/worktree from exact canonical `main`, following the [branch naming convention](#commit-pr-and-branch-names). 2. Update only the single durable owner for changed semantics and any materially changed current snapshot. If semantics remain disputed, record a hold and stop dependent work. 3. Implement and run repository hygiene, `npm run check`, the relevant browser/network gates, and review in proportion to the whole acceptance boundary. 4. Keep accepted truth checkpoints and dependent candidates on branches until the phase is complete. Rebase or rebuild a retained candidate from exact `main` only when starting a new phase, preserving main's owning truth on conflicts and transplanting only approved scoped code, tests, and new facts. @@ -37,20 +37,24 @@ the repository. Exact deployment identity belongs to the immutable release descriptor, runtime `REVISION`, and deployment record. Preserve separate commits only when they carry independently useful history. -Direct commits to `main` are reserved for an explicit user-approved exception. Never force-push shared branches or rewrite shared history without explicit approval. +Update `main` through PRs only; branch checkpoints and generated release metadata +do not bypass that boundary. Never force-push shared branches or rewrite shared +history without explicit approval. For deployment work, a routine application-only release verifies a new immutable artifact, switches to it atomically, and guarantees the pre-cutover application release only through bounded health and postflight checks; it has no retention contract afterward and is not a maintained backup. Define recovery only for the infrastructure, configuration, secrets, persistent state, or irreversible surfaces the task actually touches, before changing them. -Do not change `SIGNALING_PROTOCOL` for compatible internal, UI, routing, or -release work. A wire change makes existing pages stale and forces an active -Browser Host to reload, which ends its capture. Bump it only for an actually -incompatible message contract whose atomic cutover and capture interruption are -explicitly accepted. +Use the [version/compatibility policy](./docs/reference/versioning.md) to classify +release changes. In today's private contract an incompatible signaling cutover +forces old pages to reload and ends Browser capture; include that interruption +in the accepted cutover. Compatible UI/internal work does not require a wire bump. Clean up worktrees and branches only after semantic review and integration are complete and the normal merged-head, open-reference, clean-tree, and non-following link checks pass. Never merge an old branch wholesale after a newer truth checkpoint. ## Research Standard +The [engineering reference](./docs/reference/engineering.md) owns module and +interface discipline, including the post-change ablation pass. + Follow the [naming convention](./docs/reference/naming.md) for product copy, commands, packages and code ownership. Keep display names separate from stable protocol identifiers; use normal language-specific identifier conventions. @@ -65,6 +69,10 @@ protocol identifiers; use normal language-specific identifier conventions. ## Verification Entrypoints +Every retained test must be collected by a documented verification entry point. +Confirm runner inclusion when adding or moving a test; a file's presence alone +does not establish coverage. + - `npm run check` owns deterministic Web type-check, unit, and build acceptance. - `npm run check:client` owns Go formatting, unit tests, vet, three-platform builds, and the Windows capture compile/probe when run on Windows. The server @@ -80,16 +88,44 @@ protocol identifiers; use normal language-specific identifier conventions. identities; do not run network tests with a changing temporary executable path. Keep browser profiles isolated and consider the active firewall when diagnosing connection failures rather than changing global firewall rules. -- Server and three-platform Client candidates run only from an explicit manual - workflow dispatch with `client_checks=true`. Ordinary `main` pushes validate - source without running the expensive packaging matrix. - Publishing a tag or GitHub Release remains an explicit release decision rather - than a side effect of every merge. +- CI, versioning and release automation belong to complete, accepted squash + merges into `main`; ordinary branch pushes and PRs stay quiet. The + [versioning policy](./docs/reference/versioning.md) owns version selection, + publication and activation state. Package the merged SHA without writing + version-record commits back to `main`. + +## Commit, PR And Branch Names + +Use [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/): +`type(scope): outcome`. Scope is optional. Use `feat` for new behavior, `fix` +for repairs, or `docs`, `refactor`, `test`, `build`, `ci`, and `chore` for their +named purpose. Keep the outcome short and concrete; English is the default, +and a Chinese explanation may follow in the body. + +The PR title names the complete phase and becomes its single squash commit on +`main`, for example `fix(app): restore sharing after a source change`. Working +commits should follow the same form where useful; no history rewrite is needed +just to rename commits that will be squashed. Add `!` before the colon for a +breaking change, such as `feat(protocol)!: replace the join handshake`, and +explain its effect in the body. The final squash title and body supply release +intent; the [versioning policy](./docs/reference/versioning.md) owns version +selection and publication behavior. + +Name branches `/`, using the same types; +reserve `spike/` for experiments. No issue number is required. 中文说明:PR 标题 +与最终 squash 提交使用上述格式;分支用英文短名,破坏性变更标记 `!` 并说明影响。 ## Pull Request Scope A pull request should explain the problem, the chosen design, verification performed, user-visible or operational effects, and remaining risks. +Before an integration PR is accepted, fill its exact `## Release notes` section +with concise public copy in Chinese and English: user-visible changes, fixes and +required upgrade actions. Use `###` for subsections. Review this text as product +copy; it is published automatically from the squash commit, without PR discussion +or verification logs. The [release-note policy](./docs/reference/versioning.md#release-notes) +owns aggregation and preview. Do not add a changelog file or release-note archive. + Durable semantic or current-snapshot changes must update their single owner in the same integration boundary. Ordinary implementation and bug fixes may be complete with code plus focused evidence. ## Local Hooks diff --git a/LICENSE.zh-CN.md b/LICENSE.zh-CN.md deleted file mode 100644 index 3071655..0000000 --- a/LICENSE.zh-CN.md +++ /dev/null @@ -1,22 +0,0 @@ -# MIT 许可证中文参考译文 - -The authoritative license is the English [LICENSE](./LICENSE). -This translation is for convenience and adds no terms or restrictions. - -本文件为方便阅读提供中文参考译文,不替代英文 [LICENSE](./LICENSE), -也不增加条件或限制。Piik 自有代码的 MIT 许可不替代 -[第三方组件的许可与声明](./licenses/README.md)。 - -版权所有 (c) 2026 TNTcraft 和 Piik 贡献者 - -特此免费授予任何获得本软件及相关文档文件(以下简称“软件”)副本的人, -不受限制地处置本软件的权利,包括但不限于使用、复制、修改、合并、出版、 -分发、再许可和/或销售本软件副本,并允许获得软件的人这样做, -但须遵守以下条件: - -上述版权声明和本许可声明应包含在本软件的所有副本或实质部分中。 - -本软件按“原样”提供,不作任何明示或默示保证,包括但不限于适销性、 -特定用途适用性和不侵权的保证。在任何情况下,无论依据合同、侵权或其他 -法律理论,作者或版权持有人均不对因软件、软件的使用或其他处置而产生、 -源自或与之有关的任何索赔、损害或其他责任负责。 diff --git a/README.md b/README.md index ba6a51f..b4b958e 100644 --- a/README.md +++ b/README.md @@ -1,83 +1,109 @@ -Piik TV logo +

Piik mascot

+

Piik

+

Share the good stuff.
Private screen sharing for you and up to 20 friends.

+

+ Website · + Download · + Gitee mirror · + Try the demo · + Documentation +

+

+ License: MIT + Up to 20 viewers + P2P-first media +

+

English · 简体中文

-# Piik +Piik is a free, open-source screen sharing tool for games, movie nights, +drawings and photos. Choose what to share and send an invitation. Your friends +watch in their browsers. -English | [简体中文](./README.zh-CN.md) +
+A little room · Show / hide animation -Share your screen. Bring your friends. +

A host with a small gold crown shares an RPG, a drawing, photos and an animated film while friends watch from the sofa.

-Piik is private screen sharing for one Host and up to 20 invited friends. -Friends watch in their browsers, with no installation. +
-```mermaid -flowchart LR - Browser[Browser] --> Host[Host a room] - App[Piik App] --> Host - Host -->|Invite link| Friends[Friends watch in browsers] +[Features](#features) · [Get started](#get-started) · [Self-hosting](#self-hosting) · [Contributing](#contributing) + +## Features + +- **Watch without installing.** Friends join by invitation in a desktop or mobile browser. +- **Share from Piik App or an existing Piik site.** Capture a screen, window or browser tab; available sources and audio depend on the platform. +- **Direct connections first.** Media travels between participants where possible (P2P). A self-hosted server can provide automatic media forwarding (SFU) as a fallback. +- **Rooms you control.** Invitations, room codes and optional room passwords, for one host and up to 20 viewers. +- **Flexible viewing.** Light and dark themes, playback controls, picture-in-picture and a connection topology view. +- **One program to host a site.** Web UI, room management and optional media forwarding are packaged together. + +

+ + + Piik room: a shared game, playback controls and friends on a sofa. The host wears a small gold crown. +
+ Interface preview · Sample room with a generated game scene +

+ +## Get started + +**To watch:** open the invitation your friend sends you. Select **Play** if the +picture does not start automatically. + +**To share:** + +| Start here | What you need to do | +| --- | --- | +| [Try the online demo](https://demo.piik.tv) | Open it in a desktop browser and start sharing. | +| [Download Piik App](https://piik.tv/#download) | Extract and open the App, choose **Public invite**, then **Open Piik** to create a temporary room. | +| Use an existing Piik site | Open the address your friend or administrator provides. [Hosting your own](./docs/operations/self-hosting.md) is an advanced option. | + +

Choose a screen, send an invitation, watch together.

+ +1. Select **Start sharing**, then choose the picture and audio to share. +2. Select **Copy invite link** and send it to your friends. Keep the sharing tab open. + +Piik uses icon controls by default; select **EN** in the header to show labels. +The App offers a local room, a temporary public link, or a connection to your own +site. Keep the App running while sharing through it. + +Download an archive beginning with **`piik-app`**. The filename also identifies the platform: + +| Filename contains | Platform | +| --- | --- | +| `windows-amd64` | Windows x64 | +| `darwin-arm64` | Apple silicon; native capture requires macOS 13+ | +| `linux-amd64` | Linux x64 | + +[**Full setup guide →**](./docs/guide/getting-started.md) · [**Open the demo →**](https://demo.piik.tv) + +Browser capture requires HTTPS or `localhost`. Windows App and desktop browser +sharing are the primary tested paths. The macOS and Linux apps have not yet been +tested on physical devices; [test results and feedback are welcome](https://github.com/TNTcraftHIM/Piik/issues). +P2P-only modes, including the App's temporary public +link and the demo, may not connect on restrictive networks. + +## Self-hosting + +Piik Server is a standalone binary with the web interface built in. +Download the Linux x64 Server package, extract it, and run: + +```sh +./piik-server ``` -## Join a friend +Open `http://localhost:8787` to try it locally. For a public site, configure your +domain, HTTPS reverse proxy and STUN address. Room data is stored in SQLite. -1. Open the invitation your friend sent. -2. Press Play if your browser asks. Use the video's sound and fullscreen controls. +[**Deploy your own site →**](./docs/operations/self-hosting.md) · [Configuration](./docs/reference/configuration.md) · [Run from source](./docs/README.md#run-from-source) -Got a room code instead? Open the same Piik site and choose **Join a room**. -Site access and the room's password settings still apply. +## Contributing -## Share from your browser - -1. Open the Piik site your group uses. Enter its site password if asked. -2. Choose **Start sharing**, pick a screen, window or tab, and check the sound switch. -3. Send the room's invitation link to your friends. Keep the sharing tab open. - -Capture needs HTTPS or `localhost`; available sources and sound depend on the -browser and platform. English, Chinese and an illustrated visual mode share the -same controls, with light and dark themes. - -## Start your own room with Piik App - -With the Piik App package for your platform: - -1. Extract the whole package, keeping `runtime` beside the executable. -2. Open `piik-app.exe` on Windows, `Piik App.app` on macOS, or - `./piik-app` on Linux. -3. Choose a mode, pick what to share, and send the invitation. Keep the app running. - -| Mode | Use it for | -| --- | --- | -| **Local room** | Friends on the same local network. An optional site password controls access. | -| **Public invite** | A temporary Internet invitation. The link lasts for this App run; media remains P2P. | -| **Connect to Site** | An existing Piik site, with App capture and media capabilities available in the same browser UI. | - -Piik App opens your system browser. Its package includes the Go application and -capture/link helpers; running it needs no Node.js, npm or Go installation. -Linux native capture uses the system Portal, PipeWire and GStreamer stack. -See the [App guide](./cmd/piik-app/README.md) for platform details. - -Temporary public links use Cloudflare Quick Tunnel, which has no uptime guarantee. -Media needs a working UDP path; restrictive networks or browser/OS suspension -can interrupt sharing. Windows App and Browser are the current acceptance -focus; macOS/Linux physical capture checks remain separate. See -[current status](./docs/status.md) for candidate and release readiness. - -## Host a site or contribute - -The Server package is one Go executable with the Web UI embedded. It owns room -signaling, STUN and optional embedded SFU fallback. Follow the -[self-hosting guide](./docs/operations/self-hosting.md) to set up your own site. - -For source builds, tests and architecture, start with the -[documentation map](./docs/README.md). [Contributing](./CONTRIBUTING.md) covers the workflow. - -## Something went wrong? - -Include your version, OS/browser, expected result and steps to reproduce. -For a local report, start Piik App with `--debug` and press `D`, or add `?debug=1` -before any `#` in the Browser page URL and use its header download button. Review reports before -sharing; see [diagnostics and export](./docs/reference/configuration.md#diagnostics). -Connection trouble? Try the [Chromium WebRTC FAQ](./cmd/piik-app/README.md#chromium-webrtc-connections). +Bug reports, documentation improvements and pull requests are welcome. +Start with [CONTRIBUTING.md](./CONTRIBUTING.md), or see the +[repository layout](./docs/reference/engineering.md#repository-layout) to find your way around. ## License -Piik-owned code is [MIT licensed](./LICENSE). Third-party components retain -their own licenses and notices: [licensing guide](./licenses/README.md). +Piik-owned code is [MIT licensed](./LICENSE). Dependencies retain their own +[licenses and notices](./licenses/README.md). diff --git a/README.zh-CN.md b/README.zh-CN.md index d47ee0d..971d275 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -1,79 +1,104 @@ -Piik 小电视标志 +

Piik 吉祥物

+

Piik

+

来,看点
好康的。

私密屏幕共享,邀请最多 20 位朋友观看。

+

+ 官网 · + 下载 · + Gitee 镜像 · + 在线演示 · + 文档 +

+

+ MIT 许可 + 最多 20 位观众 + 优先使用 P2P 连接 +

+

English · 简体中文

-# Piik +Piik 是一个免费开源的屏幕共享工具,适合游戏围观、一起看电影、展示绘画或分享照片。 +选择画面,把邀请链接发给朋友,对方用浏览器即可观看。 -[English](./README.md) | 简体中文 +
+客厅小剧场 · 展开 / 收起动画 -分享你的屏幕,叫上朋友一起看。 +

戴着小金冠的房主分享 RPG 游戏、绘画、照片和动画电影,朋友们坐在沙发上观看。

-Piik 是一个私密屏幕分享工具:一位房主,最多 20 位受邀好友。 -朋友用浏览器就能观看,无须安装。 +
-```mermaid -flowchart LR - Browser[浏览器] --> Host[开房分享] - App[Piik App] --> Host - Host -->|邀请链接| Friends[好友用浏览器观看] +[功能](#功能) · [开始使用](#开始使用) · [自行部署](#自行部署) · [参与贡献](#参与贡献) + +## 功能 + +- **观看无须安装。** 朋友通过邀请链接,用电脑或手机浏览器加入。 +- **用客户端或已有 Piik 站点分享。** 支持屏幕、窗口和浏览器标签页;可选来源与音频能力取决于平台。 +- **优先直接连接。** 画面优先在设备之间传输(P2P),自建站点可启用媒体转发(SFU)自动兜底。 +- **房间由你管理。** 支持邀请链接、房间号和可选的房间密码,一位房主、最多 20 位观众。 +- **灵活的观看方式。** 支持深浅主题、播放控制、画中画和连接拓扑查看。 +- **一个程序就能建站。** 网页、房间管理和可选的媒体转发打包在同一服务端程序中。 + +

+ + + Piik 房间:共享的游戏画面、播放控件和坐在沙发上的朋友,房主戴着小金冠。 +
+ 界面预览 · 示例房间与生成的游戏画面 +

+ +## 开始使用 + +**观看分享:** 打开朋友发来的邀请链接。画面没有自动播放时,点击**播放**即可。 + +**分享画面:** + +| 从这里开始 | 需要做什么 | +| --- | --- | +| [体验在线 Demo](https://demo.piik.tv) | 在电脑浏览器打开,开始分享。 | +| [下载 Piik App](https://piik.tv/?lang=zh-CN#download) | 解压并启动客户端,选择**公网邀请**,点击**进入 Piik**,创建临时房间。 | +| 使用已有 Piik 站点 | 打开朋友或管理员提供的地址。[自行部署](./docs/operations/self-hosting.zh-CN.md)属于进阶操作。 | + +

选择画面,发送邀请,一起观看。

+ +1. 点击**开始分享**,选择要分享的画面和声音。 +2. 点击**复制邀请链接**,发给朋友。分享期间保持标签页打开。 + +Piik 默认使用图示按钮;点击顶部的**中**可显示文字。 +客户端提供本地房间、临时公网邀请和连接已有站点三种模式,分享期间需保持 App 运行。 + +下载以 **`piik-app`** 开头的压缩包,再按文件名中的平台标识选择: + +| 文件名包含 | 适用平台 | +| --- | --- | +| `windows-amd64` | Windows x64 | +| `darwin-arm64` | Apple 芯片;原生采集需 macOS 13 及以上 | +| `linux-amd64` | Linux x64 | + +[**完整使用教程 →**](./docs/guide/getting-started.zh-CN.md) · [**打开在线演示 →**](https://demo.piik.tv) + +浏览器采集需要 HTTPS 或 `localhost`。目前主要测试 Windows 客户端和桌面浏览器分享。 +macOS 和 Linux 客户端尚未经过实机测试,欢迎有设备的朋友试用并[反馈问题](https://github.com/TNTcraftHIM/Piik/issues)。 +客户端公网邀请和演示站使用纯 P2P 连接, +在受限网络下可能无法连通。 + +## 自行部署 + +Piik Server 是一个内置网页界面的独立程序。 +下载 Linux x64 服务端程序包,解压后运行: + +```sh +./piik-server ``` -## 去朋友的房间 +打开 `http://localhost:8787` 即可在本机试用。对外提供服务时, +再配置域名、HTTPS 反向代理和 STUN 地址。房间数据保存在 SQLite 中。 -1. 打开朋友发来的邀请链接。 -2. 如果浏览器提示,点击播放。声音和全屏由视频控件控制。 +[**部署自己的站点 →**](./docs/operations/self-hosting.zh-CN.md) · [配置参考](./docs/reference/configuration.md) · [从源码运行](./docs/README.md#run-from-source) -只有房间号?打开同一个 Piik 站点,选择 **加入房间**。 -站点访问权限和房间密码设置仍然适用。 +## 参与贡献 -## 用浏览器分享 - -1. 打开你们使用的 Piik 站点,按提示输入站点密码。 -2. 点击 **开始分享**,选择屏幕、窗口或标签页,并检查声音开关。 -3. 把房间邀请链接发给朋友,保持分享标签页打开。 - -浏览器采集需要 HTTPS 或 `localhost`;可选来源和声音支持取决于浏览器与平台。 -界面提供中文、英文和图示模式,以及明暗主题。 - -## 用 App 自己开房 - -拿到对应平台的 App 程序包后: - -1. 完整解压,保留可执行文件旁的 `runtime` 目录。 -2. Windows 打开 `piik-app.exe`,macOS 打开 `Piik App.app`, - Linux 运行 `./piik-app`。 -3. 选择模式和分享来源,发出邀请。分享期间保持 App 运行。 - -| 模式 | 适合什么情况 | -| --- | --- | -| **本地房间** | 与同一局域网内的好友分享,可设置站点访问密码。 | -| **公网邀请** | 生成本次 App 运行期间有效的临时邀请链接;媒体仍走 P2P。 | -| **连接站点** | 使用已有 Piik 站点,并在同一浏览器界面使用 App 的采集与媒体能力。 | - -App 会打开系统浏览器。程序包包含 Go 应用和采集、临时链接辅助程序, -运行时无须安装 Node.js、npm 或 Go。Linux 原生采集使用系统的 Portal、PipeWire -和 GStreamer,平台细节见 [App 指南](./cmd/piik-app/README.md)。 - -临时公网链接使用 Cloudflare Quick Tunnel,不保证持续可用。 -媒体需要可用的 UDP 路径;网络限制、浏览器或系统挂起都可能中断分享。 -当前以 Windows App 和浏览器为验收重点,macOS/Linux 原生采集的实机验证另行进行。 -候选版本与发布准备情况见 [当前状态](./docs/status.md)。 - -## 自建站点或参与开发 - -Server 是一个内嵌 Web 界面的 Go 可执行文件,负责房间信令、STUN 和可选的内置 -SFU 后备路径。搭建自己的站点,请看 [自托管指南](./docs/operations/self-hosting.md)。 - -源码运行、测试和架构从 [文档地图](./docs/README.md) 开始; -[贡献指南](./CONTRIBUTING.md) 说明协作流程。 - -## 遇到问题? - -反馈时附上版本、系统与浏览器、预期结果和复现步骤。 -App 可用 `--debug` 启动后按 `D` 导出本地报告;浏览器网址加上 `?debug=1` -后(放在 `#` 之前),可点击顶部下载按钮。分享前请检查报告内容,详见 -[诊断与导出](./docs/reference/configuration.md#diagnostics)。 -连接不上时,也可以查看 [Chromium WebRTC 常见问题](./cmd/piik-app/README.md#chromium-webrtc-connections)。 +欢迎提交问题、改进文档或参与开发。 +修改前请阅读[贡献指南](./CONTRIBUTING.md),也可以从 +[项目目录说明](./docs/reference/engineering.md#repository-layout)了解代码结构。 ## 许可 -Piik 自有代码采用 [MIT 许可](./LICENSE),提供 [中文参考译文](./LICENSE.zh-CN.md)。 -第三方组件保留各自的许可与声明,详见 [许可说明](./licenses/README.md)。 +Piik 自有代码采用 [MIT 许可](./LICENSE),依赖保留各自的[许可与声明](./licenses/README.md)。 diff --git a/cmd/piik-app/README.md b/cmd/piik-app/README.md index c37a393..bcd5b37 100644 --- a/cmd/piik-app/README.md +++ b/cmd/piik-app/README.md @@ -1,21 +1,20 @@ # Piik App -Piik App is the self-contained and native-capability runtime for the same -Browser application used by Hosted Piik. It does not implement another UI, -room store, signaling protocol, or route controller. +Piik App provides local rooms and native screen capture through the Piik web +interface in your system Browser. ## Run A Package Extract the matching platform bundle in full and keep `runtime` beside the App executable. Run `piik-app.exe` on Windows, `./piik-app` on -Linux, or `Piik App.app` on macOS. The launcher opens in the system Browser; -the App does not embed a browser UI. Packaged execution needs no Node.js, npm, -or Go installation. Linux native capture uses the system dependencies described +Linux, or `Piik App.app` on macOS. The launcher opens in the system Browser. +Linux native capture uses the system dependencies described in the [Linux capture guide](../../native/capture/linux/README.md). -Windows App and Browser are this phase's acceptance targets. Other platform -builds do not establish physical capture/audio acceptance; see -[current status](../../docs/status.md). Package construction and public Release +Windows App and browser sharing are the primary tested paths. The macOS and +Linux apps have not yet been tested on physical devices; test results and +[feedback](https://github.com/TNTcraftHIM/Piik/issues) are welcome. macOS native +capture requires Apple silicon and macOS 13 or newer. Package construction and public Release publication are separate steps in [deployment](../../docs/deployment.md). ## Modes @@ -25,14 +24,12 @@ publication are separate steps in [deployment](../../docs/deployment.md). Site, then enters the normal Host page. - The launcher remembers the Site address but keeps the per-run room source separate. The App RPC starts before this choice and accepts the saved Site - alongside the Local Host origin, so Local or Public invite does not disable - Site enhancement. An App-opened Site remembers the opt-in in that Browser + alongside the Local Host origin. An App-opened Site remembers the opt-in in that Browser origin; later manually opened Host pages may reuse the running App. -- `--site`, `--local`, and `--link` remain deterministic automation inputs for - CI and development. They are not required for normal use. -- The default launcher checks the official GitHub Releases metadata after it - opens and shows a notice when a newer full-SHA release exists. The check is - best-effort and never installs or replaces the App. +- The `--site`, `--local`, and `--link` flags select a mode for CI and development. +- The default launcher checks official releases after it opens, using GitHub + first and Gitee if GitHub is unavailable. It links to an available release + without installing or replacing the App. Local mode uses memory-only rooms, P2P relay, no SFU listener, and no public discovery. Ordinary Local works on a reachable LAN. The **Public invite** mode @@ -72,9 +69,8 @@ reuse suitable H.264/VP8 outputs and derive a missing lower output only for direct-child demand. Compatible children share that output; each edge receives only its selected representation. SFU publication combines its requested output prefix with one aggregate upstream budget. A lower-output constraint does not -replace the original input or higher sibling outputs. The existing route -controller still owns persistent quality evidence and any route replacement; -there is no room-wide score or periodic rebalance. See +replace the original input or higher sibling outputs. The route controller owns +persistent quality evidence and route replacement. See [media quality](../../docs/product/media-quality.md) for the implemented behavior and [status](../../docs/status.md) for its acceptance limits. @@ -89,7 +85,7 @@ The terminal shows the current mode, entry links and startup state; its language follows the launcher and App-enabled pages. Press `o` to reopen the Browser, or `q` / Ctrl+C to end Local rooms and stop the local server and temporary public link. Plain-text output -uses Ctrl+C. A Site-loaded Browser tab does not own the App process. +uses Ctrl+C. The App keeps running after a Site tab closes. For one-link Internet sharing, open the App launcher, choose **Public invite**, create a room in the opened Browser, and send its normal invitation link. The @@ -106,8 +102,7 @@ use a configured Site when persistent control availability or SFU fallback matte Chromium-based Browsers can restrict WebRTC UDP through Browser settings, extensions or managed policies. Disabling non-proxied UDP can prevent even the local Browser-to-App media connection; successful capture or page loading -does not prove that this separate connection is available. This is not specific -to one Browser brand or to VPN use. +does not prove that this separate connection is available. Check the Browser's WebRTC/IP-handling policy and any extension's WebRTC or IP-leak protection setting. Restore a policy that permits WebRTC UDP, reload @@ -170,12 +165,21 @@ admits at most two independent strict v9 control sessions. After `hello`, each session may list local capture choices, own one generation-fenced Host share, or receive one native Viewer source and its bounded encoded child edges. Closing one session retires only its resources. Its public `instanceToken` -distinguishes the discovered process but is not authentication; room authority -and remote signaling remain in the Browser. Viewer receive/relay remains +distinguishes the discovered process but is not authentication. The shared Go +service owns room authority; the Browser owns the room protocol client and +orchestrates native media through this control session. Viewer receive/relay remains available even when this machine has no accepted native capture encoder. Capture sidecars must match the App's current probe/encoded-output contract; the package-candidate wrapper validates that version before accepting its artifact. +Packaged builds report the product version and source SHA in the terminal and +diagnostic context. The launcher uses that identity to distinguish a newer +version, a different build of the same version, and an official release offered +to a development build; the link never installs or interrupts a share. +[Versioning](../../docs/reference/versioning.md) +owns these meanings and [GitHub operations](../../docs/operations/github.md) +owns automatic publication. + ## Packaging From a clean revision, build the application release and run the platform's diff --git a/cmd/piik-server/main.go b/cmd/piik-server/main.go index ae18948..43cfe90 100644 --- a/cmd/piik-server/main.go +++ b/cmd/piik-server/main.go @@ -1,10 +1,10 @@ // Command piik-server is the Hosted Piik entry point: it loads the // environment, serves the embedded Browser UI together with the signaling and -// room API, and stops on SIGINT or SIGTERM. It replaces src/server/index.ts. +// room API, and stops on SIGINT or SIGTERM. // // Two maintenance modes exit without serving: --check-config validates the // environment (the release wrapper runs it as the service user before cutover) -// and --check-release compares the deployed revision with the latest published +// and --check-release compares this binary with the latest published // release (see release.go). package main @@ -37,17 +37,16 @@ const shutdownTimeout = 15 * time.Second // release directory. Hosted keeps its real values in the service secret store. const environmentFile = ".env" -// BuildRevision is the full Git revision the release packager links in with -// -ldflags. It matches the REVISION file shipped beside the binary and is -// reported at startup so one journal line identifies the running release. -var BuildRevision = "development" +// The packager injects the product version and full source revision together. +var ( + BuildVersion = "development" + BuildRevision = "development" +) func main() { checkConfig := flag.Bool("check-config", false, "validate the environment and exit") checkRelease := flag.Bool("check-release", false, "report whether a newer published release exists and exit") - currentFile := flag.String("current-file", "", - "path to the deployed REVISION file read by --check-release") apiURL := flag.String("api-url", defaultReleaseAPIURL, "release metadata endpoint used by --check-release") debug := flag.Bool("debug", false, "save opt-in server diagnostics to rotated files") @@ -58,7 +57,7 @@ func main() { } switch { case *checkRelease: - os.Exit(checkDeployedRelease(context.Background(), *currentFile, *apiURL, os.Stdout)) + os.Exit(checkDeployedRelease(context.Background(), *apiURL, os.Stdout)) case *checkConfig: if _, err := config.Load(environment()); err != nil { fail(err) @@ -98,14 +97,16 @@ func serve(debug bool) (returnedErr error) { defer func() { stopExport() _ = dependencyLog.Close() - logger.Info("piik-server", "event", "stopped", "failed", returnedErr != nil, diagnostics.Error(returnedErr)) returnedErr = errors.Join(returnedErr, exportServerDiagnostics(recorder), recorder.Close()) slog.SetDefault(previous) log.SetOutput(previousWriter) log.SetFlags(previousFlags) }() - logger.Info("piik-server", "event", "start", "revision", BuildRevision) + logger.Info("piik-server", "event", "start", "version", BuildVersion, "revision", BuildRevision) } + defer func() { + logger.Info("piik-server", "event", "stopped", "failed", returnedErr != nil, diagnostics.Error(returnedErr)) + }() configuration, err := config.Load(environment()) if err != nil { return err @@ -133,11 +134,26 @@ func serve(debug bool) (returnedErr error) { if err != nil { return err } - fmt.Printf("Piik %s is listening on %s:%d; public URL %s\n", - BuildRevision, configuration.ListenHost, port, config.Origin(configuration.PublicBaseURL)) + logger.Info("Piik server is listening", "event", "ready", "version", BuildVersion, "revision", BuildRevision, + "host", configuration.ListenHost, "port", port, "publicUrl", config.Origin(configuration.PublicBaseURL), + "sqlite", configuration.RoomDatabasePath != "", "siteAccessProtected", configuration.SiteAccessPassword != "", + "sfu", configuration.SFU != nil, "natPrediction", configuration.NATPredictionEnabled) ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) - defer stop() + // One bounded background lookup never delays serving. Join it before closing + // diagnostic outputs so shutdown cannot leave a late writer behind. + releaseChecked := make(chan struct{}) + go func() { + defer close(releaseChecked) + result := checkRelease(ctx, BuildVersion, BuildRevision, defaultReleaseAPIURL, os.Getenv("GITHUB_TOKEN"), mirrorReleaseAPIURL) + if ctx.Err() == nil { + logReleaseNotice(logger, result) + } + }() + defer func() { + stop() + <-releaseChecked + }() <-ctx.Done() shutdownCtx, cancel := context.WithTimeout(context.Background(), shutdownTimeout) defer cancel() @@ -185,9 +201,9 @@ func environment() map[string]string { return values } -// loadEnvironmentFile ports the src/server/index.ts loadEnvFile() call: an -// absent file is the ordinary case and every other read failure stops startup. -// Like Node, a name the process already carries is never overwritten, so a +// loadEnvironmentFile reads optional startup overrides. An absent file is the +// ordinary case and every other read failure stops startup. A name the process +// already carries is never overwritten, so a // systemd EnvironmentFile still wins over a stale file in the release // directory. The accepted syntax is DECISIONS D8: KEY=value lines, whole-line // '#' comments, an optional 'export ' prefix, and a single- or double-quoted diff --git a/cmd/piik-server/release.go b/cmd/piik-server/release.go index eb4d188..e9c818e 100644 --- a/cmd/piik-server/release.go +++ b/cmd/piik-server/release.go @@ -6,86 +6,66 @@ import ( "errors" "fmt" "io" + "log/slog" "net/http" "net/url" "os" "regexp" "strings" "time" + + "golang.org/x/mod/semver" ) -// defaultReleaseAPIURL is DEFAULT_RELEASE_API_URL: the same published release -// contract the Browser update notice uses (src/client/lib/release-update.ts). +// The Browser notice and operator checker read the same published release. const defaultReleaseAPIURL = "https://api.github.com/repos/TNTcraftHIM/Piik/releases/latest" +const mirrorReleaseAPIURL = "https://gitee.com/api/v5/repos/TNTcraftHIM/Piik/releases" -// Exit codes are UPDATE_EXIT_CODES. deploy/check-release.sh forwards them, so a -// checker failure stays distinguishable from "an update exists". +// deploy/check-release.sh forwards these codes without installing anything. const ( exitUpToDate = 0 exitUpdateAvailable = 10 exitUpdateUnavailable = 20 - // exitUsage is the exit code parseArguments set when --current-file was - // missing. Without it a forgotten argument reads as an unavailable check. - exitUsage = 2 + releaseTimeout = 5 * time.Second + releaseURLPrefix = "https://github.com/TNTcraftHIM/Piik/releases/tag/" + releaseAPIHost = "api.github.com" + mirrorReleasePrefix = "https://gitee.com/TNTcraftHIM/Piik/releases/tag/" ) -// releaseUsage is the usage string parseArguments threw, with this binary's -// name in place of the script's. -const releaseUsage = "Usage: piik-server --check-release --current-file [--api-url ]" - -const ( - // releaseTimeout bounds the whole check; an operator runs it interactively. - releaseTimeout = 5 * time.Second - // releasePathPrefix is RELEASE_PATH_PREFIX: the only accepted release page. - releasePathPrefix = "/TNTcraftHIM/Piik/releases/tag/" - releaseHost = "github.com" - releaseAPIHost = "api.github.com" -) - -// fullRevisionPattern is FULL_REVISION applied to an already lowercased value, -// which is why the JavaScript case-insensitive flag has no counterpart here. var fullRevisionPattern = regexp.MustCompile(`^[0-9a-f]{40}$`) +var mirrorSourcePattern = regexp.MustCompile(`(?m)^\r?$`) -// Release status values are the "status" field of the printed JSON line, which -// deploy/check-release.sh reads; each maps to one exit code above. const ( statusUpToDate = "up-to-date" statusUpdateAvailable = "update-available" + statusDifferentBuild = "different-build" + statusOfficialRelease = "official-release" statusUnavailable = "unavailable" ) -// releaseResult is the single JSON line the checker prints. Field order is the -// JSON.stringify order of the ported object, and the pointers are the nulls: a -// value the check could not establish is present and null, never omitted. +// Unknown identity fields are explicit nulls in the single printed JSON line. type releaseResult struct { Status string `json:"status"` + CurrentVersion *string `json:"currentVersion"` + LatestVersion *string `json:"latestVersion"` CurrentRevision *string `json:"currentRevision"` LatestRevision *string `json:"latestRevision"` ReleaseURL *string `json:"releaseUrl"` } -// checkDeployedRelease is the --check-release entry point. It writes one JSON -// line and returns the process exit code. An unreadable revision file is an -// unavailable check rather than an update, which is what the ported script's -// silently swallowed read failure meant; an absent --current-file is a usage -// error, which is what parseArguments made it. -func checkDeployedRelease(ctx context.Context, currentFile, apiURL string, out io.Writer) int { - if currentFile == "" { - fmt.Fprintln(os.Stderr, releaseUsage) - return exitUsage +func checkDeployedRelease(ctx context.Context, apiURL string, out io.Writer) int { + mirrorURL := "" + if apiURL == defaultReleaseAPIURL { + mirrorURL = mirrorReleaseAPIURL } - current := "" - if content, err := os.ReadFile(currentFile); err == nil { - current = string(content) - } - result := checkRelease(ctx, current, apiURL, os.Getenv("GITHUB_TOKEN")) + result := checkRelease(ctx, BuildVersion, BuildRevision, apiURL, os.Getenv("GITHUB_TOKEN"), mirrorURL) encoder := json.NewEncoder(out) encoder.SetEscapeHTML(false) if err := encoder.Encode(result); err != nil { return exitUpdateUnavailable } switch result.Status { - case statusUpdateAvailable: + case statusUpdateAvailable, statusDifferentBuild, statusOfficialRelease: return exitUpdateAvailable case statusUpToDate: return exitUpToDate @@ -94,149 +74,213 @@ func checkDeployedRelease(ctx context.Context, currentFile, apiURL string, out i } } -// checkRelease ports checkRelease. Every failure - an unusable current -// revision, an unusable endpoint, a redirect, a timeout, a non-2xx reply or -// metadata that fails validation - collapses to the same unavailable result: -// the checker never guesses that an update exists. -func checkRelease(ctx context.Context, currentRevision, apiURL, token string) releaseResult { - current := normalizeRevision(currentRevision) - if current == "" { - return unavailableResult(currentRevision) +func checkRelease(ctx context.Context, currentVersion, currentRevision, apiURL, token, mirrorURL string) releaseResult { + result := unavailableResult(currentVersion, currentRevision) + comparableVersion := result.CurrentVersion != nil && semver.IsValid(*result.CurrentVersion) + if !comparableVersion && result.CurrentRevision == nil { + return result } + version, revision, releaseURL, ok := fetchLatestRelease(ctx, apiURL, token, false) + if !ok && mirrorURL != "" { + version, revision, releaseURL, ok = fetchLatestRelease(ctx, mirrorURL, "", true) + } + if !ok { + return result + } + result.LatestVersion = &version + result.LatestRevision = revisionOrNull(revision) + result.ReleaseURL = &releaseURL + if comparableVersion { + result.Status = statusUpToDate + order := semver.Compare(version, *result.CurrentVersion) + if order > 0 { + result.Status = statusUpdateAvailable + } else if order == 0 && result.CurrentRevision != nil && result.LatestRevision != nil && + *result.CurrentRevision != *result.LatestRevision { + result.Status = statusDifferentBuild + } + } else if result.CurrentRevision != nil && result.LatestRevision != nil { + result.Status = statusOfficialRelease + if *result.CurrentRevision == *result.LatestRevision { + result.Status = statusUpToDate + } + } + return result +} + +// Routine startup only announces actionable release choices. An unavailable +// provider is not a server failure, and never changes the running installation. +func logReleaseNotice(logger *slog.Logger, result releaseResult) { + var message string + switch result.Status { + case statusUpdateAvailable: + message = "A newer Piik release is available" + case statusDifferentBuild: + message = "This Piik build differs from the official release" + case statusOfficialRelease: + message = "An official Piik release is available" + default: + return + } + current := "development" + if result.CurrentVersion != nil { + current = *result.CurrentVersion + } + logger.Info(message, "event", "release-check", "status", result.Status, + "currentVersion", current, "latestVersion", *result.LatestVersion, "releaseUrl", *result.ReleaseURL) +} + +func fetchLatestRelease(ctx context.Context, apiURL, token string, mirror bool) (version, revision, releaseURL string, ok bool) { endpoint, err := url.Parse(apiURL) if err != nil || (endpoint.Scheme != "https" && endpoint.Scheme != "http") { - return unavailableResult(current) + return "", "", "", false } - ctx, cancel := context.WithTimeout(ctx, releaseTimeout) defer cancel() + for page := 1; page <= 10; page++ { + if mirror { + query := endpoint.Query() + query.Set("per_page", "100") + query.Set("page", fmt.Sprint(page)) + query.Set("direction", "desc") + endpoint.RawQuery = query.Encode() + } + body, err := readReleaseResponse(ctx, endpoint, token, mirror) + if err != nil { + return "", "", "", false + } + if !mirror { + return parseReleaseMetadata(body) + } + var releases []json.RawMessage + if err := json.Unmarshal(body, &releases); err != nil || releases == nil || len(releases) > 100 { + return "", "", "", false + } + for _, payload := range releases { + candidateVersion, candidateRevision, candidateURL, valid := parseMirrorReleaseMetadata(payload) + if valid && (!ok || semver.Compare(candidateVersion, version) > 0) { + version, revision, releaseURL, ok = candidateVersion, candidateRevision, candidateURL, true + } + } + if len(releases) < 100 { + return + } + } + // Incomplete pagination cannot establish the highest stable version. + return "", "", "", false +} + +func readReleaseResponse(ctx context.Context, endpoint *url.URL, token string, mirror bool) ([]byte, error) { request, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint.String(), nil) if err != nil { - return unavailableResult(current) + return nil, err } - request.Header.Set("Accept", "application/vnd.github+json") - request.Header.Set("X-GitHub-Api-Version", "2022-11-28") + request.Header.Set("Accept", "application/json") request.Header.Set("User-Agent", "Piik-release-check") - if authorizesGitHub(endpoint, token) { - request.Header.Set("Authorization", "Bearer "+strings.TrimSpace(token)) + if !mirror { + request.Header.Set("Accept", "application/vnd.github+json") + request.Header.Set("X-GitHub-Api-Version", "2022-11-28") + if authorizesGitHub(endpoint, token) { + request.Header.Set("Authorization", "Bearer "+strings.TrimSpace(token)) + } } - client := &http.Client{CheckRedirect: func(*http.Request, []*http.Request) error { - // The ported redirect: "error" policy. A moved release endpoint is not - // followed, so the Authorization header cannot travel to another origin. return errors.New("release endpoint redirected") }} response, err := client.Do(request) if err != nil { - return unavailableResult(current) + return nil, err } defer response.Body.Close() if response.StatusCode < 200 || response.StatusCode > 299 { - return unavailableResult(current) + return nil, errors.New("release endpoint unavailable") } - body, err := io.ReadAll(response.Body) - if err != nil { - return unavailableResult(current) - } - latest, releaseURL, ok := parseReleaseMetadata(body) - if !ok { - return unavailableResult(current) - } - status := statusUpdateAvailable - if latest == current { - status = statusUpToDate - } - return releaseResult{ - Status: status, - CurrentRevision: ¤t, - LatestRevision: &latest, - ReleaseURL: &releaseURL, + const limit = 4 << 20 + body, err := io.ReadAll(io.LimitReader(response.Body, limit+1)) + if err != nil || len(body) > limit { + return nil, errors.New("release metadata unreadable or too large") } + return body, nil } -// authorizesGitHub reports whether the operator token may be sent to endpoint. -// A fixture, mirror or plaintext endpoint must never receive it, which is why -// the scheme and the host are both part of the rule. +// Only the HTTPS GitHub API receives an operator token, and redirects are refused. func authorizesGitHub(endpoint *url.URL, token string) bool { return endpoint.Scheme == "https" && strings.ToLower(endpoint.Hostname()) == releaseAPIHost && strings.TrimSpace(token) != "" } -// unavailableResult is unavailableResult: the current revision is reported when -// it is well formed and null otherwise. -func unavailableResult(currentRevision string) releaseResult { - return releaseResult{Status: statusUnavailable, CurrentRevision: revisionOrNull(currentRevision)} +func unavailableResult(currentVersion, currentRevision string) releaseResult { + currentVersion = strings.TrimSpace(currentVersion) + if canonical := semver.Canonical(currentVersion); canonical != "" { + currentVersion = canonical + } + result := releaseResult{Status: statusUnavailable, CurrentRevision: revisionOrNull(currentRevision)} + if currentVersion != "" { + result.CurrentVersion = ¤tVersion + } + return result } func revisionOrNull(value string) *string { - revision := normalizeRevision(value) - if revision == "" { + revision := strings.ToLower(strings.TrimSpace(value)) + if !fullRevisionPattern.MatchString(revision) { return nil } return &revision } -// normalizeRevision ports normalizeRevision, returning "" where it returned -// null: only a full 40-hex commit identifies a release. -func normalizeRevision(value string) string { - revision := strings.ToLower(strings.TrimSpace(value)) - if !fullRevisionPattern.MatchString(revision) { - return "" - } - return revision -} - -// releaseMetadata is the subset of the GitHub release payload that is read. -// A tag_name or html_url of another type fails to decode, which is the -// rejection the JavaScript reached through typeof. Deviation: the JavaScript -// tested `draft === true`, so it read a draft of another type as "not a -// draft"; here such a payload fails to decode and the check reports -// unavailable. Only stricter, and a release payload the checker cannot -// understand must never be reported as an update. type releaseMetadata struct { - TagName *string `json:"tag_name"` - HTMLURL *string `json:"html_url"` - Draft *bool `json:"draft"` - Prerelease *bool `json:"prerelease"` + TagName string `json:"tag_name"` + HTMLURL string `json:"html_url"` + TargetCommitish string `json:"target_commitish"` + Draft bool `json:"draft"` + Prerelease bool `json:"prerelease"` } -// parseReleaseMetadata ports parseReleaseMetadata. It accepts one shape only: a -// published, non-draft release whose tag is a full commit revision and whose -// page is exactly https://github.com/TNTcraftHIM/Piik/releases/tag/ with no credentials, port, query or fragment. The returned URL is -// rebuilt from the validated parts so nothing unverified reaches the operator. -func parseReleaseMetadata(payload []byte) (revision string, releaseURL string, ok bool) { +// A branch-valued target_commitish is unknown provenance, never a source SHA. +func parseReleaseMetadata(payload []byte) (version, revision, releaseURL string, ok bool) { var metadata releaseMetadata if err := json.Unmarshal(payload, &metadata); err != nil { - return "", "", false + return "", "", "", false } - if (metadata.Draft != nil && *metadata.Draft) || - (metadata.Prerelease != nil && *metadata.Prerelease) { - return "", "", false - } - if metadata.TagName == nil || metadata.HTMLURL == nil { - return "", "", false - } - revision = normalizeRevision(*metadata.TagName) - if revision == "" { - return "", "", false - } - parsed, err := url.Parse(*metadata.HTMLURL) - if err != nil || parsed.Scheme != "https" || - strings.ToLower(parsed.Hostname()) != releaseHost || parsed.User != nil || - parsed.Port() != "" || parsed.RawQuery != "" || parsed.Fragment != "" { - return "", "", false - } - // The escaped path is compared, so an encoded separator such as %2F cannot - // smuggle another path past the prefix the way the decoded form would. - path := parsed.EscapedPath() - if !strings.HasPrefix(path, releasePathPrefix) { - return "", "", false - } - tag, err := url.PathUnescape(strings.TrimPrefix(path, releasePathPrefix)) - if err != nil || normalizeRevision(tag) != revision { - return "", "", false - } - return revision, "https://" + releaseHost + path, true + return normalizeReleaseMetadata(metadata) +} + +func normalizeReleaseMetadata(metadata releaseMetadata) (version, revision, releaseURL string, ok bool) { + if metadata.Draft || metadata.Prerelease { + return "", "", "", false + } + version = metadata.TagName + if version == "" || semver.Canonical(version) != version || semver.Prerelease(version) != "" || + metadata.HTMLURL != releaseURLPrefix+version { + return "", "", "", false + } + if source := revisionOrNull(metadata.TargetCommitish); source != nil { + revision = *source + } + return version, revision, metadata.HTMLURL, true +} + +func parseMirrorReleaseMetadata(payload []byte) (version, revision, releaseURL string, ok bool) { + var metadata struct { + TagName string `json:"tag_name"` + Body string `json:"body"` + Prerelease *bool `json:"prerelease"` + } + if err := json.Unmarshal(payload, &metadata); err != nil || metadata.Prerelease == nil || *metadata.Prerelease { + return "", "", "", false + } + sources := mirrorSourcePattern.FindAllStringSubmatch(metadata.Body, -1) + if len(sources) != 1 { + return "", "", "", false + } + // The mirror's target_commitish identifies its README, not the original source. + version, revision, _, ok = normalizeReleaseMetadata(releaseMetadata{ + TagName: metadata.TagName, TargetCommitish: sources[0][1], HTMLURL: releaseURLPrefix + metadata.TagName, + }) + if !ok { + return "", "", "", false + } + return version, revision, mirrorReleasePrefix + version, true } diff --git a/cmd/piik-server/release_test.go b/cmd/piik-server/release_test.go index 53f9e52..0a0feb9 100644 --- a/cmd/piik-server/release_test.go +++ b/cmd/piik-server/release_test.go @@ -4,202 +4,174 @@ import ( "bytes" "context" "encoding/json" + "log/slog" "net/http" "net/http/httptest" "net/url" "os" - "path/filepath" "strings" "testing" ) const ( + deployedVersion = "v1.9.0" + latestVersion = "v1.10.0" deployedRevision = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" latestRevision = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" - latestReleaseURL = "https://github.com/TNTcraftHIM/Piik/releases/tag/" + latestRevision - latestRelease = `{"tag_name":"` + latestRevision + `","html_url":"` + latestReleaseURL + `"}` + latestReleaseURL = "https://github.com/TNTcraftHIM/Piik/releases/tag/" + latestVersion + latestRelease = `{"tag_name":"` + latestVersion + `","html_url":"` + latestReleaseURL + `","target_commitish":"` + latestRevision + `"}` ) +func TestReleaseNoticeUsesOperatorLogsOnlyForAvailableChoices(t *testing.T) { + for _, status := range []string{statusUpdateAvailable, statusDifferentBuild, statusOfficialRelease, statusUpToDate, statusUnavailable} { + t.Run(status, func(t *testing.T) { + var output bytes.Buffer + version, releaseURL := latestVersion, latestReleaseURL + logReleaseNotice(slog.New(slog.NewJSONHandler(&output, nil)), releaseResult{ + Status: status, LatestVersion: &version, ReleaseURL: &releaseURL, + }) + if status == statusUpToDate || status == statusUnavailable { + if output.Len() != 0 { + t.Fatalf("non-actionable check produced a notice: %s", output.String()) + } + return + } + var entry map[string]any + if err := json.Unmarshal(output.Bytes(), &entry); err != nil { + t.Fatal(err) + } + if entry["status"] != status || entry["currentVersion"] != "development" || + entry["latestVersion"] != latestVersion || entry["releaseUrl"] != latestReleaseURL || entry["level"] != "INFO" { + t.Fatalf("release notice lost its version or download action: %v", entry) + } + if status == statusDifferentBuild && strings.Contains(entry["msg"].(string), "newer") { + t.Fatal("SHA inequality was presented as release ordering") + } + }) + } +} + func TestParseReleaseMetadataAcceptsOnlyTheStrictReleaseIdentity(t *testing.T) { - tagged := func(page string) string { - return `{"tag_name":"` + latestRevision + `","html_url":"` + page + `"}` + tagged := func(version, page string) string { + return `{"tag_name":"` + version + `","html_url":"` + page + `"}` } testCases := []struct { - name string - payload string - revision string - url string + name, payload, version, revision, url string }{ - {"published release", latestRelease, latestRevision, latestReleaseURL}, - { - "uppercase tag normalizes", - `{"tag_name":"` + strings.ToUpper(latestRevision) + `","html_url":"` + latestReleaseURL + `"}`, - latestRevision, latestReleaseURL, - }, - {"short tag", `{"tag_name":"short","html_url":"` + latestReleaseURL + `"}`, "", ""}, - {"other host", tagged("https://evil.example"), "", ""}, - { - "userinfo", - tagged("https://user@github.com/TNTcraftHIM/Piik/releases/tag/" + latestRevision), - "", "", - }, - { - "port", - tagged("https://github.com:8443/TNTcraftHIM/Piik/releases/tag/" + latestRevision), - "", "", - }, - { - "plaintext page", - tagged("http://github.com/TNTcraftHIM/Piik/releases/tag/" + latestRevision), - "", "", - }, - {"query", tagged(latestReleaseURL + "?asset=1"), "", ""}, - {"fragment", tagged(latestReleaseURL + "#assets"), "", ""}, - { - "encoded path separator", - tagged("https://github.com/TNTcraftHIM%2FPiik/releases/tag/" + latestRevision), - "", "", - }, - { - "page tag disagrees with the release tag", - tagged("https://github.com/TNTcraftHIM/Piik/releases/tag/" + deployedRevision), - "", "", - }, - { - "draft", - `{"tag_name":"` + latestRevision + `","html_url":"` + latestReleaseURL + `","draft":true}`, - "", "", - }, - { - "prerelease", - `{"tag_name":"` + latestRevision + `","html_url":"` + latestReleaseURL + `","prerelease":true}`, - "", "", - }, - {"missing page", `{"tag_name":"` + latestRevision + `"}`, "", ""}, - {"array payload", `[]`, "", ""}, - {"not an object", `"released"`, "", ""}, - {"not json", `nonsense`, "", ""}, + {"published release", latestRelease, latestVersion, latestRevision, latestReleaseURL}, + {"uppercase source normalizes", strings.Replace(latestRelease, latestRevision, strings.ToUpper(latestRevision), 1), latestVersion, latestRevision, latestReleaseURL}, + {"branch source is unknown", strings.Replace(latestRelease, latestRevision, "main", 1), latestVersion, "", latestReleaseURL}, + {"missing source is unknown", tagged(latestVersion, latestReleaseURL), latestVersion, "", latestReleaseURL}, + {"short source is unknown", strings.Replace(latestRelease, latestRevision, "bbbbbbb", 1), latestVersion, "", latestReleaseURL}, + {"old SHA tag", tagged(latestRevision, releaseURLPrefix+latestRevision), "", "", ""}, + {"short version", tagged("v1", releaseURLPrefix+"v1"), "", "", ""}, + {"missing v prefix", tagged("1.10.0", releaseURLPrefix+"1.10.0"), "", "", ""}, + {"prerelease version", tagged("v1.10.0-rc.1", releaseURLPrefix+"v1.10.0-rc.1"), "", "", ""}, + {"build suffix", tagged("v1.10.0+build", releaseURLPrefix+"v1.10.0+build"), "", "", ""}, + {"other host", tagged(latestVersion, "https://evil.example"), "", "", ""}, + {"userinfo", tagged(latestVersion, "https://user@github.com/TNTcraftHIM/Piik/releases/tag/"+latestVersion), "", "", ""}, + {"port", tagged(latestVersion, "https://github.com:8443/TNTcraftHIM/Piik/releases/tag/"+latestVersion), "", "", ""}, + {"plaintext page", tagged(latestVersion, "http://github.com/TNTcraftHIM/Piik/releases/tag/"+latestVersion), "", "", ""}, + {"query", tagged(latestVersion, latestReleaseURL+"?asset=1"), "", "", ""}, + {"fragment", tagged(latestVersion, latestReleaseURL+"#assets"), "", "", ""}, + {"encoded path separator", tagged(latestVersion, "https://github.com/TNTcraftHIM%2FPiik/releases/tag/"+latestVersion), "", "", ""}, + {"mismatched page tag", tagged(latestVersion, releaseURLPrefix+deployedVersion), "", "", ""}, + {"draft", strings.TrimSuffix(latestRelease, "}") + `,"draft":true}`, "", "", ""}, + {"prerelease", strings.TrimSuffix(latestRelease, "}") + `,"prerelease":true}`, "", "", ""}, + {"missing page", `{"tag_name":"` + latestVersion + `"}`, "", "", ""}, + {"array payload", `[]`, "", "", ""}, + {"not an object", `"released"`, "", "", ""}, + {"not json", `nonsense`, "", "", ""}, } for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { - revision, releaseURL, ok := parseReleaseMetadata([]byte(testCase.payload)) - if ok != (testCase.revision != "") || revision != testCase.revision || - releaseURL != testCase.url { - t.Fatalf("parseReleaseMetadata(%s) = %q, %q, %t; want %q, %q", - testCase.payload, revision, releaseURL, ok, testCase.revision, testCase.url) + version, revision, releaseURL, ok := parseReleaseMetadata([]byte(testCase.payload)) + if ok != (testCase.version != "") || version != testCase.version || revision != testCase.revision || releaseURL != testCase.url { + t.Fatalf("parseReleaseMetadata = %q, %q, %q, %t; want %q, %q, %q", + version, revision, releaseURL, ok, testCase.version, testCase.revision, testCase.url) } }) } } func TestCheckReleaseReportsTheDeployedComparison(t *testing.T) { + branchRelease := strings.Replace(latestRelease, latestRevision, "main", 1) testCases := []struct { - name string - current string - status int - body string - wantStatus string - wantCurrent string - wantLatest string - wantReleaseURL string - wantRequestSent bool + name, version, revision, body, wantStatus, wantLatest string }{ - { - name: "newer release", current: deployedRevision, status: http.StatusOK, - body: latestRelease, wantStatus: "update-available", wantCurrent: deployedRevision, - wantLatest: latestRevision, wantReleaseURL: latestReleaseURL, wantRequestSent: true, - }, - { - name: "same release", current: latestRevision, status: http.StatusOK, - body: latestRelease, wantStatus: "up-to-date", wantCurrent: latestRevision, - wantLatest: latestRevision, wantReleaseURL: latestReleaseURL, wantRequestSent: true, - }, - { - name: "endpoint unavailable", current: deployedRevision, - status: http.StatusServiceUnavailable, body: "offline", - wantStatus: "unavailable", wantCurrent: deployedRevision, wantRequestSent: true, - }, - { - name: "unusable metadata", current: deployedRevision, status: http.StatusOK, - body: `{"tag_name":"v1","html_url":"https://evil.example"}`, - // The published page failed validation, so no update is claimed. - wantStatus: "unavailable", wantCurrent: deployedRevision, wantRequestSent: true, - }, - { - name: "deployed revision is not a release", current: "development", - status: http.StatusOK, body: latestRelease, - // No request at all: the check cannot compare an unknown deployment. - wantStatus: "unavailable", wantCurrent: "", wantRequestSent: false, - }, + {"newer numeric version", deployedVersion, deployedRevision, latestRelease, statusUpdateAvailable, latestRevision}, + {"newer version of same source", deployedVersion, latestRevision, latestRelease, statusUpdateAvailable, latestRevision}, + {"older version", "v2.0.0", deployedRevision, latestRelease, statusUpToDate, latestRevision}, + {"same version and source", latestVersion, latestRevision, latestRelease, statusUpToDate, latestRevision}, + {"same version with different source", latestVersion, deployedRevision, latestRelease, statusDifferentBuild, latestRevision}, + {"development has an official choice", "development", deployedRevision, latestRelease, statusOfficialRelease, latestRevision}, + {"development has same source", "development", latestRevision, latestRelease, statusUpToDate, latestRevision}, + {"other unversioned build", "custom-build", deployedRevision, latestRelease, statusOfficialRelease, latestRevision}, + {"local prerelease", "v1.10.0-rc.1", deployedRevision, latestRelease, statusUpdateAvailable, latestRevision}, + {"newer version without known remote source", deployedVersion, deployedRevision, branchRelease, statusUpdateAvailable, ""}, + {"same version without known remote source", latestVersion, deployedRevision, branchRelease, statusUpToDate, ""}, + {"development without known remote source", "development", deployedRevision, branchRelease, statusUnavailable, ""}, + {"newer version without known local source", deployedVersion, "", latestRelease, statusUpdateAvailable, latestRevision}, } for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { - requested := false - server := httptest.NewServer(http.HandlerFunc( - func(writer http.ResponseWriter, request *http.Request) { - requested = true - if got := request.Header.Get("Accept"); got != "application/vnd.github+json" { - t.Errorf("Accept = %q", got) - } - if got := request.Header.Get("X-GitHub-Api-Version"); got != "2022-11-28" { - t.Errorf("X-GitHub-Api-Version = %q", got) - } - if got := request.Header.Get("User-Agent"); got != "Piik-release-check" { - t.Errorf("User-Agent = %q", got) - } - writer.WriteHeader(testCase.status) - _, _ = writer.Write([]byte(testCase.body)) - })) + server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { + if request.Header.Get("Accept") != "application/vnd.github+json" || + request.Header.Get("X-GitHub-Api-Version") != "2022-11-28" || request.Header.Get("User-Agent") != "Piik-release-check" { + t.Error("release request headers changed") + } + _, _ = writer.Write([]byte(testCase.body)) + })) defer server.Close() - - result := checkRelease(t.Context(), testCase.current, server.URL, "") - if requested != testCase.wantRequestSent { - t.Fatalf("endpoint requested = %t; want %t", requested, testCase.wantRequestSent) - } - assertResult(t, result, testCase.wantStatus, testCase.wantCurrent, - testCase.wantLatest, testCase.wantReleaseURL) + result := checkRelease(t.Context(), testCase.version, testCase.revision, server.URL, "", "") + assertResult(t, result, testCase.wantStatus, testCase.version, latestVersion, testCase.revision, testCase.wantLatest, latestReleaseURL) }) } } func TestCheckReleaseFailsClosedWithoutAUsableEndpoint(t *testing.T) { - redirect := httptest.NewServer(http.HandlerFunc( - func(writer http.ResponseWriter, request *http.Request) { - http.Redirect(writer, request, "https://evil.example/releases", http.StatusFound) - })) + redirect := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { + http.Redirect(writer, request, "https://evil.example/releases", http.StatusFound) + })) defer redirect.Close() - reachable := httptest.NewServer(http.HandlerFunc( - func(writer http.ResponseWriter, _ *http.Request) { - _, _ = writer.Write([]byte(latestRelease)) - })) + reachable := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, _ *http.Request) { + _, _ = writer.Write([]byte(latestRelease)) + })) defer reachable.Close() - + unavailable := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, _ *http.Request) { + writer.WriteHeader(http.StatusServiceUnavailable) + })) + defer unavailable.Close() + malformed := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, _ *http.Request) { + _, _ = writer.Write([]byte(`{"tag_name":"v1","html_url":"https://evil.example"}`)) + })) + defer malformed.Close() cancelled, cancel := context.WithCancel(t.Context()) cancel() - testCases := []struct { - name string - ctx context.Context - apiURL string + for _, testCase := range []struct { + name, apiURL string + ctx context.Context }{ - {"redirected endpoint", t.Context(), redirect.URL}, - {"unsupported scheme", t.Context(), "file:///etc/passwd"}, - {"relative endpoint", t.Context(), "releases/latest"}, - {"cancelled check", cancelled, reachable.URL}, - } - for _, testCase := range testCases { + {"redirected endpoint", redirect.URL, t.Context()}, + {"unsupported scheme", "file:///etc/passwd", t.Context()}, + {"relative endpoint", "releases/latest", t.Context()}, + {"cancelled check", reachable.URL, cancelled}, + {"server unavailable", unavailable.URL, t.Context()}, + {"unusable metadata", malformed.URL, t.Context()}, + } { t.Run(testCase.name, func(t *testing.T) { - result := checkRelease(testCase.ctx, deployedRevision, testCase.apiURL, "") - assertResult(t, result, "unavailable", deployedRevision, "", "") + result := checkRelease(testCase.ctx, deployedVersion, deployedRevision, testCase.apiURL, "", "") + assertResult(t, result, statusUnavailable, deployedVersion, "", deployedRevision, "", "") }) } + result := checkRelease(t.Context(), "development", "unknown", "", "", "") + assertResult(t, result, statusUnavailable, "development", "", "", "", "") } func TestOperatorTokenReachesGitHubOnly(t *testing.T) { - endpoints := []struct { - name string - url string - token string - authorized bool + for _, endpoint := range []struct { + name, url, token string + authorized bool }{ {"github api", defaultReleaseAPIURL, "operator-token", true}, {"github api with padded token", defaultReleaseAPIURL, " operator-token ", true}, @@ -208,95 +180,143 @@ func TestOperatorTokenReachesGitHubOnly(t *testing.T) { {"release host", "https://github.com/repos", "operator-token", false}, {"local fixture", "http://127.0.0.1:8787/releases/latest", "operator-token", false}, {"lookalike host", "https://api.github.com.evil.example/x", "operator-token", false}, - } - for _, endpoint := range endpoints { + } { t.Run(endpoint.name, func(t *testing.T) { parsed, err := url.Parse(endpoint.url) if err != nil { t.Fatal(err) } if got := authorizesGitHub(parsed, endpoint.token); got != endpoint.authorized { - t.Fatalf("authorizesGitHub(%q) = %t; want %t", - endpoint.url, got, endpoint.authorized) + t.Fatalf("authorizesGitHub(%q) = %t; want %t", endpoint.url, got, endpoint.authorized) } }) } var received http.Header - fixture := httptest.NewServer(http.HandlerFunc( - func(writer http.ResponseWriter, request *http.Request) { - received = request.Header.Clone() - _, _ = writer.Write([]byte(latestRelease)) - })) + fixture := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { + received = request.Header.Clone() + _, _ = writer.Write([]byte(latestRelease)) + })) defer fixture.Close() - checkRelease(t.Context(), deployedRevision, fixture.URL, "fixture-secret") + checkRelease(t.Context(), deployedVersion, deployedRevision, fixture.URL, "fixture-secret", "") if authorization := received.Get("Authorization"); authorization != "" { t.Fatalf("fixture endpoint received %q", authorization) } } -func TestCheckDeployedReleasePrintsOneLineAndTheOperatorExitCode(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc( - func(writer http.ResponseWriter, _ *http.Request) { - _, _ = writer.Write([]byte(latestRelease)) - })) +func TestCheckDeployedReleaseUsesTheBinaryIdentityAndOperatorExitCode(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, _ *http.Request) { + _, _ = writer.Write([]byte(latestRelease)) + })) defer server.Close() - - testCases := []struct { - name string - revision string - wantCode int - wantJSON string + originalVersion, originalRevision := BuildVersion, BuildRevision + t.Cleanup(func() { BuildVersion, BuildRevision = originalVersion, originalRevision }) + for _, testCase := range []struct { + name, version, revision, status string + code int }{ - { - "update available", deployedRevision + "\n", exitUpdateAvailable, - `{"status":"update-available","currentRevision":"` + deployedRevision + - `","latestRevision":"` + latestRevision + - `","releaseUrl":"` + latestReleaseURL + `"}`, - }, - { - "up to date", latestRevision, exitUpToDate, - `{"status":"up-to-date","currentRevision":"` + latestRevision + - `","latestRevision":"` + latestRevision + - `","releaseUrl":"` + latestReleaseURL + `"}`, - }, - { - "unreadable deployment", "", exitUpdateUnavailable, - `{"status":"unavailable","currentRevision":null,` + - `"latestRevision":null,"releaseUrl":null}`, - }, - } - for _, testCase := range testCases { + {"newer release", deployedVersion, deployedRevision, statusUpdateAvailable, exitUpdateAvailable}, + {"different build", latestVersion, deployedRevision, statusDifferentBuild, exitUpdateAvailable}, + {"official release", "development", deployedRevision, statusOfficialRelease, exitUpdateAvailable}, + {"up to date", latestVersion, latestRevision, statusUpToDate, exitUpToDate}, + {"unknown build", "development", "development", statusUnavailable, exitUpdateUnavailable}, + } { t.Run(testCase.name, func(t *testing.T) { - currentFile := filepath.Join(t.TempDir(), "REVISION") - if testCase.revision != "" { - if err := os.WriteFile(currentFile, []byte(testCase.revision), 0o600); err != nil { - t.Fatal(err) - } - } + BuildVersion, BuildRevision = testCase.version, testCase.revision var out bytes.Buffer - code := checkDeployedRelease(t.Context(), currentFile, server.URL, &out) - if code != testCase.wantCode || out.String() != testCase.wantJSON+"\n" { - t.Fatalf("checkDeployedRelease = %d, %q; want %d, %q", - code, out.String(), testCase.wantCode, testCase.wantJSON+"\n") + code := checkDeployedRelease(t.Context(), server.URL, &out) + if code != testCase.code || strings.Count(out.String(), "\n") != 1 { + t.Fatalf("checkDeployedRelease = %d, %q; want code %d and one line", code, out.String(), testCase.code) + } + var result releaseResult + if err := json.Unmarshal(out.Bytes(), &result); err != nil { + t.Fatal(err) + } + if testCase.status == statusUnavailable { + assertResult(t, result, testCase.status, testCase.version, "", "", "", "") + } else { + assertResult(t, result, testCase.status, testCase.version, latestVersion, testCase.revision, latestRevision, latestReleaseURL) } }) } } -// assertResult compares the whole result through its JSON form so a value that -// must be null is distinguished from one that is merely empty. -func assertResult(t *testing.T, result releaseResult, status, current, latest, releaseURL string) { +func TestMirrorFallbackUsesOriginalSourceAndCompleteStablePagination(t *testing.T) { + payload, err := os.ReadFile("../../tests/fixtures/mirror-releases.json") + if err != nil { + t.Fatal(err) + } + var releases []json.RawMessage + if err := json.Unmarshal(payload, &releases); err != nil { + t.Fatal(err) + } + for i, raw := range releases { + _, revision, _, ok := parseMirrorReleaseMetadata(raw) + if ok != (i < 2) || (ok && revision != latestRevision) { + t.Fatalf("mirror fixture %d: accepted=%t revision=%s", i, ok, revision) + } + } + primaryAvailable, truncate := false, false + primary := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if primaryAvailable { + _, _ = w.Write([]byte(latestRelease)) + } else { + w.WriteHeader(http.StatusServiceUnavailable) + } + })) + defer primary.Close() + mirrorCalls := 0 + mirror := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + mirrorCalls++ + if r.Header.Get("Authorization") != "" || r.Header.Get("X-GitHub-Api-Version") != "" || + r.Header.Get("Accept") != "application/json" { + t.Error("mirror received credentials or GitHub headers") + } + if r.URL.Query().Get("per_page") != "100" || r.URL.Query().Get("direction") != "desc" { + t.Error("mirror pagination missing") + } + if r.URL.Query().Get("page") == "1" || truncate { + page := make([]json.RawMessage, 100) + for i := range page { + page[i] = releases[0] + } + _ = json.NewEncoder(w).Encode(page) + } else { + _, _ = w.Write(payload) + } + })) + defer mirror.Close() + result := checkRelease(t.Context(), deployedVersion, deployedRevision, primary.URL, "operator-secret", mirror.URL) + assertResult(t, result, statusUpdateAvailable, deployedVersion, latestVersion, deployedRevision, latestRevision, mirrorReleasePrefix+latestVersion) + if mirrorCalls != 2 { + t.Fatalf("mirror calls = %d", mirrorCalls) + } + result = checkRelease(t.Context(), "v2.0.0", deployedRevision, primary.URL, "", mirror.URL) + if result.Status != statusUpToDate { + t.Fatal("a lagging mirror offered a downgrade") + } + truncate = true + result = checkRelease(t.Context(), deployedVersion, deployedRevision, primary.URL, "", mirror.URL) + if result.Status != statusUnavailable { + t.Fatal("an incomplete mirror list was accepted") + } + primaryAvailable = true + mirrorCalls = 0 + result = checkRelease(t.Context(), latestVersion, latestRevision, primary.URL, "", mirror.URL) + if result.Status != statusUpToDate || mirrorCalls != 0 { + t.Fatal("a usable primary release consulted the mirror") + } +} + +func assertResult(t *testing.T, result releaseResult, status, currentVersion, latestVersion, currentRevision, latestRevision, releaseURL string) { t.Helper() payload, err := json.Marshal(result) if err != nil { t.Fatal(err) } expected, err := json.Marshal(releaseResult{ - Status: status, - CurrentRevision: reported(current), - LatestRevision: reported(latest), - ReleaseURL: reported(releaseURL), + Status: status, CurrentVersion: reported(currentVersion), LatestVersion: reported(latestVersion), + CurrentRevision: reported(currentRevision), LatestRevision: reported(latestRevision), ReleaseURL: reported(releaseURL), }) if err != nil { t.Fatal(err) @@ -306,24 +326,9 @@ func assertResult(t *testing.T, result releaseResult, status, current, latest, r } } -// reported turns the empty test expectation into the JSON null the result uses. func reported(value string) *string { if value == "" { return nil } return &value } - -// release-update.mjs parseArguments threw the usage string and set -// process.exitCode = 2 when --current-file was absent, so the operator could -// tell a forgotten argument from a check that could not reach GitHub. -func TestCheckDeployedReleaseWithoutACurrentFileIsAUsageError(t *testing.T) { - var out bytes.Buffer - code := checkDeployedRelease(context.Background(), "", defaultReleaseAPIURL, &out) - if code != exitUsage { - t.Fatalf("exit code = %d, want %d", code, exitUsage) - } - if out.Len() != 0 { - t.Fatalf("a usage error wrote %q, want no result line", out.String()) - } -} diff --git a/deploy/check-release.sh b/deploy/check-release.sh index a800fd0..148fc78 100644 --- a/deploy/check-release.sh +++ b/deploy/check-release.sh @@ -1,11 +1,9 @@ #!/bin/bash set -Eeuo pipefail -if [ "$#" -gt 1 ]; then - printf 'usage: %s [current-revision-file]\n' "$0" >&2 +if [ "$#" -ne 0 ]; then + printf 'usage: %s\n' "$0" >&2 exit 2 fi -current_file="${1:-/opt/piik/current/REVISION}" - -exec /opt/piik/current/piik-server --check-release --current-file "$current_file" +exec /opt/piik/current/piik-server --check-release diff --git a/deploy/nginx/share.bonfire.icu.conf.example b/deploy/nginx/piik.conf.example similarity index 88% rename from deploy/nginx/share.bonfire.icu.conf.example rename to deploy/nginx/piik.conf.example index c920c93..a0bfb3f 100644 --- a/deploy/nginx/share.bonfire.icu.conf.example +++ b/deploy/nginx/piik.conf.example @@ -8,7 +8,7 @@ limit_req_zone $binary_remote_addr zone=piik_site_access:10m rate=5r/m; server { listen 80; listen [::]:80; - server_name share.bonfire.icu; + server_name share.example.com; location ^~ /.well-known/acme-challenge/ { root /var/www/letsencrypt; @@ -16,17 +16,17 @@ server { } location / { - return 301 https://share.bonfire.icu$request_uri; + return 301 https://share.example.com$request_uri; } } server { listen 443 ssl http2; listen [::]:443 ssl http2; - server_name share.bonfire.icu; + server_name share.example.com; - ssl_certificate /etc/letsencrypt/live/share.bonfire.icu/fullchain.pem; - ssl_certificate_key /etc/letsencrypt/live/share.bonfire.icu/privkey.pem; + ssl_certificate /etc/letsencrypt/live/share.example.com/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/share.example.com/privkey.pem; include /etc/letsencrypt/options-ssl-nginx.conf; ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; diff --git a/deploy/release-app.sh b/deploy/release-app.sh index 9b4434a..fe83d7d 100644 --- a/deploy/release-app.sh +++ b/deploy/release-app.sh @@ -26,7 +26,8 @@ descriptor_text() { sed -n "s|^ \"$1\": \"\([A-Za-z0-9._/-]*\)\",\{0,1\}\$|\1|p" "$descriptor" } -grep -qx ' "schema": 1,' "$descriptor" +grep -qx ' "schema": 2,' "$descriptor" +version="$(descriptor_text version)" revision="$(descriptor_text revision)" release_id="$(descriptor_text releaseId)" artifact_name="$(descriptor_text artifact)" @@ -37,6 +38,7 @@ file_count="$(sed -n 's|^ "fileCount": \([0-9]\{1,\}\),\{0,1\}$|\1|p' "$descrip main_asset="$(descriptor_text mainAsset)" [[ "$revision" =~ ^[0-9a-f]{40}$ ]] +[[ "$version" = development || "$version" =~ ^v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$ ]] [[ "$release_id" =~ ^[0-9a-f]{7}$ ]] test "$release_id" = "${revision:0:7}" test "$artifact_name" = "piik-${release_id}-runtime.tar.gz" @@ -232,7 +234,8 @@ test -d "$release" test ! -L "$release" test "$(stat -c '%U:%G %a' "$release")" = 'root:root 755' -firewall_before="$(nft list table inet bonfire_filter | sha256sum | awk '{print $1}')" +# Compare rules without traffic counters; no operator-specific table name. +firewall_before="$(nft --stateless list ruleset | sha256sum | awk '{print $1}')" nginx_restarts="$(systemctl show nginx.service -p NRestarts --value)" cutover_since="$(date '+%Y-%m-%d %H:%M:%S')" cutover_start="$(date +%s%3N)" @@ -249,7 +252,7 @@ health_ready="$(date +%s%3N)" test "$(systemctl show piik.service -p ActiveState --value)" = 'active' test "$(systemctl show piik.service -p NRestarts --value)" = '0' test "$(systemctl show nginx.service -p NRestarts --value)" = "$nginx_restarts" -test "$(nft list table inet bonfire_filter | sha256sum | awk '{print $1}')" = "$firewall_before" +test "$(nft --stateless list ruleset | sha256sum | awk '{print $1}')" = "$firewall_before" test "$(readlink -f -- "$current")" = "$release" pid="$(systemctl show piik.service -p MainPID --value)" test "$pid" -gt 1 diff --git a/docs/README.md b/docs/README.md index c012a17..1b7e90c 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,32 +1,36 @@ -# Documentation +# Piik Documentation -[English Quick Start](../README.md) · [中文快速开始](../README.zh-CN.md) +[About Piik](../README.md) · [中文介绍](../README.zh-CN.md) · [中文上手指南](./guide/getting-started.zh-CN.md) -## Use Piik +## Guides And References -| What you need | Start here | +| Task | Guide | | --- | --- | -| Join a friend or share a screen | [Quick Start](../README.md) | -| Run App, choose a mode, or fix capture trouble | [App guide](../cmd/piik-app/README.md) | -| Collect a useful bug report | [Diagnostics and export](./reference/configuration.md#diagnostics) | -| Host your own site | [Self-hosting](./operations/self-hosting.md) | -| Configure ports, access or persistence | [Configuration reference](./reference/configuration.md) | -| Update an existing deployment | [Deployment and recovery](./deployment.md) | -| Check platform and release readiness | [Current status](./status.md) | +| Try the online demo | [Online demo](https://demo.piik.tv) · [Demo steps](./guide/getting-started.md#try-the-online-demo) | +| Watch a friend or share my first screen | [Getting started](./guide/getting-started.md) | +| Choose an App mode or fix sound and connection trouble | [First-use help](./guide/getting-started.md#choose-an-app-mode) | +| Run Piik App, check platform requirements, or build a package | [App guide](../cmd/piik-app/README.md) | +| Try or change the source code | [Run from source](#run-from-source) | +| Find a directory or the module responsible for a behavior | [Repository layout and module map](./reference/engineering.md#repository-layout) | +| Host a site for my group | [Self-hosting](./operations/self-hosting.md) · [中文部署](./operations/self-hosting.zh-CN.md) | +| Keep a server running with systemd or Docker | [Service management](./operations/service-management.md) | +| Set passwords, ports, or room storage | [Configuration](./reference/configuration.md) | +| Update my server | [Server updates](./operations/self-hosting.md#keep-it-running-and-update) | +| Build, publish or recover a maintained release | [Maintainer release tooling](./deployment.md) | +| Send a useful bug report | [Diagnostics and export](./reference/configuration.md#diagnostics) | +| Check release and platform readiness | [Current status](./status.md) | -The public entry points have English and Chinese versions. Technical references -keep one owner rather than duplicate the full documentation tree. - -Contributors should follow the [naming convention](./reference/naming.md) for -display names, commands, packages and App/Server/Browser code boundaries. +The introduction, getting-started and self-hosting guides are available in English and Chinese. +Technical references keep one shared version. ## Run from source -Use Node from [.node-version](../.node-version), npm from -[package.json](../package.json), and Go 1.26 as specified by [go.mod](../go.mod). -Node builds and serves the development UI; the application server runs in Go. +You can try the Browser application locally with Node from +[.node-version](../.node-version), npm from +[package.json](../package.json), and Go 1.26 from [go.mod](../go.mod). +Run these commands from the repository root. -Start the UI: +Start the web UI: ```sh npm ci @@ -49,49 +53,58 @@ go build -o build/dev/piik-server.exe ./cmd/piik-server ``` The fixed Windows executable path avoids repeated firewall prompts from `go run`. +Open [localhost:8787](http://localhost:8787), start sharing, and open the invitation +in another browser tab to try watching. This localhost invitation stays on your +computer; remote invitations need a reachable origin as described in +[configuration](./reference/configuration.md). +Browser capture requires `localhost` or HTTPS. -Open `http://localhost:8787`. The development UI also listens on the LAN; -screen capture requires `localhost` or HTTPS. For another invitation origin, -follow the [configuration reference](./reference/configuration.md). +For the self-contained App, follow [App development](../cmd/piik-app/README.md#development) +and [packaging](../cmd/piik-app/README.md#packaging), including the platform's +native capture requirements. -The standard checks are: +## Work on Piik + +[Contributing](../CONTRIBUTING.md) covers the workflow and choosing focused checks. +The main verification commands are: ```sh npm run check npm run check:client ``` -The [contributing guide](../CONTRIBUTING.md#verification-entrypoints) explains -native build prerequisites, focused checks and separate physical gates. +Use the [naming and copy guide](./reference/naming.md) for product names, +commands, role labels and Chinese/English voice. +Piik-owned code uses [MIT](../LICENSE); see the +[licensing guide](../licenses/README.md) for third-party components. -## Product and design +For the public website, see the [introduction design](./design/public-introduction.md) +and [website preview and publishing](./operations/website.md). -Current behavior belongs to four product modules: +## Understand the product -- [Rooms and access](./product/rooms-access.md) -- [Routing and transport](./product/routing-transport.md) -- [Capture, audio and media quality](./product/media-quality.md) -- [Presentation and lifecycle](./product/presentation-lifecycle.md) +| Topic | Its reference | +| --- | --- | +| Invitations, room codes and passwords | [Rooms and access](./product/rooms-access.md) | +| How viewers connect | [Routing and transport](./product/routing-transport.md) | +| Screen capture, quality controls and sound | [Media quality](./product/media-quality.md) | +| Host and viewer behavior | [Presentation and lifecycle](./product/presentation-lifecycle.md) | +| Why a design was chosen | [Architecture decisions](./adr/) | +| Measurements and platform limits | [Research](./research/) and [verification status](./verification-status.md) | -Key decisions cover [automatic routing](./adr/0005-automatic-hybrid-media-routing.md), -the [shared Go core](./adr/0012-shared-go-backend-core.md), -[embedded media and Native output groups](./adr/0013-embedded-node-local-media.md), -and the [Browser encoding-pool candidate](./adr/0014-browser-node-local-encoding-pool.md). -Browse [all ADRs](./adr/) for their context and consequences. +## Shared conventions -## Evidence and current work +| When changing… | Follow this owner | +| --- | --- | +| Module responsibilities or interfaces | [Engineering and contract map](./reference/engineering.md) | +| Product/tool names, role labels or copy | [Naming and copy](./reference/naming.md) | +| UI colour, illustration, motion or layout | [Visual language](./design/visual-language.md) | +| Overlays, titles or state indicators | [Media status](./design/media-status.md) | +| Releases, protocols or stored formats | [Versions and compatibility](./reference/versioning.md) | +| Main protection or automatic publishing | [GitHub operations](./operations/github.md) | +| Documentation or a duplicated rule | [Documentation ownership](./maintenance.md) | +| Checks, PRs, integration or cleanup | [Contributing](../CONTRIBUTING.md) | -- [Project memory](./project-memory.md): compact product map and invariants. -- [Status](./status.md): current source, production and acceptance boundaries. -- [TODO](./todo.md): the only work ledger. -- [Verification status](./verification-status.md): remaining physical evidence. -- [Native encoding research](./research/webrtc-encoder-pool.md) and - [Browser pooling research](./research/browser-local-encoding-pool.md): measured - results, failed controls and limits; neither promises universal performance. -- [Research directory](./research/): transport, capture, platform and backend evidence. -- [Maintenance](./maintenance.md): document ownership and repository lifecycle. -- [Licensing](../licenses/README.md): Piik's MIT scope and third-party notices. - -Research distinguishes observations from assumptions. Product modules own -accepted behavior, source and tests own implementation detail, and Git/PRs own -completed history. +For ongoing work, use [TODO](./todo.md). [Project memory](./project-memory.md) +provides the compact product map; [status](./status.md) indexes current source +and deployment boundaries. Completed work lives in Git history and pull requests. diff --git a/docs/adr/0001-p2p-first-media-topology.md b/docs/adr/0001-p2p-first-media-topology.md index 0b8e702..a0b6e38 100644 --- a/docs/adr/0001-p2p-first-media-topology.md +++ b/docs/adr/0001-p2p-first-media-topology.md @@ -25,7 +25,7 @@ browser-first product surface but did not establish a sustainable fanout, fallback order, or server-resource policy. The MVP explicitly excluded always-SFU conferencing, public-broadcast scaling, -MCU transcoding, and a custom Parsec-like transport. Those workloads either +MCU transcoding, and a custom low-latency media transport. Those workloads either contradict the private-room cost model or duplicate mature WebRTC capabilities. ## Current Ownership diff --git a/docs/adr/0009-optional-nat-prediction.md b/docs/adr/0009-optional-nat-prediction.md index 5f6ff42..98d8c76 100644 --- a/docs/adr/0009-optional-nat-prediction.md +++ b/docs/adr/0009-optional-nat-prediction.md @@ -107,6 +107,8 @@ RTP packets to an independent Linux Pion Viewer over a selected direct host-to-srflx pair. This proves the shared-socket candidate carries DTLS-SRTP; the tested TUN network exposed only one distinct public mapping, so it does not prove a predicted candidate win. +Survey inputs, platform limits and the measured boundaries live in +[NAT traversal research](../research/nat-traversal.md). The current Native path maps selected Pion candidate foundations to the same anonymous `ordinary | predicted | unknown` evidence as Browser ICE. diff --git a/docs/adr/0012-shared-go-backend-core.md b/docs/adr/0012-shared-go-backend-core.md index 0bb496e..a60ee8d 100644 --- a/docs/adr/0012-shared-go-backend-core.md +++ b/docs/adr/0012-shared-go-backend-core.md @@ -56,18 +56,19 @@ the same Browser build. stay synchronous inside the lock and keep the current fail-fast storage policy: an unrecoverable authority write ends the process instead of continuing on divergent state. -6. LiveKit access uses the standard library: HS256 participant and service tokens - plus Twirp JSON RoomService calls over `net/http`, with the current grants, - bounded per-request timeout, per-managed-room serialization, and not-found - rule. No LiveKit SDK is added. +6. Superseded by [ADR-0013](./0013-embedded-node-local-media.md): the port initially + used standard-library tokens and Twirp calls for external LiveKit. Current + App/Server media adapters share embedded Pion/LiveKit forwarding and the + authenticated room signaling path; that external service/token layer is gone. 7. Room persistence uses the pure-Go `modernc.org/sqlite` driver over the schema, pragmas, single exclusive connection, and recovery accepted in [ADR-0002](./0002-memory-resident-protected-rooms.md). Both storage modes, their validation, and their failure policy are unchanged, and no build requires a C toolchain. -8. The port changed no contract. Signaling stayed on its then-current v21 wire; the App - loopback stays v8, capture framing stays v4, and the HTTP API, cookies, error - codes, and close codes keep their exact shapes. No compatibility alias, dual +8. The port changed no contract. At that boundary signaling was v21, App loopback + v8 and the capture probe v4; the HTTP API, cookies, error codes and close codes + retained their shapes. [Status](../status.md#accepted-release-contract) owns + the current contract tuple. No compatibility alias, dual reader, or migration is added for a change of implementation language. ## Consequences @@ -91,7 +92,7 @@ the same Browser build. production host need no Node; [deployment](../deployment.md) owns their exact commands. - Development runs two processes: the Vite dev server for the Browser bundle and - the Go application server behind it. The [README](../../README.md) owns those + the Go application server behind it. The [development guide](../README.md#run-from-source) owns those commands. - ADR-0010 items 1, 5, 7, 12 (its supervisor sentence), 13, 14, and its two-internal-process consequence are superseded and amended in place. The rest diff --git a/docs/adr/0013-embedded-node-local-media.md b/docs/adr/0013-embedded-node-local-media.md index a7845cb..7e3012e 100644 --- a/docs/adr/0013-embedded-node-local-media.md +++ b/docs/adr/0013-embedded-node-local-media.md @@ -120,3 +120,4 @@ is introduced. - [RTP switching and source projection, RFC 7667](https://www.rfc-editor.org/rfc/rfc7667) - [LiveKit demand envelope](https://github.com/livekit/livekit/blob/v1.13.6/pkg/rtc/dynacast/dynacastmanagervideo.go) - [Implementation proposal and reuse map](../research/node-local-media-design.md) +- [Embedded media evaluation](../research/embedded-media.md) diff --git a/docs/assets/quickstart.svg b/docs/assets/quickstart.svg new file mode 100644 index 0000000..d94aaac --- /dev/null +++ b/docs/assets/quickstart.svg @@ -0,0 +1,63 @@ + + Pick a screen, send an invitation, watch together + Three illustrated steps: choose a window, send its room invitation, and join friends on the sofa. Each loop pauses to let the scene settle. + + + + 01 + + + + + + + + + + 02 + + + + + + + + + 03 + + + + + + + + + + diff --git a/docs/assets/room-en-dark.png b/docs/assets/room-en-dark.png new file mode 100644 index 0000000..39b4171 Binary files /dev/null and b/docs/assets/room-en-dark.png differ diff --git a/docs/assets/room-en-light.png b/docs/assets/room-en-light.png new file mode 100644 index 0000000..84d778e Binary files /dev/null and b/docs/assets/room-en-light.png differ diff --git a/docs/assets/room-zh-dark.png b/docs/assets/room-zh-dark.png new file mode 100644 index 0000000..3b79ae4 Binary files /dev/null and b/docs/assets/room-zh-dark.png differ diff --git a/docs/assets/room-zh-light.png b/docs/assets/room-zh-light.png new file mode 100644 index 0000000..bedfc10 Binary files /dev/null and b/docs/assets/room-zh-light.png differ diff --git a/docs/deployment.md b/docs/deployment.md index cf15788..2c9270c 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -12,6 +12,9 @@ media, bare-metal nginx, nftables, and application port 8787. It requires an existing running Go release. Another proxy, firewall owner, application port, first installation, or initial embedded-media cutover needs its own scoped bootstrap/updater; do not call this wrapper generic. +Its firewall check compares the nftables ruleset without traffic counters; it +does not depend on a particular table name. Coordinate other firewall writers +outside the cutover window. See [nft output options](https://netfilter.org/projects/nftables/manpage.html). ## Release Boundary @@ -95,11 +98,12 @@ Retain the descriptor and successful deployment output as release metadata. Do not create a follow-up source commit solely to duplicate their revision, asset, or hashes. -After `validate` succeeds on an explicit workflow dispatch with -`client_checks=true`, CI packages the Server application release and the -three-platform App candidates that consume it. The artifacts are retained -for 14 days. Ordinary `main` pushes do not run these packaging jobs. This -workflow does not create a tag, public GitHub Release, or deployment. +Manual `client_checks=true` dispatch packages Server and three-platform App +candidates after validation. Once [automatic publication](./operations/github.md) +is explicitly enabled, accepted main merges run that same pipeline and publish +the verified artifacts. Candidate artifacts are retained for 14 days. Branches +and PRs do not start cloud CI; no release-record commit is written back to main. +The workflow does not deploy into a running application service. Do not build or run the full repository check on a constrained production host. That host runs only the packaged binary and needs no Node, npm, or dependency @@ -107,15 +111,19 @@ install. ## Update Check -The application, Server deployment, and platform App all use the full Git -revision as their release identity. A formal GitHub Release must use that exact -40-character revision as its tag and keep the corresponding release URL. The -current CI workflow produces short-lived candidates but does not publish a -GitHub Release; publication remains an explicit distribution decision. +Builds carry a product version plus full source revision. Packagers use one +release plan or exact Git tag and inject its identity into Server, App and Web; +untagged local candidates use `development`. Schema-2 package descriptors carry +the same pair and artifact hashes. The [version policy](./reference/versioning.md) +owns ordering and first-public-release readiness. -The default App launcher starts immediately, then performs one background -request to the official Piik GitHub Releases API. It shows a link only when -the latest release has a valid full revision different from the packaged one. +The default App launcher starts immediately, then checks GitHub Releases in the +background, falling back to the Gitee mirror if GitHub is unavailable. The +[release-source policy](./reference/versioning.md#release-sources) owns selection +and provenance checks. It shows a link only when +the latest stable release is newer, the same version has a known different source +SHA, or a development build can choose the official release. These notices are +distinct; a different SHA alone is not called newer. The request sends no current revision, credentials, room data, or media data; network errors, private-repository responses, and missing releases are treated as no notice. It never downloads, replaces, or interrupts a running share. @@ -126,62 +134,26 @@ An operator can perform the corresponding read-only Server check: bash deploy/check-release.sh ``` -The script runs the deployed binary's release check, which reads -`/opt/piik/current/REVISION` and prints one JSON result. -Exit status `0` means the deployed revision is current, `10` means a newer -release is available, and `20` means the check could not establish a valid -release identity. For a private repository, inject a short-lived `GITHUB_TOKEN` +The Server also checks once in the background after startup and logs an available +release with its download URL. It does not delay startup, periodically poll, +notify room participants or install anything. + +The script runs the deployed binary's `--check-release`, using that binary's +injected identity, and prints one JSON result with versions, available source +SHAs and the release URL. Exit `0` means no newer/different official build, +`10` means `update-available`, `different-build` or `official-release`, and `20` +means the check is unavailable. It does not accept a separate revision file. +For a private repository, inject a short-lived `GITHUB_TOKEN` through the operator environment; never place it in the repository or command line. The command does not mutate files, services, containers, or persistent -state. A different current-revision file may be supplied as its only argument. - -## Permanent-Room Schema Cutover - -The schema 2 / signaling v23 cutover required matching Web/App/Server builds -and an accepted active-session interruption. Native control stayed v9; that -schema change preserved Browser credential keys. It was not an app-only release. -The separate [brand cutover](./research/piik-rename-plan.md) must preserve this -database rather than repeat the schema change. - -1. Verify the candidate; record the current release, environment, absolute DB - path and ownership. Stop ingress and the old application; take and retain a - SQLite backup. Require `application_id=1396920910`, `user_version=1`, expected - columns and `PRAGMA quick_check='ok'` before touching a separate protected copy. -2. On that copy, use SQLite with `DROP COLUMN` support and stop on SQL errors: - - ```sql - BEGIN EXCLUSIVE; - ALTER TABLE rooms DROP COLUMN lease_expires_at_ms; - PRAGMA user_version = 2; - COMMIT; - ``` - -3. Verify integrity, exact columns/version and equal row counts. Compare retained - columns in both directions against the backup using `EXCEPT`; both must be - empty. Record only counts/pass/fail, not private verifiers. The candidate must - open this copy successfully; no room is dropped because of its former deadline. -4. Retain the absolute `ROOM_DATABASE_PATH`; remove `ROOM_LEASE_SECONDS`. Install - the verified copy and release while stopped, retaining permissions/ownership. - Start; check health and Host/Viewer reauthentication before reopening ingress. -5. On failure, stop and restore prior database, environment and release together. - Once new authority mutations are accepted, restoring the backup could revive - revoked grants: preserve those changes or make an explicit recovery decision. - -The runtime reads only schema 2. SQLite's [column removal](https://www.sqlite.org/lang_altertable.html#altertabdropcol) -and [backup guidance](https://www.sqlite.org/backup.html) define this offline operation. +state. ## Atomic Cutover -### First embedded-media cutover prerequisite - -The first move from external services to embedded STUN/SFU is a coordinated -infrastructure and protocol transaction, including Node-to-Go where still -needed. Complete the [self-hosting cutover procedure](./operations/self-hosting.md#coordinated-embedded-media-cutover) -with matching Web/Server signaling and native protocol v9 App builds, -accepted active-session interruption, released UDP ports, and exact -unit/environment/proxy/service recovery. The routine wrapper does not perform -that transaction; it requires an already running packaged Go release and cannot -restore the previous infrastructure. +The wrapper requires an already running packaged Go release and the service +layout described above. For a new installation, use the +[self-hosting guide](./operations/self-hosting.md). Infrastructure changes need +their own scoped recovery procedure under [CONTRIBUTING](../CONTRIBUTING.md). Run the tracked server entry with the uploaded descriptor: @@ -249,3 +221,8 @@ operator intervention; it must not silently report the new release as active. Infrastructure or persistent-state recovery is separate and limited to the surfaces changed by that task. Do not restore an entire server, old firewall, or old secret set for an application-only failure. + +If a task changes persistent room data, prepare and verify a separate +[SQLite backup](https://www.sqlite.org/backup.html) and recover matching data, +configuration and application together. Restoring old data can revive revoked +credentials; account for later authority changes before reopening ingress. diff --git a/docs/design/media-status.md b/docs/design/media-status.md new file mode 100644 index 0000000..edaf545 --- /dev/null +++ b/docs/design/media-status.md @@ -0,0 +1,113 @@ +# Media Status + +`src/client/ui/media-status.ts` owns the shared visual projection, demonstrated +by `/__status-preview`. It is not a routing or quality controller. The existing +presentation reducer continues to own playback facts; roster and quality +evidence retain their own authority. Overlay, indicators, notices and title are +consumers, not truth sources. + +Frontend controls, names and details share `Tooltip`, following the +[visual language](./visual-language.md). Its native Popover top layer escapes scrolling +containers, with viewport-relative placement. Hover, keyboard focus and touch +hold share one visibility owner; Escape dismisses even a mouse-only hint. +`/__tooltip-preview` shows the catalogue. Playback actions and their local audio +ownership are defined in [presentation lifecycle](../product/presentation-lifecycle.md#playback-ownership); +`/__playback-preview` exercises the actual control bar with generated local media. + +## Status Meaning + +Keep three questions separate: is the media path available, is this picture +playing, and is this particular stream limited? An available path does not +prove excellent quality. One weak child does not make its parent's received +picture or a healthy sibling unhealthy. + +The shared [visual language](./visual-language.md) owns colours, cast, shapes, +panel accents and motion. This document owns the evidence and scope that select +those meanings. Current `StatusDescriptor` tone and progress take precedence +over a reused comic's default; text and pure-visual tooltips retain that context. + +## Projection Rules + +- Overlay: appears when there is no usable picture, an intentional Host pause, + or a required user action. Ordinary quality warnings never cover playback. + Initial connection guidance is not an error even though it occupies the stage. +- Television: one media-status icon sits inside the lower television frame + (`lr-tv-chin`). Visual mode explains it with the existing comic tooltip; + text modes expose the specific localized status in the same paper tooltip. + Both reuse `Tooltip` for hover, keyboard focus, touch hold and viewport + placement; no native `title` competes with the custom status hint. There is no + permanent text bubble, additional round lamp, or repeated control-menu caption. + The Host television describes its local source, not an arbitrary outbound + child. A confirmed limitation may colour the relevant icon amber while the + page title still says watching. +- Host couch: each Viewer has one round lamp. Grey means waiting for the share, + blue means waiting for the media path, green means committed `mediaReady`, and + amber means a recent receive freeze. The Host's own pawn has no duplicate + lamp. `deriveParticipantStatus` supplies both the couch and Host overview; + neither stores another copy of the status. +- Viewer couch: pawns have no round lamps. While awaiting media readiness a + pawn's body breathes gently; it becomes still when ready. This expresses the + same roster fact without claiming to know other Viewers' quality. +- Header: the page's connection to the room server. Its control-link comics + show a Browser and server, not a failed video path. A media-route failure can + coexist with a healthy server connection, and vice versa. +- Connection notice: signaling recovery may coexist with healthy media. It + cannot convert proved playback into waiting. +- Notices: necessary coexisting issues and operation feedback use the existing + pills near the stage; they are not another primary media status. Raw + PeerConnection errors remain diagnostic observations, not persistent red + warnings after media recovers. The control menu does not repeat the television + status or aggregate the first weak outbound edge into a global quality warning. +- Title: keeps the primary activity, with a compact warning for an applicable + problem. Losing focus alone never means waiting for media. +- Recovery: every projection is recomputed from current facts. A warning requires + current evidence; an expired or mismatched sample becomes unknown, not healthy + or disconnected. A ready participant can therefore return to a green readiness + lamp without claiming that its quality has recovered. No warning latch or + additional timer is needed. + +## Evidence And Integration Boundary + +Use WebRTC `connectionState` for transport explanation, not first-frame proof. +`disconnected` may recover; `failed` is terminal for that connection. Closing a +retired connection is not an error in the replacement connection. +`qualityLimitationReason` describes an **outbound** encoder. A relay's sender +warning must never become a warning about its own inbound picture. Low FPS, +resolution or bitrate alone does not prove poor quality; a static screen or a +small source can legitimately need little data. Do not introduce UI thresholds +or a new quality score. + +Current roster messages expose committed `mediaReady` to both roles, not a +complete remote quality/failure state. Missing readiness means waiting or +recovery, not confirmed disconnection; departed participants leave the roster. +Viewer receive evidence is shared with its parent and the Host, not with every +other Viewer. No additional remote-quality broadcast is needed. + +The Host's amber participant lamp requires an active share, committed readiness, +fresh receive evidence for the current upstream, and a positive +`freezeCountDelta` or `freezeDurationMsDelta`. The existing evidence-expiry path +clears stale warnings. Hidden, paused and other diagnostic-only windows provide +no freeze proof; low FPS, resolution or bitrate is not a substitute. + +All `*Delta` metrics describe one observation window and never inherit an older +non-null value. Non-window fields retain the existing details-display cache. +Participant status reads the current evidence directly; it adds no stored +status, timer, threshold or score. Detailed sender/receiver metrics +remain available in connection details and Debug with their original scope. + +The preview labels supplied example quality facts for the displayed stream. +Examples do not authorize guessing unavailable production facts. + +## References + +- [W3C WebRTC connection states](https://www.w3.org/TR/webrtc/#dom-rtcpeerconnectionstate): + transport lifecycle is distinct from receiving and presenting media. +- [W3C WebRTC statistics](https://www.w3.org/TR/webrtc-stats/#dom-rtcoutboundrtpstreamstats-qualitylimitationreason): + native `none`, `bandwidth`, `cpu`, `other` describe outbound limitations. +- [LiveKit connection quality](https://docs.livekit.io/intro/basics/rooms-participants-tracks/webhooks-events/#connection-quality): + quality and connection events are separate; unknown is an explicit value. +- [LiveKit reconnection](https://docs.livekit.io/intro/basics/connect/#network-changes-and-reconnection): + transient signaling/media recovery does not always interrupt the picture. + +We reuse these distinctions and the existing Piik owners, not a new SDK, scoring +algorithm or transport policy. diff --git a/docs/design/piik-brand.html b/docs/design/piik-brand.html deleted file mode 100644 index d7f10e2..0000000 --- a/docs/design/piik-brand.html +++ /dev/null @@ -1,239 +0,0 @@ - - - - - -Piik · Brand study / 品牌预览 - -
-
-
Piik / 品牌预览
- -
-
-
-
01 / A name taking shape
-

A little peek.
A familiar face.

-

From “Peek” in games to a little TV for watching together. Keep the wink. Give it a name.

-

探个头,看看朋友在玩什么。名字换了,小电视还是那个小电视。

-
-
-
- Piik → TV - - Piik becomes the winking TV - The P bowl opens into the screen, with the outer letter stems joining its sides. The two i dots become eyes, their stems become feet, and the k arms open into the TV antennae. - -
-

Same strokes. Familiar wink.P 撑开屏幕,ii 变成眼睛和脚,k 的两笔转成电视天线。

-
- - - - -
- -
-
- -

Three ways to write it三个名字写法,同一套笔画

One drawing style. Only the casing changes.

-
- -
-
02More contrast
- PiiK -

PiiK

A stronger K gives the name a sharper silhouette.两侧更高、更硬朗,也更抢眼。

-
-
-
03Softer
- piik -

piik

Relaxed and compact, with a lower centre of gravity.更轻松。小写 p 会伸到基线下方。

-
-
-
-
-
02 / The two little details
-

Two dots. Two curious eyes.

-

Round dots keep the ii easy to read, then become the familiar TV eyes.

-

The wordmark below is shown at everyday sizes.

-

两个圆点保留 ii 的辨识度,再变成小电视的眼睛。下面按实际尺寸查看。

-
Keep the existing TV for the tiny icon.
No four-letter favicon puzzle.
-
- - - - - - - -
HeightPiik
64 px
32 px
24 px
-
-

At home on a page放到页面上:官网首屏的方向草图

A static website. The sharing app stays a tool.

-
-
- -
DownloadGuideGitHub
-
-
-

Your game.
Their front row.

Share your screen. Bring your friends.
They watch right in their browsers.

开一局,朋友坐前排。
发个邀请,打开浏览器就能看。

- -
-
-
01 / Pick

A screen, a window, a game.选好想分享的画面。

-
02 / Invite

Send a link to your friends.把邀请链接发给朋友。

-
03 / Hang out

A little room for your people.人到齐,就一起看。

-
-
-
Display / 显示名称 PiikTechnical / 技术名称 piik-appWebsite / 官网 piik.tvPage title / 页签 Piik | 6020 · 📺
-
Design study · 09 Sep 2026 · 待讨论,尚未替换产品Layout references: LocalSend · RustDesk
-
- - diff --git a/docs/design/public-introduction.md b/docs/design/public-introduction.md new file mode 100644 index 0000000..a5cb3d8 --- /dev/null +++ b/docs/design/public-introduction.md @@ -0,0 +1,148 @@ +# Public introduction + +This file owns the website and README content structure. The +[visual language](./visual-language.md) owns their shared illustration and motion. + +## One story, three readers + +Lead with friends sharing something worth seeing: a game, a drawing, a film or +travel photos. The [copy guide](../reference/naming.md#voice-and-terminology) owns +Chinese/English voice and role labels. A visitor should +understand what Piik does, choose an entry and complete the first useful action. + +| Surface | Reader's question | Content | +| --- | --- | --- | +| README, English and Chinese | What is this, and how do I start? | Brand introduction and interface preview, features, getting started, self-hosting, contribution and license | +| `site/`, for GitHub Pages | What does using it feel like, and how do I start? | Room illustration and primary download link, illustrated steps, demo/App/existing-site walkthroughs, FAQ, platform downloads, documentation and source | +| Getting-started guides | Which button or mode do I use? | Browser invitation, sharing, App setup and three mode choices | +| Self-hosting guides | How do I put my own site online? | Run the binary, set the domain and HTTPS proxy, open ports and verify; service management remains a separate reference | +| Developer and operations docs | How do I build or host it? | Existing configuration, deployment, architecture and diagnostic owners | + +The [documentation map](../README.md) is the Wiki-style entry. Link it from the +website navigation and footer, with direct self-hosting, configuration and +update/recovery links in the documentation section. Keep the guides in this +repository; do not maintain a second GitHub Wiki copy of the same instructions. + +## Presentation + +Keep three presentation responsibilities distinct: + +| Layer | Owns | Review question | +| --- | --- | --- | +| Shared identity | Mascot geometry and wink, gold sparkle, wordmark/orange dot, public-page brand control | Does this read as the same Piik at every size and entry? | +| Product demonstration | Actual components, labels, source selection, room controls and participants with sample data | Does the pictured action still match the current product? | +| Promotional composition | Original scenery, large typography, framing, transitions and optional music | Does the spectacle help a newcomer understand the product and find the next action? | + +Use one public-page brand component for the homepage and film. The mascot's +curved wink and gold sparkle are recognizable features; a prop or a pictured +game does not replace its face with another expression. Original RPG scenery +can suggest exploration through terrain, ruins and small discoveries without +borrowing another product's characters, named places, logos or artwork. + +Homepage sections follow the visitor's decisions: understand Piik and find the +primary download action, see the sharing steps, choose a short tutorial, resolve +questions, then find platform downloads, detailed documentation and source. +The primary download link lets returning visitors skip the tutorials. Guide +choices run from online demo to App to an existing site, with only the demo +expanded initially. Watching requires no installation. Explain where an existing +site comes from; mark self-hosting as advanced and link its complete guide. +Platform download cards share one App tutorial entry. Keep section navigation +available on phones rather than hiding the only route to documentation. +The film can use bolder composition, but must show real operations and concrete +benefits before its closing action. The [copy guide](../reference/naming.md#voice-and-terminology) +owns technical disclosure; a dramatic heading does not exempt copy from it. + +Review whole Chinese phrases, English meaning and adjacent number labels in +finished frames, not just the source string. Recheck the smallest viewport and +both languages when changing a title or its timing. Keep enough still time to +read it; do not solve overflow by shrinking an important caption beyond legibility. + +Use the [shared visual language](./visual-language.md) for cast, objects, +accessible interaction and motion. This document owns the website's editorial +layout and introduction, not a separate product-status vocabulary. + +Each section needs one reader question, evidence that answers it and a clear +next action. A scene should demonstrate one action or benefit before changing +the topic. Keep decorative words and transitions away from control labels and +the primary action; sound and motion must not carry essential instructions alone. +Gameplay reactions follow a visible result: establish the action, show its +success, then celebrate. Keep enough of the play visible to make the reaction +understandable; a genre label or a score burst does not replace the action. + +Use mint walls, clear control edges, a warm orange/yellow sofa and the curious TV +mascot. The site is a welcoming room, not a monitoring dashboard. Keep one +primary action, native navigation and short instructions. English is the first +view with an explicit Chinese choice; theme follows the system unless changed. +Respect reduced motion and retain useful content without JavaScript. + +The room illustration cycles through four distinct uses: an RPG, drawing, +travel photos and a movie. Treat coding and design as creative work rather than +proliferating overlapping examples. The host holds a gamepad during the game; +the role badge identifies the host independently of the activity. +Keep participant drawings consistent across the site, READMEs, +interface captures and tooltip/status comics. + +Present Piik as an open-source project: plain explanations, room for tinkering, +and an invitation to contribute. Keep the friendly illustration and avoid +company-style pitches, repeated slogans or claims about a support team. +The short comics illustrate choosing, inviting and joining, following the +shared motion grammar and whole-control replay rule. The optional film starts +only after an explicit action and has native buttons and a seek slider. Open it +within the homepage, loading its single player on demand; closing it unloads +playback. The standalone page serves recording and direct links. Fullscreen +watching keeps touch controls available; hiding them is an explicit recording action. +Both READMEs share the friendly opening and room illustration. Their feature, +limitation and setup sections use the factual register in the copy guide. + +Use a consistent README structure: +a distinct brand opening, bold headings and key phrases, regular explanatory +text, quiet captions and optional detail. Use images and short demonstrations +to explain different things rather than filling space. Keep essential setup and +availability in readable text, and distinguish generated demo content from a +real captured workload. GitHub-native Markdown/HTML must remain useful on mobile +and in both themes without custom CSS. + +The product UI remains mascot-only. On the website, keep the header's TV +mascot before the Piik name. The wordmark and mascot do not transform +into one another. Match the product's +playful style through the mascot's wink and short comic interactions. An +original room illustration is shared by the site and READMEs. Interface previews +use generated scenes and sample participants. Keep private invitations, +user identities and personal media out of published assets; no external fonts +or tracking are needed. + +## Small implementation + +The website and film initially follow the browser's primary language: Chinese +uses Chinese copy, and other languages use English. An explicit language choice +is shared between these two pages and remembered; linked language choices take +precedence when following a link. + +Use static HTML, CSS, SVG and small scripts for the public pages; no router, +CMS, translation dependency or new application backend. The film's isolated +demonstration frame bundles the existing React product components and styles, +using staged inputs. It cannot access storage, run capture or contact the room +service. The film clock owns its actions and decorative CSS animation positions. +Rebuilding the site picks up component/style/copy changes; changes to the actual +workflow still require reviewing the sequence and camera framing. Do not replace +shared controls with a second hand-drawn UI or retain obsolete screenshot plates. +The existing App/Web +continues to own sharing. The homepage explains it rather than simulating a +second room authority. Public copies of advanced product/research documents +are not automatically included in the website artifact. + +The [website operations guide](../operations/website.md) owns Pages publication, +public destinations and the separate P2P-only demo. The primary action leads +to the platform download section. Use permanent release-page links for downloads; +archive names include a revision. Verify destinations as part of publication. +Demo, App and existing-site walkthroughs +use real action names and explain the first sharing and viewing steps. Keep +platform requirements and connection limitations beside the relevant steps. + +## Review boundary + +Review desktop/mobile, English/Chinese, light/dark, keyboard navigation, +reduced motion, no JavaScript and a nested project URL. Check links, accurate +platform/connection claims and a single copy of each responsibility. Remove +decorative controls or code that do not improve understanding. This is a public +introduction overhaul, not an excuse to change configuration or media policy. diff --git a/docs/design/visual-language.md b/docs/design/visual-language.md new file mode 100644 index 0000000..b1bcf15 --- /dev/null +++ b/docs/design/visual-language.md @@ -0,0 +1,210 @@ +# Visual Language + +This is the single design requirement for Piik's shared Web/App interface: +illustration, semantic colour, panel grammar, motion and tooltip presentation. +New UI work follows this file and the local `/__tooltip-preview` catalogue. +[Media status](./media-status.md) owns which facts justify each status; +[presentation lifecycle](../product/presentation-lifecycle.md) owns interaction +and playback. Neither an illustration nor its colour creates product state. + +Use one stroke language for single concepts, small panel comics for causes and +sequences, and literal scene objects for people, rooms and media. Glyphs do not +form sentences. The same meaning and visual hierarchy must work in Chinese, +English and pure-visual modes, in both themes and at narrow widths. + +Piik's living room uses simple silhouettes, generous space, rounded controls +and a small responsive bounce. Clear hierarchy and legible interaction keep +the playful details easy to use. +The [copy guide](../reference/naming.md#voice-and-terminology) owns its voice. +The [public introduction guide](./public-introduction.md) owns website and film +hierarchy: shared identity, current product demonstrations and promotional +composition. It applies the copy guide's audience and technical-detail layers. + +Keep playfulness restrained: clean silhouettes, balanced proportions, quiet +expressions and brief gestures. Keep the interface approachable; avoid +pushing ordinary characters toward infant proportions, exaggerated +grins, dense cute decoration or children's picture-book styling. + +## Layout And Input + +Present a small shared living room: the television is the media stage, the +couch and pawns make presence spatial, and the four-digit room code stays +prominent. Decoration must clarify product facts without creating another state +model. Universal digits, transport symbols, URLs and measured values remain +literal when the user needs the data. + +Functional controls use native buttons/inputs and localized accessible names. +Hover, keyboard focus and touch receive equivalent guidance. Keep information +order and interaction ownership consistent across languages, themes and viewport +sizes. Reflow or bounded scrolling must not hide a primary action, truncate an +essential value, overlap controls or change meaning. + +Shared buttons and chips use pill shapes, a quiet lower edge and a short lift +on hover or keyboard focus, then compress on press. Inputs and option tiles +share softer corners. Keep a distinct focus ring and a persistent selected +state; disabled controls must not pretend to activate. Reduced motion keeps +the colour, outline and selected position without decorative displacement. + +## Cast And Objects + +The brand mascot keeps its own identity, separate from ordinary participants. +Participant characters should leave room for that distinctive presence. + +| Meaning | Representation | +| --- | --- | +| The person using the pictured action | Green pawn (`YOU` / `--you`), in every panel | +| Somebody else | Blue pawn (`SKY`), or another distinct non-green illustration colour | +| Host role | Gold crown above the head: rounded three-point outline, dark gold edge and a quiet lower rim; the standalone absent-Host symbol uses the same outline, unfilled and dashed | +| Shared or watched media | Small television, including antenna, body and feet | +| Browser application | Browser chrome around content; not a second design of television | +| A captured window or display | Window title bar or display stand, with the shared media metaphor kept distinct | +| Server forwarding media | A server on the media path; opening a Site alone does not imply SFU | + +These are **identity/object colours**, not status. A green pawn remains green +in a failed scene; a successful scene does not turn other people green. Real +roster identities retain the shared UUID colour function, not the illustrative +"you versus others" palette. Actor positions and identities remain stable from +the first panel to the result. + +Room pawns, tooltip/status comics and introduction participants use +a round head, half-oval body and two restrained eyes. Reuse the shared +figure and face geometry in product illustrations so expressions stay inside +the head as it scales. Larger scenes can add a small lean; small status panels +may omit facial detail to keep the action clear. When a gesture needs hands, +use small floating round shapes. A held gamepad is a scene prop; a separate role +crown identifies the host and moves with the body. Its gold is an identity colour, +not a warning or achievement. The share control retains its +cast icon. The cast represents friends sharing games, drawings +and other screen content. Exploratory figures are not alternate +participant identities. Keep the brand mascot distinct. + +The couch has a back, seat, arms and feet. A full room reflows into rows of seats +without hiding people or requiring horizontal scrolling. Topology preserves +each participant and actual parent edge at a readable scale; reduce spare +spacing, then use a vertical outline when depth exceeds the available width. + +Couch upholstery uses the fixed warm orange/yellow `--couch` / `--couch-dark` +palette. Never draw upholstery from the participant palette or randomize it by +room. Keep automatic participant colours outside the upholstery's amber/orange +range and the crown's gold; check all eight tokens against both objects in light +and dark themes. The crown retains a bright gold fill with a dark gold edge. +Figures and exterior upholstery use flat fills without outline strokes. +Use differences in tone, spacing and name tags to keep people readable; retain +only the few interior seams that clarify cushions. Apply this convention to +the website and README cast as well as the product UI. + +## Semantic Colour And Shape + +Use the existing theme tokens in `src/client/styles.css`. The shared +`comic-presentation.ts` maps meanings to those tokens for tooltip outlines, +carets, result panels and verdict marks. Do not choose independent hex colours +or animation rules inside an individual scene. + +| Meaning | Token / code tone | Panel and symbol | +| --- | --- | --- | +| Explanation, preference, ordinary action, intentional closure or unknown state | Neutral ink / `off` | Neutral panels; illustrate the action/object without a success stamp | +| Confirmed completion or delivered media | Green `--live` / `live` | Neutral before, green result; check or completed object | +| Still usable with a limitation, unavailable optional capability, or intentional media pause | Amber `--warn` / `warn` | Neutral before, amber result; limit, pause, lock or unavailable mark on the affected part | +| Confirmed failed or blocked action | Red `--danger` / `bad` | Neutral before, red result; cross, broken path or other explicit failure mark | +| Work in progress or awaiting an action/frame | Blue `--action` / `busy` | Neutral before, blue current/result panel; connection, progress or play symbol | + +Recovery retains amber because service is interrupted while work continues. +Waiting for a Host and unknown observations remain neutral. A control's +prospective action is not proof of success: opening details, switching theme, +adjusting volume or offering P2P stays neutral. Actual copy feedback can change +that same hint to green or red, using the existing operation result. + +Two-panel comics always read before/context on the left and result/current +condition on the right. Only the result carries the semantic accent; a single +panel carries the current condition. Tooltip outline and caret use that same +tone in Chinese, English and visual modes. Neutral uses theme ink, not literal +white. Keep paper fills quiet; do not tint the whole scene green/yellow/red. +Meaning must also survive without colour through objects, symbols and accessible +localized names. Never show celebration or a completed live path as the verdict +of a failed or unavailable operation. + +A missing optional feature is not a failed media path. For example, unavailable +NAT prediction marks only extra candidate paths; ordinary P2P remains a neutral +possibility. A rejected PiP request is failure; lack of PiP support is a limitation. + +## Motion Grammar + +| Presentation | Rule | +| --- | --- | +| Action demonstration (`demo`) | Play one meaningful transition when shown, then hold its result | +| Work actually continuing (`progress`) | Repeat a gentle connection/recovery beat while that state exists | +| Settled success, failure, limitation, pause or idle (`still`) | Show the informative final pose immediately; no repeated stamping, shaking, blinking or celebration | + +Demonstrations and progress share the existing 3.2-second comic beat. The main +action belongs in its first half, followed by a readable hold; small internal +staggering is allowed. At most two independent movers per panel. An explanatory +slider describes purpose and available range, not its live numeric value. +Decorative sparkle must support the pictured action, never fake current success. + +Reduced motion and settled states reuse each scene's **same explicit final +pose**. Disabling animation alone is insufficient if the base SVG hides the +result or shows a misleading starting pose. Static styling is scoped to its SVG; +one still tooltip must not freeze another progress scene. New keyframes must end +in the documented result rather than reset for an obsolete loop. + +The tooltip panel itself uses the same restrained entrance/exit for every tone. +Replay belongs to opening/hovering/focusing the whole control, not just its SVG. +Ordinary controls keep their icons visible during hover and keyboard focus; +surface colour, lift, focus and press feedback carry the interaction. Reserve +icon drawing/replay for primary actions such as starting a share or joining a +room, and for explicit state-entry feedback. The whole control owns the trigger. +Stroke and solid drawings share one total duration and easing; stroke count +must not lengthen the animation. Disabled controls and passive status icons +retain their informative pose; busy indicators retain their progress beat. +Do not add a per-scene timer or another interaction/state owner. Outside status +comics, the brand mascot and participant figures may blink or glance occasionally; +a pictured screen can guide the Host's gaze. Keep eye gestures and small body +leans brief, separated by long pauses and offset between people. A participant's +UUID seeds the cadence; align its phase to wall time in every view, including +later mounts and return from background. Names, roster order and the observer's +role do not reseed it. Device clock skew bounds cross-device precision; cosmetic +motion adds no signaling or media state. They express personality independently +of connection feedback. Follow the system's reduced-motion preference, keeping a +natural, open-eyed pose when enabled; participant gestures have no separate +product playback control. Status comics still follow the table above. + +Introduction artwork is separate from product status. Website/README scenes may +repeat a short action with a generous pause. Keep broad movement to one or two +focal elements; small participant gestures follow the quiet cadence above. +Provide a pause or hide control for automatic loops, and a useful static pose +for reduced motion. Product-state comics still follow the table above. + +Entry screens may carry one quiet, original welcome line in Chinese or English +text mode. Pure-visual mode pairs familiar pictograms with static pixel +pseudo-lettering tied to the same line. This decoration is hidden from assistive +technology. The selected line stays stable during that visit, including mode +changes, and never replaces an action, error, loading message or connection +progress. Keep it on one line, apart from the actions; primary controls take +precedence when space is tight. + +## Ownership And Incremental Review + +- Existing status/operation facts outrank the scene default. Reused `warning` + artwork can represent a warning or a confirmed failure; pass the actual tone. +- `comic-presentation.ts` owns defaults and semantic tokens; `Frame`, `RedX`, + `rmBlock`, `Comic` and `HintComic` apply the shared grammar. Scenes own only + their cast, causal illustration and final pose. `Tooltip` owns interaction + and placement. Do not duplicate these rules in each scene or page. +- Keep the semantic kind and tone in all languages; localized text changes + expression, not severity. Symbol-only controls retain accessible names. +- For each changed scene inspect its start, transition and settled pose in + both themes, reduced motion and narrow layouts. Compare success, failure, + limitation and neutral neighbours in the catalogue. Code-level tests do not + establish visual clarity. +- Update this owner when accepting a new convention. Product documents link + here instead of copying the palette, cast or animation table. + +## Interaction References + +[Carbon status indicators](https://v10.carbondesignsystem.com/patterns/status-indicator-pattern/) +supports combining semantic colour with shape and symbols; +[Carbon motion](https://carbondesignsystem.com/elements/motion/overview/) +distinguishes functional feedback from occasional expressive movement. + +[Web Animations start time](https://developer.mozilla.org/en-US/docs/Web/API/Animation/startTime) +provides the native timeline alignment used by participant gestures. diff --git a/docs/guide/getting-started.md b/docs/guide/getting-started.md new file mode 100644 index 0000000..0e2a4ce --- /dev/null +++ b/docs/guide/getting-started.md @@ -0,0 +1,133 @@ +# Your first Piik room + +English · [简体中文](./getting-started.zh-CN.md) · [Back to Piik](../../README.md) + +Piik shares one person's screen with up to 20 invited viewers. This guide covers +watching, trying the online demo, using Piik App and sharing through an existing site. + +[Join a room](#join-a-friends-room) · [Online demo](#try-the-online-demo) · +[Piik App](#share-with-piik-app) · [Existing site](#share-from-your-browser) · +[Troubleshooting](#when-something-gets-in-the-way) + +Viewers need a browser and an invitation. To try sharing, use the online demo +or **Public invite** in Piik App. + +If Piik shows icons only, choose **EN** in the header to show the labels +used below. **中** switches to Chinese; **✦** returns to the illustrations. + +## Join a friend's room + +1. Open the invitation link your friend sent in a desktop or mobile browser. +2. Wait for their screen to appear. If the browser blocks autoplay, press **Play**. +3. Move the pointer over the picture or tap it to show the playback bar. + +The playback bar controls only your own picture and sound. + +
+Playback controls, at a glance + +| Control | What it does | +| --- | --- | +| **Theater mode** | Expands the picture inside the browser tab. | +| **Fullscreen** | Fills your screen. | +| **Picture in picture** | Opens a floating video where supported. | +| **Volume** | Goes up to 200% with audio boost, or 100% otherwise. Lower it if the sound distorts. | + +
+ +With just a four-digit room code, open the same Piik site and choose +**Join a room**. You may need the site's passphrase and the room's password. +The host decides whether code entry is available; ask for an invitation link if +you cannot join by code. + +If an old link no longer works, ask the host for the current invitation. +App Local rooms and temporary public invitations end when that App run stops. + +## Try the online demo + +1. Open [demo.piik.tv](https://demo.piik.tv) in a desktop browser that supports + screen sharing. +2. Select **Start sharing**, choose a screen, window or tab, and enable audio if + needed and offered by the browser. +3. Confirm the preview and select **Copy invite link**. Open that link on a + second device or send it to a friend. +4. Select **Stop sharing** when finished. + +The project hosts this site for you. It uses direct media connections (P2P); +restrictive networks may load the page but block video. + +## Share with Piik App + +1. Download a **`piik-app`** archive for your platform from + [GitHub Releases](https://github.com/TNTcraftHIM/Piik/releases) or the + [Gitee mirror](https://gitee.com/TNTcraftHIM/Piik/releases). Extract the whole + archive and keep its directory structure intact. +2. Open `piik-app.exe` on Windows, `Piik App.app` on macOS, or `./piik-app` on Linux. + The launcher opens in your system browser. +3. Choose **Public invite** for friends outside your local network, then select + **Open Piik**. Other modes are explained below. +4. Choose **Start sharing**, then select **Browser**, **Apps / Windows**, or + **Screens** as available. Pick the actual source and sound option. +5. Copy and send the room invitation. Keep both the App and sharing tab open. + +| Filename contains | Platform | +| --- | --- | +| `windows-amd64` | Windows x64 | +| `darwin-arm64` | Apple silicon; native capture requires macOS 13+ | +| `linux-amd64` | Linux x64 | + +Windows App and browser sharing are the primary tested paths. The macOS and +Linux apps have not yet been tested on physical devices; +[test results and feedback are welcome](https://github.com/TNTcraftHIM/Piik/issues). +Linux native capture also needs [system components](../../native/capture/linux/README.md). +See the [App guide](../../cmd/piik-app/README.md) for package and runtime details. + +### Choose an App mode + +| Mode | When to choose it | What to keep in mind | +| --- | --- | --- | +| **Local room** | Friends are on the same local network. | Your computer must be reachable from theirs. A local site passphrase is optional. | +| **Public invite** | Friends are elsewhere and you want a temporary invitation. | Needs Internet access. The public address lasts for this App run. | +| **Connect to Site** | Your group already has a Piik site. | Enter its address; the App adds capture capabilities to the site's sharing page. | + +**A page that opens does not guarantee a video connection.** Public invite gives +your room a temporary web address; picture and sound still travel between +participants. This mode has no media-server fallback, and the temporary address +has no uptime guarantee. All modes need a working UDP media path. +The [App guide](../../cmd/piik-app/README.md#modes) explains the connection setup. + +## Share from your browser + +You need an existing HTTPS site running Piik Server, provided by a friend or +administrator. If you do not have one, start with the [demo](#try-the-online-demo) +or [Piik App](#share-with-piik-app). Hosting a site yourself is an advanced option: +follow the [self-hosting guide](../operations/self-hosting.md) for server, HTTPS +and network setup. + +1. Open your group's Piik site on your computer. Enter its site passphrase if asked. +2. Choose **Start sharing**. In the browser picker, select the screen, window or + tab you want friends to see, and enable audio if offered. +3. Check the preview, then choose **Copy invite link** and send it to your friends. +4. Keep the sharing tab open. Use **Pause sharing**, **Switch source**, or + **Stop sharing** when you need them. + +Screen capture requires an HTTPS site or `localhost`. The sources and audio +offered by the picker depend on your browser and operating system. +Invitation links grant access to that room; share them with the people you want there. + +## When something gets in the way + +| What you see | Try this | +| --- | --- | +| Picture but no sound | Unmute the video. The host should choose a source with shareable audio; if sound was disabled, stop and start sharing with it enabled. App window/screen capture keeps that setting when switching sources. | +| No screen picker | Allow the browser or App to record the screen when the OS asks. Browser capture needs HTTPS or `localhost`; try sharing from a desktop computer. | +| Local invitation will not open | Check that both devices are on the same network and can reach each other. Guest Wi-Fi or firewall rules can block local access. | +| Page opens but video will not connect | Choose **Reconnect** in the playback bar. If it still fails, check [WebRTC connection settings](../../cmd/piik-app/README.md#chromium-webrtc-connections) or try a site with media fallback. | +| Sharing stops after sleep or suspension | Wake the device and return to the sharing tab; start sharing again if needed. Browser and OS suspension can interrupt capture or playback. | + +For a bug report, include the version, OS/browser, what you expected, and how +to reproduce it. [Diagnostics and export](../reference/configuration.md#diagnostics) +explains how to collect a local report and what to review before sharing it. + +Ready to host a site for your group? Follow [self-hosting](../operations/self-hosting.md). +For everything else, use the [documentation map](../README.md). diff --git a/docs/guide/getting-started.zh-CN.md b/docs/guide/getting-started.zh-CN.md new file mode 100644 index 0000000..5193627 --- /dev/null +++ b/docs/guide/getting-started.zh-CN.md @@ -0,0 +1,122 @@ +# 第一次用 Piik + +[English](./getting-started.md) · 简体中文 · [回到 Piik](../../README.zh-CN.md) + +Piik 支持一位房主分享屏幕,最多 20 位受邀观众通过浏览器观看。 +本指南介绍加入房间、体验在线 Demo、使用客户端,以及通过已有站点分享的方法。 + +[加入房间](#加入朋友的房间) · [在线 Demo](#体验在线-demo) · +[Piik App](#用-piik-app-分享) · [已有站点](#用浏览器分享) · [问题排查](#遇到问题了) + +观众只需要浏览器和邀请链接。 +初次体验分享,可以使用在线 Demo 或客户端的 **公网邀请**。 + +如果 Piik 只显示图示,点击顶部的 **中** 即可显示下文提到的按钮文字。 +**EN** 切换到英文,**✦** 返回图示模式。 + +## 加入朋友的房间 + +1. 用电脑或手机浏览器打开朋友发来的邀请链接。 +2. 等待画面出现。如果浏览器没有自动播放,点击 **播放**。 +3. 将鼠标移到画面上,或轻点画面,显示播放控制栏。 + +播放控制栏只改变你自己看到的画面和听到的声音。 + +
+播放按钮分别做什么? + +| 按钮 | 作用 | +| --- | --- | +| **剧场模式** | 在浏览器标签页内展开画面。 | +| **全屏观看** | 让画面铺满屏幕。 | +| **悬浮小窗** | 浏览器支持时,把视频放进独立小窗。 | +| **音量** | 支持增强时最高 200%,否则最高 100%;声音失真时调低一些。 | + +
+ +只有四位房间号时,打开同一个 Piik 站点,选择 **加入房间**。 +你可能需要站点口令和房间密码。能否通过房间号加入由房主决定; +无法加入时,请朋友发邀请链接。 + +旧链接失效了,就请房主重新发一份当前邀请。 +App 的本地房间和临时公网邀请会随本次 App 退出而结束。 + +## 体验在线 Demo + +1. 在支持屏幕共享的电脑浏览器中打开 [demo.piik.tv](https://demo.piik.tv)。 +2. 点击 **开始分享**,选择屏幕、窗口或标签页;如需声音且浏览器提供该选项,启用音频。 +3. 确认预览后,点击 **复制邀请链接**,用另一台设备打开,或发送给朋友。 +4. 体验结束后,点击 **停止分享**。 + +演示站由项目提供,画面通过设备间直连(P2P)传输。 +在受限网络下,可能出现页面能打开、画面无法连接的情况。 + +## 用 Piik App 分享 + +1. 从 [GitHub Releases](https://github.com/TNTcraftHIM/Piik/releases) 或 + [Gitee 镜像](https://gitee.com/TNTcraftHIM/Piik/releases)下载以 **`piik-app`** 开头、 + 对应平台的压缩包,完整解压并保留目录结构。 +2. Windows 打开 `piik-app.exe`,macOS 打开 `Piik App.app`,Linux 运行 `./piik-app`。 + 启动页会在系统浏览器里打开。 +3. 邀请不在同一局域网的朋友时,选择 **公网邀请**,再点击 **进入 Piik**。 + 其他模式的用途见下表。 +4. 点击 **开始分享**,按可用选项选择 **浏览器**、**程序 / 窗口** 或 **屏幕**, + 然后选定要分享的画面与声音。 +5. 复制房间邀请发给朋友。分享期间保持 App 和分享标签页打开。 + +| 文件名包含 | 适用平台 | +| --- | --- | +| `windows-amd64` | Windows x64 | +| `darwin-arm64` | Apple 芯片;原生采集需 macOS 13 及以上 | +| `linux-amd64` | Linux x64 | + +目前主要测试 Windows 客户端和浏览器分享。macOS 和 Linux 客户端尚未经过实机测试, +欢迎有设备的朋友试用并[反馈问题](https://github.com/TNTcraftHIM/Piik/issues)。 +Linux 原生采集还需要[系统组件](../../native/capture/linux/README.md)。 +程序包与启动参数的细节见 [App 指南](../../cmd/piik-app/README.md)。 + +### 选择 App 模式 + +| 模式 | 什么时候用 | 需要知道 | +| --- | --- | --- | +| **本地房间** | 朋友和你在同一个局域网。 | 对方需要能连接到你的电脑,可以设置本地站点口令。 | +| **公网邀请** | 朋友在别处,想临时发个邀请。 | 需要互联网连接;公网地址只在本次 App 运行期间有效。 | +| **连接站点** | 你们已经有一个 Piik 站点。 | 输入站点地址,在原有分享页面使用 App 的采集能力。 | + +**网页能打开,不代表视频一定能连通。** 公网邀请为房间提供临时网页地址, +画面和声音仍靠参与者之间的连接传输。此模式没有媒体服务器兜底, +临时地址也不保证持续可用。所有模式都需要可用的 UDP 媒体路径。 +具体连接方式见 [App 指南](../../cmd/piik-app/README.md#modes)。 + +## 用浏览器分享 + +需要先有一个运行 Piik Server 的 HTTPS 站点,可以使用朋友或管理员提供的地址。 +还没有站点时,请先体验[在线 Demo](#体验在线-demo) 或使用[客户端](#用-piik-app-分享)。 +自建站点属于进阶操作,服务器、HTTPS 和网络端口的配置步骤见[部署指南](../operations/self-hosting.zh-CN.md)。 + +1. 在电脑上打开 Piik 站点。如果站点设置了口令,先输入口令。 +2. 点击 **开始分享**,在浏览器弹窗里选择屏幕、窗口或标签页; + 如需声音且浏览器提供该选项,启用音频。 +3. 确认预览画面后,点击 **复制邀请链接** 发给朋友。 +4. 保持分享标签页打开。需要时使用 **暂停分享**、**切换来源** 或 **停止分享**。 + +浏览器屏幕采集需要 HTTPS 站点或 `localhost`。 +弹窗里有哪些画面和声音可选,取决于浏览器与操作系统。 +拿到邀请链接的人可以进入这个房间,请把它发给你想邀请的人。 + +## 遇到问题了 + +| 你遇到的情况 | 可以先试什么 | +| --- | --- | +| 有画面,没声音 | 先取消视频静音。房主应选择支持分享声音的来源;如果开始时关闭了声音,请停止后重新开始并打开声音。通过 App 采集窗口或屏幕时,切换来源会保留原来的声音设置。 | +| 没有屏幕选择弹窗 | 按系统提示允许浏览器或 App 录制屏幕。浏览器采集需要 HTTPS 或 `localhost`,请在电脑上尝试分享。 | +| 本地邀请打不开 | 确认两台设备在同一网络且可以互相访问。访客 Wi-Fi 或防火墙规则可能阻止本地连接。 | +| 网页能打开,画面连不上 | 点击播放控制栏的 **重新连接**;仍然失败时,检查 [WebRTC 连接设置](../../cmd/piik-app/README.md#chromium-webrtc-connections),或尝试有媒体服务器兜底的站点。 | +| 休眠或挂起后分享中断 | 唤醒设备,回到分享标签页,必要时重新开始分享。浏览器或系统挂起可能中断采集和播放。 | + +反馈问题时,请附上版本、系统与浏览器、预期结果和复现步骤。 +[诊断与导出](../reference/configuration.md#diagnostics) +说明如何收集本地报告,以及分享前需要检查哪些内容。 + +想为朋友搭一个站点?从 [部署指南](../operations/self-hosting.zh-CN.md) 开始。 +其他内容可在 [文档导航](../README.md) 找到。 diff --git a/docs/maintenance.md b/docs/maintenance.md index 3a7f429..5942301 100644 --- a/docs/maintenance.md +++ b/docs/maintenance.md @@ -1,68 +1,95 @@ -# 项目维护与上下文治理 +# Documentation Ownership -## 目标 +The repository is the durable source across sessions, devices and agents. +Use [the documentation map](./README.md) to find a topic, then update its owner. +Chat, agent summaries and an old branch do not replace current code, contracts +and evidence. Task authority stays in [AGENTS.md](../AGENTS.md); delivery and +verification procedures stay in [CONTRIBUTING.md](../CONTRIBUTING.md). -仓库是跨 session、设备和 agent 的持久事实源。会影响后续工作的结论必须先进入其权威文档;聊天、agent 摘要、旧分支和临时计划都不能替代仓库真相。 +## Owners -常驻上下文只保留每次工作都需要的边界和导航。细节按需读取,完成历史交给 Git。任务范围和交付遵循 `AGENTS.md`,最小正确结果遵循仓库内的 [`ponytail` skill](../.agents/skills/ponytail/SKILL.md);本文件只维护仓库特有的事实治理规则。 - -## 事实分层 - -| 内容 | 权威归属 | 维护方式 | +| Topic | Owner | Keep here | | --- | --- | --- | -| 每次都必须遵守的硬约束和导航 | `AGENTS.md` | 保持短、单义、稳定;不复制流程手册或临时状态 | -| 分支、研究、验证、PR、合并和清理流程 | `CONTRIBUTING.md` | 维护可执行的日常流程 | -| 产品总览和领域导航 | `docs/project-memory.md` | 只保留每次 substantial task 都需要的摘要与链接 | -| 当前生产、里程碑、阻塞项和明细索引 | `docs/status.md` | 只保留 current/index;接近 120 行或 12 KB 时提示园艺审查 | -| 当前未决、已授权或待用户决策的工作 | `docs/todo.md` | 不是完成记录;不把 checkbox、实验或 agent 建议自动升级为 TODO | -| 当前产品合同 | `docs/product/` | 每个领域一个 owner;只写行为、边界和非显然不变量 | -| 架构取舍 | `docs/adr/` | 明确状态;新决定替代冲突旧决定 | -| 可复核证据和研究结论 | `docs/research/` | 标日期、来源、推断、适用边界和许可证 | -| 当前验证与部署边界 | `docs/verification-status.md`、`docs/deployment.md`、`docs/operations/`、`docs/reference/` | 按需读取,不保存发布流水账 | -| 已完成工作的时间线 | commit、PR、issue | 不复制进常驻 memory 或 status | +| Universal constraints and navigation | `AGENTS.md` | Short mandatory boundaries and links; no procedure manual | +| Branches, review, validation, PRs, releases and cleanup | `CONTRIBUTING.md` | Executable workflow | +| Module boundaries, interface discipline and ablation | [Engineering](./reference/engineering.md) | Current responsibility map and shared coding rules | +| Names, terminology and voice | [Naming and copy](./reference/naming.md) | Product/tool names, role labels and Chinese/English writing conventions | +| Illustration, palette, motion and accessible layout | [Visual language](./design/visual-language.md) | Shared Browser/App design grammar | +| Status projection | [Media status](./design/media-status.md) | Which facts each display surface may express | +| Release identity and compatibility | [Versioning](./reference/versioning.md) | Version roles, protected interfaces and public-readiness boundary | +| Current product behavior | `docs/product/` | One owner per domain; promises, boundaries and non-obvious invariants | +| Architecture decisions | `docs/adr/` | Decision status, rationale and consequences; explicitly supersede changed clauses | +| Measurements and external evidence | `docs/research/` | Date, sources, observations, inference, limits and license scope | +| Deployment and configuration | `docs/deployment.md`, `docs/operations/`, `docs/reference/configuration.md` | Runnable operator reference; no per-release diary | +| Compact product map | `docs/project-memory.md` | Stable summaries and domain navigation, not implementation inventories | +| Current execution/deployment index | `docs/status.md` | Current boundaries and pointers; exact artifact identity stays in release metadata | +| Remaining work and decisions | `docs/todo.md` | One task ledger, with executable work separated from parked ideas | +| Unresolved physical acceptance | `docs/verification-status.md` | Still-relevant evidence gaps across modules | +| Completed history | Git commits and PRs | Changes and outcomes; do not reproduce them in memory or TODO | -每个事实只有一个 owner。顶层文件提供链接和当前结论,不复制深层正文;`docs/README.md` 维护完整文档索引。 +Source schemas and existing conformance fixtures own exact interface fields; +package comments own local lock/resource invariants. The engineering reference +links to them instead of copying every field. README and guides explain how to +use the product; technical details link to the relevant owner. English/Chinese +entry guides are intentional translations and must change together; technical +references keep one version. -## 顶级指令卫生 +## Updating A Rule -[官方调研](./research/agent-context-governance.md)的共同结论是:顶层指令应是短而稳定的地图,流程和专业细节按需加载,并定期删除冲突或陈旧规则。检查器把官方边界分成推荐线和硬顶:Codex 项目指令链在默认 32 KiB 上限的 80% 提示、到上限失败;Claude 的有效启动指令在官方建议的 200 行提示,并容许到 250 行的项目缓冲;Hermes 在动态下限 20,000 字符的 80% 提示、到下限失败。`CLAUDE.md` 导入的 `AGENTS.md` 计入有效行数;必读的 repo Ponytail skill 使用同一保守包络。 +1. Trace the current product contract, actual callers and relevant evidence. + Search hits are leads, not proof of a defect. Separate an implementation + mismatch from a historical decision or a still-unaccepted proposal. +2. Update the existing owner first. Add a new owner only for a distinct topic + with real consumers; give it an entry in the documentation map. Delete + replaced normative text elsewhere and link to the owner. +3. Keep current owners consistent. Update memory/status only when their compact + snapshot materially changes; put unresolved work in TODO. Checkpoint and + integration timing follows CONTRIBUTING, not an automatic extra commit for + every small edit. -推荐线只触发园艺提醒,不是压缩目标。硬顶失败时应把目录专属规则迁到嵌套或 path-scoped context,把流程和参考资料迁到按需 skill/文档,简化重复措辞,并删除已完成历史或陈旧结论;不要为了过闸损失仍有消费者的约束。模块特有规则只有在对应代码存在且每次读取确有必要时,才放到最近目录的 `AGENTS.md`;流程说明进入 `CONTRIBUTING.md`,当前工作进入 `docs/todo.md`,证据与理由进入其 owner。 +When semantics are disputed, record the decision needed and hold only dependent +implementation. At handoff or a material phase change, persist accepted rules, +remaining work and actual blockers before relying on a conversation summary. +Do not store tool transcripts, raw logs, temporary paths or repeated completed +checklists as product truth. +After an audit, put accepted constraints in their existing owners and unresolved +work in TODO, then remove the report and handoff bundle. Do not create dated +audit-summary directories or replacement archives of completed work. -## 真相更新顺序 +Published copy describes Piik's behavior and design directly. Remove discarded +design labels, private operational details and conversational comparisons to +other products. Retain primary technical evidence and required attribution or +license notices; rewriting prose must not obscure the origin of reused work. -1. 按 [`AGENTS.md`](../AGENTS.md) 的 authority 和 scope 规则,对照完整产品模型、当前代码、测试和证据确定目标;语义未定时只冻结依赖项。 -2. 就地更新受影响的 product、ADR 或 research owner;仅在快照确实变化时同步 memory/status,未决执行项进入 TODO。 -3. 检查各 current owner 一致并形成 Git-tracked truth checkpoint,再开始依赖该结论的实现或派发。 +## Consolidation And Ablation -语义仍未决定时,在 TODO 记录 hold,只冻结依赖项。具体 Git、worktree、checkpoint 和 integration 流程由 `CONTRIBUTING.md` 拥有。 +Keep one owner per fact, with short summaries where navigation needs them. +Consolidation is not concatenating documents into one giant manual. Remove +stale procedures and duplicated prescriptions; preserve still-consumed behavior, +failure boundaries, reasoning and reproducible evidence. An ADR can retain a +historical decision when its superseded status is clear. -## 机制减负审查 +Document cost is part of [engineering ablation](./reference/engineering.md#ablation-and-review). +Do not delete a product promise solely to shrink a file. Archive completed work +in Git, keep candidate ideas out of active instructions, and never promote an +agent suggestion to accepted policy simply by writing it in a checklist. -机制减负遵循 [`ponytail`](../.agents/skills/ponytail/SKILL.md) 的最小正确结果原则,范围由 `AGENTS.md` 约束。本仓库的审查先只读清点机制服务的用户行为,量化移除后的真实退让,并与代码、配置、迁移、测试、运行时状态、部署和故障面的整体成本比较。文档也是需要同步和审查的机制:长期真相只保留产品合同、非显然不变量、算法/模型、关键取舍和外部证据,普通 UI 与实现细节由代码、测试和 PR 持有。若 owner 接受有界退让,先更新产品合同再删除失去必要性的完整表面;否则保留合同并寻找更小实现。审查不以行数为目标,也不授权无边界重构。 +The top-level context stays a map. Directory-specific instructions belong near +the code only when that module exists and the rule is needed every time. +The [context-governance research](./research/agent-context-governance.md) owns +the external rationale; the existing hygiene scripts own executable limits. +Their gardening warnings invite review, not arbitrary compression; hard failures +for broken links, required owners and instruction-loading limits remain binding. -TURN 的减负证据由 [ADR-0005](./adr/0005-automatic-hybrid-media-routing.md) 和相关 transport research 持有;轻量/SQLite 房间取舍由 [ADR-0002](./adr/0002-memory-resident-protected-rooms.md) 持有。这些案例只证明应按当前消费者重审完整成本,不能被推广为永远删除或永远保留某类机制的规则。 +## Checks -## 检查点与恢复 +For documentation work use the existing link, whitespace and repository-hygiene +checks; do not rerun unrelated media benchmarks. The shared +[`required-project-paths.txt`](../scripts/required-project-paths.txt) lists durable +owners for both hygiene entry points. No additional checker is needed for each +new convention. -在需求或优先级被接受、设计或研究结论改变、实质阶段切换、分支/PR/agent 交接、可能 compaction 或长暂停前更新事实 owner。不要为每个函数、工具调用或未形成结论的探索建立检查点。 - -检查点按 [`AGENTS.md`](../AGENTS.md) 的 durable-truth 规则,只保存以后仍需要的结论、当前状态、下一步和真实阻塞;不保存原始对话或工具输出、完整日志、临时路径、可廉价重查的事实和过程性 TODO 流水账。 - -恢复上下文时依次读取 `AGENTS.md`、project memory、status、TODO、相关 product/ADR/research,然后检查当前分支、`git status`、近期 commit 和 PR。摘要与仓库冲突时,以当前代码、测试和权威文档为准。 - -## 清理规则 - -- `project-memory.md` 和 `status.md` 接近预算时,先删除可从代码或 Git 恢复的完成/失效内容,再把仍需保留的细节迁到现有 owner;不要反复微压缩句子。 -- `todo.md` 只保留真实未完工作、明确 hold 和待用户决策,不保留完成清单或一次性审计过程。 -- `verification-status.md` 只保留仍影响验收的跨模块证据;成熟结论迁回 product、ADR、research 或 operations,过期结果删除。 -- Product 合同不追加确认流水;研究失效时更新正文及最后验证日期;只有仍有解释价值的历史取舍进入 ADR。 -- 每个重要 PR 检查 current 文档是否互相矛盾、链接是否有效、memory/status/TODO 是否仍代表现在。 - -## 验证与自动化 - -- 迭代中运行覆盖当前改动的最小定向检查;完整套件、真实浏览器矩阵、网络整形、跨设备、长时稳定性和部署检查在相关候选的验收边界批量运行。 -- 文档改动默认运行本地 Markdown 链接、whitespace、diff 和 repository-hygiene 检查;不因无关文档变化重跑媒体基准。 -- 断链、缺失必需 owner 和格式损坏是 hard failure。每次注入的有效 `AGENTS.md`/`CLAUDE.md` 上下文与必读的 repo Ponytail skill 使用“推荐线 warning、硬顶 failure”,避免把建议值误当死线,也避免超过工具加载边界。Product、ADR、research、operations 等按需文档的行数/字节阈值始终只是 warning:它提示检查是否应拆分二级 owner、删除历史或移出过程信息,不要求为通过门禁压缩合理正文,也不是交付质量指标。 -- 昂贵证据记录 commit、环境、结果和适用边界。只有相关路径、协议、配置、依赖、环境或门槛改变,或旧证据失败/含糊时才重跑。 -- `.githooks/pre-commit`、`scripts/check-project-state.sh` 和 PowerShell 等价入口共享 `scripts/required-project-paths.txt`。Hook 只做快速、确定、可复现的检查;工程判断留给评审和测试。 +At a meaningful integration boundary check that current documents agree, links +resolve, and memory/status/TODO still describe what remains. Expensive evidence +retains its measured revision, environment and limits; rerun it when a relevant +contract, dependency, platform, workload or unexplained failure changes. diff --git a/docs/operations/github.md b/docs/operations/github.md new file mode 100644 index 0000000..64add0c --- /dev/null +++ b/docs/operations/github.md @@ -0,0 +1,94 @@ +# GitHub Repository And Releases + +[Versioning](../reference/versioning.md) owns release meaning and compatibility; +[CONTRIBUTING](../../CONTRIBUTING.md) owns review and local validation. This file +owns the one-time GitHub settings. The source repository is public; automatic +package publication has its own activation gate below. + +## Main And Pull Requests + +Use squash merging only, with the PR title and body as the squash commit. These +repository settings are applied. Keep automatic branch deletion off: the normal +cleanup step checks local worktrees and remaining references first. + +Protect `main` with PRs required, zero mandatory peer approvals for the solo +maintainer, administrator enforcement, linear history, and no force pushes or +deletions. Do not require cloud PR checks while branch/PR CI is intentionally +quiet; local validation and acceptance precede the squash merge. PR/issue +templates live in `.github/`; [naming](../../CONTRIBUTING.md#commit-pr-and-branch-names) +is shared with automatic version selection. + +These protections are active, including conversation resolution. GitHub Pages +uses the **GitHub Actions** publishing source and the `piik.tv` custom domain; +the repository homepage points to `https://piik.tv`. The dedicated Website +workflow publishes the static site under [website operations](./website.md). + +## Enable Automatic Publication Once + +Before enabling, accept the release packages, compatibility baseline, public +copy and repository visibility; finish main protection. Then set repository +Actions variable `PIIK_RELEASES_ENABLED` to `true`. This is a repository setting, +not an environment variable for Piik App or Server. + +Subsequent accepted PRs merged to main use the existing CI workflow: calculate +one version, validate, package Server and all App targets, and publish the same +artifacts. Runs queue rather than overlap or cancel one another. Branches/PRs +stay quiet; manual `client_checks` dispatch remains available for candidates. +Writing a new version back to main is unnecessary and prohibited. + +For a complete publisher rehearsal, dispatch with `client_checks: true` and an +explicit `candidate_version` such as `v1.0.0`. This builds genuine Server and +native App artifacts under one revision and version without tagging or +publishing. Prepare its [release-note section](../reference/versioning.md#release-notes) +and inspect the generated preview in the run summary. For a local preview, run +`node scripts/release-notes.mjs v1.0.0 FULL_SOURCE_SHA` from the checkout with +full history and tags. Download that run's artifacts and pass their directory, version and +full revision to `scripts/publish-release.mjs --dry-run` (put the flag after +the three positional arguments). An omitted candidate version keeps the usual +development/exact-tag identity. + +Enable GitHub's built-in release immutability for formal publication where +available. Upload all assets to the draft before publishing. A failed upload can +resume its same-source draft; a published version is never clobbered. If its +files are wrong or incomplete, publish a corrected new version. An older retry +must not replace a newer release's `latest` designation. + +To stop future automatic publication, remove/set false the activation variable; +this does not alter existing releases or a currently running workflow. Inspect +the active run separately. No pipeline step changes visibility or installs an +update into a running Piik deployment. + +## Gitee Download Mirror + +[TNTcraftHIM/Piik](https://gitee.com/TNTcraftHIM/Piik) is a public README/Release +mirror. Source, documentation and issues stay on GitHub. The publishing job uses +the repository Actions secret `GITEE_TOKEN`, which is configured; rotate it there +when needed. Never put it in source, a remote URL or a distributed package. +GitHub publication continues to use the workflow's scoped token. + +After GitHub publication, the same job runs `scripts/mirror-release.mjs` on the +original artifacts. Upload failure leaves the Gitee release marked as a preview. +Rerun the failed publishing job to verify existing files and upload only missing +ones. GitHub's published release remains unchanged. Both publishers reject +changing a published package; use a corrected new version if its bytes are wrong. + +If the original Actions artifacts have expired, download that release's files +from GitHub into an external working directory, then run: + +```sh +gh release download v1.0.0 --repo TNTcraftHIM/Piik --dir /tmp/piik-mirror +node scripts/mirror-release.mjs /tmp/piik-mirror v1.0.0 FULL_SOURCE_SHA +``` + +Use the release's actual version and full `target_commitish` SHA, with +`GITEE_TOKEN` supplied through the publisher environment. `--dry-run` after the +three arguments checks local identity, checksums and attachment sizes without +publishing. The [release-source policy](../reference/versioning.md#release-sources) +owns provenance, selection and quota limits. Small-file upload/download acceptance +has passed; full-package mirror acceptance remains part of release readiness. + +## Platform References + +- [Protected branch availability](https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-protected-branches/about-protected-branches) +- [Workflow concurrency and queued runs](https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax#example-queueing-multiple-pending-runs) +- [Immutable release workflow](https://docs.github.com/en/code-security/concepts/supply-chain-security/immutable-releases) diff --git a/docs/operations/self-hosting.md b/docs/operations/self-hosting.md index f6b2b8a..e54be75 100644 --- a/docs/operations/self-hosting.md +++ b/docs/operations/self-hosting.md @@ -1,226 +1,93 @@ -# Self-Hosting Operations +# Deploy Piik Server -This runbook owns initial single-host service setup. Application releases use -[deployment](../deployment.md); accepted environment values and ports are in -[configuration reference](../reference/configuration.md). +English · [简体中文](./self-hosting.zh-CN.md) · [Documentation](../README.md) -## Embedded Media Configuration +Piik Server packages the web interface, room management and optional media +forwarding in **one server binary**. Extract and run it; room data is stored in SQLite. -The current source embeds STUN and SFU in the Piik process under -[ADR-0013](../adr/0013-embedded-node-local-media.md). Hosted `STUN_URLS` -advertises ordinary discovery and enables the UDP 3478 listener on -`STUN_LISTEN_HOST` (IPv4, default `0.0.0.0`). Enabling -`NAT_PREDICTION_ENABLED` also binds UDP 3479/3480 on that address. Local and -public-link App modes use discovery without creating media-service listeners. +## Try it locally -Set `SFU_UDP_PORT=7882` to enable automatic embedded SFU fallback. Its IPv4 bind -address is `SFU_LISTEN_HOST`, default `0.0.0.0`; `SFU_PUBLIC_IP` optionally -overrides the advertised IPv4 address when the host is behind NAT. Unset or -blank `SFU_UDP_PORT` keeps SFU disabled. SFU control uses authenticated Piik -signaling through the Web origin. No separate media-service executable, control -origin, or API credentials are needed by this source contract. +These server instructions use a Linux x64 machine. -These templates describe the embedded deployment. Its first cutover requires -the active-session check, matching artifacts and public media verification below. - -## Single-Host Topology - -```text -Browser -- HTTPS/WSS --> nginx :443 --> Piik :8787 -Browser <---------- DTLS-SRTP P2P ----------> Browser -Browser -------- ordinary STUN/UDP --------> Piik :3478 -Browser -------- optional STUN/UDP --------> Piik :3479/3480 -Browser <--------- DTLS-SRTP/UDP ----------> Piik :7882 -``` - -The public baseline uses one Piik Go binary, nginx, a valid Web TLS -certificate, and time synchronization. nginx terminates Web HTTPS/WSS and -proxies only to Piik's private application listener; media UDP reaches the -Go process directly. There is no separate SFU token endpoint or TCP 7880 service. -Web and STUN may use different DNS names on the same public IP. Another proxy or -firewall owner needs its own matching updater and recovery checks. - -Expose only the public listeners in -[configuration reference](../reference/configuration.md). Piik TCP 8787 stays -private. STUN answers Binding requests only; TURN allocations remain unavailable. - -## Templates - -- [Piik systemd unit](../../deploy/systemd/piik.service.example) -- [nginx site](../../deploy/nginx/share.bonfire.icu.conf.example) - -Copy templates outside the repository, inject independent secrets through an -access-restricted environment file or service manager, and adapt only addresses, -domains, certificates, users, and resource limits required by the host. - -## Initial Setup - -1. Configure DNS and TLS for the Web origin and STUN name. -2. Install nginx. The Piik release is one static binary and needs no - language runtime or separate media-service executable. -3. Create an unprivileged `piik` service account, `/opt/piik/releases`, - `/opt/piik/uploads`, and an access-restricted environment file. -4. Provision one verified initial immutable release and atomically point - `/opt/piik/current` to it before enabling the unit. The tracked release - wrapper is upgrade-only and deliberately refuses a missing prior release; - this repository does not provide a generic first-install transaction. -5. SQLite is the Hosted default. The service template selects - `/var/lib/piik/rooms.sqlite` in its service-owned StateDirectory; retain - that absolute path in the environment file and include it in the host's - backup/recovery policy. Use `ROOM_DATABASE_PATH=:memory:` only for intentional - process-only rooms. Do not leave an empty override in a service environment. -6. Install the tracked service and proxy templates. Bind HTTP to loopback on - bare metal; STUN and SFU retain their independent public IPv4 binds. - Containers may bind HTTP to `0.0.0.0` only when publishing/firewall rules - preserve the private application boundary. -7. Apply the public-port allowlist and confirm every retired TURN/TCP port - is closed. -8. Start Piik, then nginx. Piik binds its configured media listeners - before accepting signaling and closes them with the application; a listener - bind failure rolls back startup. Verify public UDP reachability separately. - -## Optional Container - -The [runtime-only Dockerfile](../../deploy/container/Dockerfile) consumes the -existing linux/amd64 Server release; it does not rebuild Go or Browser assets. -Its pinned [Distroless static non-root base](https://github.com/GoogleContainerTools/distroless) -includes CA certificates for HTTPS release checks and has no shell or package -manager. This is an optional recipe, not a published image or automatic installer. -On the local Linux Docker runtime, non-root/read-only startup, both persistence -modes, diagnostics export, STUN Binding and media listener cleanup pass. That -check covers the container lifecycle, not public-network SFU media or the host's -proxy/firewall configuration; verify those on the target deployment. - -Verify the release archive against its descriptor and manifest using the -[release procedure](../deployment.md), then extract its four files into a new -build-context directory: `piik-server`, `LICENSE`, `THIRD-PARTY-NOTICES.txt` -and `REVISION`. Use that directory, not the repository or a secrets directory: +Download the **Linux x64 Server** archive from [GitHub Releases](https://github.com/TNTcraftHIM/Piik/releases) +or the [Gitee mirror](https://gitee.com/TNTcraftHIM/Piik/releases), +extract it, and run the following in its directory: ```sh -docker build --platform linux/amd64 \ - -f /path/to/Piik/deploy/container/Dockerfile \ - -t piik: /path/to/extracted-runtime -docker run --rm --env-file /path/to/piik.env \ - piik: --check-config +./piik-server ``` -Use the existing [production configuration](../reference/configuration.md). -Keep `LISTEN_HOST=0.0.0.0` inside the container and retain the host's existing -HTTPS/WebSocket proxy. With Docker port mapping, set `SFU_PUBLIC_IP` to the -reachable server IPv4 address when SFU is enabled; container-private candidates -are not Internet-reachable. Advertised STUN names must resolve to the server's -public address, and UDP must reach the container directly. +Open `http://localhost:8787`. This is a local trial; use the configuration below +to let friends connect over the internet. To start a temporary room on your computer, +use the [Piik App guide](../guide/getting-started.md). -The following example publishes the standard listeners and keeps HTTP private -to a proxy running on the same host. Omit UDP publish options for disabled SFU -or auxiliary STUN listeners; use the matching port if configuration changes it. +## Put it online + +You need a Linux x64 server and a domain pointing to its public IP. +The example uses `share.example.com`; replace it with your domain. + +### 1. Configure and start Piik + +Create a `.env` file next to the binary: + +```dotenv +PIIK_ENV=production +LISTEN_HOST=127.0.0.1 +PUBLIC_BASE_URL=https://share.example.com +STUN_URLS=stun:share.example.com:3478 +MAX_VIEWERS_PER_ROOM=20 +SITE_ACCESS_PASSWORD= +``` + +Run these commands from that directory as a regular user: ```sh -docker run -d --name piik --restart unless-stopped --stop-timeout 20 \ - --read-only --cap-drop ALL --security-opt no-new-privileges \ - --env-file /path/to/piik.env \ - --mount type=volume,source=piik-data,target=/home/nonroot \ - -e ROOM_DATABASE_PATH=/home/nonroot/rooms.sqlite \ - -p 127.0.0.1:8787:8787/tcp \ - -p 3478:3478/udp -p 3479:3479/udp -p 3480:3480/udp -p 7882:7882/udp \ - piik: +./piik-server --check-config +./piik-server ``` -The image's `/home/nonroot` is owned by UID/GID `65532:65532` with mode `0700`. -A new named volume inherits that directory; an existing volume or bind mount -must already be writable by that identity. Keep this volume when replacing the -container. Set `ROOM_DATABASE_PATH=:memory:` for memory-only room authority. Diagnostics -remain opt-in and use `/home/nonroot/logs`; [export and retention](../reference/configuration.md#diagnostics) -remain the operator's responsibility. Do not mount application files writable. -With diagnostics enabled, `docker kill --signal=USR1 piik` requests a local -export without stopping the container; the ZIP remains in the mounted log directory. +The server reads `.env` automatically and stores rooms in `rooms.sqlite` in the +working directory. A blank `SITE_ACCESS_PASSWORD` allows +entry without a site passphrase; set 8–128 visible ASCII characters to require +one. Room invitations and access settings still apply. -Check `/healthz` from the host and public proxy, then verify configured STUN/SFU -UDP and room/media behavior using [operational verification](#operational-verification). -There is no shell-based healthcheck in the image. Retain the previous image, -environment and volume backup before replacement. Stop the old container before -reusing its listeners or SQLite volume, and recreate with the previous image and -environment if verification fails. The bare-metal `release-app.sh` wrapper does -not manage containers. The first move from external media services still follows -the coordinated cutover below; this recipe changes no proxy or firewall itself. +### 2. Enable HTTPS -## Coordinated Embedded-Media Cutover +Use your existing HTTPS reverse proxy to forward to `127.0.0.1:8787` with +WebSocket support. If you do not have one, [install Caddy](https://caddyserver.com/docs/install) +and add this to its Caddyfile: -Replacing the external STUN/SFU services, including Node-to-Go where still -needed, is one explicitly authorized infrastructure and protocol transaction. -The routine application wrapper cannot perform or recover it. +```caddyfile +share.example.com { + reverse_proxy 127.0.0.1:8787 +} +``` -1. Prepare and verify matching Web/Server signaling and native protocol v9 - App artifacts. Check active sessions and obtain the owner's acceptance of - share interruption before cutover; old pages and Apps must reload or - update together. -2. Retain the exact prior release path, Piik unit and drop-ins, environment, - nginx configuration, and external media-service units, configuration, secrets, - and enabled/active state for rollback. Keep this material access-restricted - outside the repository. Define recovery for any persistent-state or firewall - surface the transaction actually changes. -3. Stage the current unit with - `ExecStart=/opt/piik/current/piik-server` and - `PIIK_ENV=production`. Remove retired `NODE_ENV`, `PEER_ASSISTED_MEDIA` - and external `LIVEKIT_*` configuration; use the current - [environment contract](../reference/configuration.md), including - `SFU_UDP_PORT=7882` when fallback is enabled. Replace route `NODE_DEBUG` with - `PIIK_DEBUG=route` when needed. Validate the new binary's production - configuration as the service user before switching. -4. Stop Piik and the old coturn/LiveKit services. Disable those old media - services and remove the installed Piik LiveKit-readiness drop-in so they - cannot reclaim ports or delay startup. Verify that UDP 3478/3479/3480/7882 - are released before the new Go process binds them. Install the current unit, - environment and nginx template, remove the old RTC proxy locations, validate - nginx, and reload systemd. Preserve unrelated proxy headers, limits, TLS, - asset handling, and firewall rules. -5. Atomically point the current release at the verified candidate, start - Piik, and reload nginx. Verify the actual unit command and environment, - local/public health, the matching Web asset, UDP listener ownership, public - STUN Binding and admitted SFU media, and the agreed active-session outcome. -6. On failure, stop the new Go process and confirm it released its UDP sockets. - Restore the exact prior application path, unit/drop-ins, environment, proxy - and changed infrastructure configuration; reload systemd and validate nginx. - Restore the previous external service states before starting the previous - application with its matching unit, then prove health and media. Restore - matching pages and Apps as part of protocol rollback. A symlink-only - rollback cannot recover this transaction. +Reload Caddy. It obtains and renews the certificate automatically when DNS points +to the server and TCP 80/443 are reachable. See [Caddy's HTTPS proxy guide](https://caddyserver.com/docs/quick-starts/reverse-proxy#https). -Retain the recovery material through the agreed acceptance window. This is a -single replacement contract with no permanent legacy aliases or migration mode. -Later application-only releases use the ordinary -[release wrapper](../deployment.md#atomic-cutover). +### 3. Open the ports and verify -## Optional NAT Prediction +Allow **TCP 80/443** for HTTPS and **UDP 3478** for STUN in the server firewall +and cloud security group. Keep TCP 8787 private. The STUN hostname must resolve +directly to the server; a CDN HTTP proxy does not forward its UDP traffic. -Allow UDP 3479 and 3480 in both the cloud security group and the existing -nftables input rule, validating the complete ruleset with -`nft -c -f /etc/nftables.conf`. Set `NAT_PREDICTION_ENABLED=true` and restart -Piik within an accepted service window. Verify `ss -lunp` shows all three -Piik STUN listeners and that a public Binding request succeeds on each. -Do not add auxiliary URLs to `STUN_URLS`; enabled connections derive them from -the ordinary STUN authority. Keep both firewall rules while the capability is -enabled. Set `NAT_PREDICTION_ENABLED=false` and restart Piik before removing -the auxiliary firewall rules. Avoid verbose STUN/ICE logging because those -requests contain client IP addresses. +Open `https://share.example.com/healthz`; it should return `{"status":"ok"}`. +Then open the site, share a screen and join from another device. This initial +configuration uses P2P media, so participants need a usable UDP path. -These same-IP auxiliary ports measure destination-port mapping behavior. They -are not a full RFC 5780 alternate-address deployment; that requires a second -public IPv4. +## Optional: media fallback -## Operational Verification +Add `SFU_UDP_PORT=7882` to `.env`, allow UDP 7882, and restart Piik to enable +automatic SFU fallback. When the server is behind NAT, also set `SFU_PUBLIC_IP` +to its reachable public IPv4 address. The same binary provides the fallback. -- `/healthz` returns `{"status":"ok"}` locally and through the public origin. -- Piik and nginx are active with expected restart counts and bounded - logs/resources; Piik owns the configured STUN/SFU UDP listeners. -- The official Trickle ICE sample obtains an ordinary UDP `srflx` candidate from - the configured STUN service and no relay candidate. -- A direct room presents media on two networks when Peer ICE succeeds. -- A constrained route creates one managed Host publication and only admitted - subscriptions over embedded SFU UDP; Peer descendants remain ordinary P2P. -- Blocking all UDP reaches a clear bounded failure. -- Restarting Piik in stable mode retains room authority while Browser - sessions and media reconnect from fresh process state. +## Keep it running and update -Representative network, mobile, audio, 20-Viewer, and long-running evidence is -tracked in [verification status](../verification-status.md), not in this runbook. +For automatic startup, use the [systemd or container guide](./service-management.md). +Keep `.env` and `rooms.sqlite` across updates. Back up room data while the server is +stopped, replace the executable with the new release, then restart and check +health and room access. Read release notes before an upgrade that changes data formats. + +[All settings and ports](../reference/configuration.md) · +[Maintainer release tooling](../deployment.md) diff --git a/docs/operations/self-hosting.zh-CN.md b/docs/operations/self-hosting.zh-CN.md new file mode 100644 index 0000000..98a19ed --- /dev/null +++ b/docs/operations/self-hosting.zh-CN.md @@ -0,0 +1,87 @@ +# 部署 Piik Server + +[English](./self-hosting.md) · 简体中文 · [文档导航](../README.md) + +Piik Server 将网页、房间管理和可选的媒体转发打包在**一个服务端程序**中。 +解压后即可运行,房间数据保存在 SQLite 中。 + +## 先在本机试用 + +以下服务端操作在 Linux x64 环境中执行。 + +从 [GitHub Releases](https://github.com/TNTcraftHIM/Piik/releases) 或 +[Gitee 镜像](https://gitee.com/TNTcraftHIM/Piik/releases)下载 **Linux x64 服务端**程序包, +解压后,在该目录运行: + +```sh +./piik-server +``` + +打开 `http://localhost:8787` 即可试用。需要让朋友通过互联网访问时, +继续完成下方配置。如果只想在自己的电脑上临时开房间,可直接使用 [Piik App](../guide/getting-started.zh-CN.md#用-piik-app-分享)。 + +## 对外提供服务 + +准备一台 Linux x64 服务器,以及一个指向服务器公网 IP 的域名。 +下文以 `share.example.com` 为例,请替换成自己的域名。 + +### 1. 配置并启动 Piik + +在程序旁新建 `.env` 文件: + +```dotenv +PIIK_ENV=production +LISTEN_HOST=127.0.0.1 +PUBLIC_BASE_URL=https://share.example.com +STUN_URLS=stun:share.example.com:3478 +MAX_VIEWERS_PER_ROOM=20 +SITE_ACCESS_PASSWORD= +``` + +使用普通用户,在该目录执行: + +```sh +./piik-server --check-config +./piik-server +``` + +程序会自动读取 `.env`,并将房间数据保存在工作目录的 `rooms.sqlite` 中。 +`SITE_ACCESS_PASSWORD` 留空时,进入站点无须口令; +如需设置口令,可填写 8–128 个可见 ASCII 字符。房间邀请与加入权限仍由房主管理。 + +### 2. 配置 HTTPS + +如果已有 HTTPS 反向代理,将请求转发到 `127.0.0.1:8787`,并启用 WebSocket 支持。 +如果还没有,可以[安装 Caddy](https://caddyserver.com/docs/install),在 Caddyfile 中加入: + +```caddyfile +share.example.com { + reverse_proxy 127.0.0.1:8787 +} +``` + +重新加载 Caddy。域名解析正确且 TCP 80/443 可访问时,它会自动申请和续期证书。 +具体操作见 [Caddy 的 HTTPS 代理说明](https://caddyserver.com/docs/quick-starts/reverse-proxy#https)。 + +### 3. 放行端口并检查 + +在服务器防火墙和云平台安全组中放行 **TCP 80/443**(HTTPS)和 **UDP 3478**(STUN)。 +TCP 8787 仅供本机反向代理访问。STUN 域名需要直接解析到服务器,不能只经过 CDN 的 HTTP 代理。 + +打开 `https://share.example.com/healthz`,应返回 `{"status":"ok"}`。 +随后打开站点,分享一个画面,并用另一台设备加入验证。 +上述基础配置使用 P2P 传输,参与者之间需要可用的 UDP 通路。 + +## 可选:启用媒体兜底 + +在 `.env` 中添加 `SFU_UDP_PORT=7882`,放行 UDP 7882,再重启 Piik,即可启用自动 SFU 兜底。 +如果服务器处于 NAT 后方,还需将 `SFU_PUBLIC_IP` 设置为外部可达的公网 IPv4 地址。 +这项功能由同一个服务端程序提供。 + +## 长期运行与更新 + +需要开机启动时,参阅 [systemd 与容器配置](./service-management.md)。 +更新时保留 `.env` 和 `rooms.sqlite`:停止服务、备份房间数据、替换新版程序,再启动并检查健康状态和房间访问。 +涉及数据格式变化的版本,请先阅读发布说明。 + +[全部配置与端口](../reference/configuration.md) · [维护者发版工具](../deployment.md) diff --git a/docs/operations/service-management.md b/docs/operations/service-management.md new file mode 100644 index 0000000..12fafda --- /dev/null +++ b/docs/operations/service-management.md @@ -0,0 +1,104 @@ +# Service Management + +For first installation, use the [short self-hosting guide](./self-hosting.md). +This reference covers a persistent Linux service and the optional container recipe. + +## systemd + +The [service template](../../deploy/systemd/piik.service.example) runs as the +`piik` user, reads `/etc/piik/piik.env`, and starts +`/opt/piik/current/piik-server`. Create the service account and place the extracted +release at that path, or point `current` at its release directory. Copy your +configuration to `/etc/piik/piik.env` and set +`ROOM_DATABASE_PATH=/var/lib/piik/rooms.sqlite`; the unit creates this state directory. +Keep the environment file readable only by its operator. + +Install the template as `/etc/systemd/system/piik.service`, then run: + +```sh +sudo systemctl daemon-reload +sudo systemctl enable --now piik +sudo systemctl status piik +``` + +Keep HTTP bound to loopback when the HTTPS proxy runs on the same host. +STUN and optional SFU use their own public UDP listeners. An existing nginx +installation can use the [proxy example](../../deploy/nginx/piik.conf.example) +after adapting the domain and certificate paths. + +## Container + +The [runtime-only Dockerfile](../../deploy/container/Dockerfile) consumes the +existing linux/amd64 Server release; it does not rebuild Go or Browser assets. +Its pinned [Distroless static non-root base](https://github.com/GoogleContainerTools/distroless) +includes CA certificates for HTTPS release checks and has no shell or package +manager. This is an optional recipe, not a published image or automatic installer. + +Verify the release archive against its descriptor and manifest using the +[release procedure](../deployment.md), then extract its four files into a new +build-context directory: `piik-server`, `LICENSE`, `THIRD-PARTY-NOTICES.txt` +and `REVISION`. Use that directory, not the repository or a secrets directory: + +```sh +docker build --platform linux/amd64 \ + -f /path/to/Piik/deploy/container/Dockerfile \ + -t piik: /path/to/extracted-runtime +docker run --rm --env-file /path/to/piik.env \ + piik: --check-config +``` + +Use the existing [production configuration](../reference/configuration.md). +Keep `LISTEN_HOST=0.0.0.0` inside the container and retain the host's existing +HTTPS/WebSocket proxy. With Docker port mapping, set `SFU_PUBLIC_IP` to the +reachable server IPv4 address when SFU is enabled; container-private candidates +are not Internet-reachable. Advertised STUN names must resolve to the server's +public address, and UDP must reach the container directly. + +The following example publishes the standard listeners and keeps HTTP private +to a proxy running on the same host. Omit UDP publish options for disabled SFU +or auxiliary STUN listeners; use the matching port if configuration changes it. + +```sh +docker run -d --name piik --restart unless-stopped --stop-timeout 20 \ + --read-only --cap-drop ALL --security-opt no-new-privileges \ + --env-file /path/to/piik.env \ + --mount type=volume,source=piik-data,target=/home/nonroot \ + -e ROOM_DATABASE_PATH=/home/nonroot/rooms.sqlite \ + -p 127.0.0.1:8787:8787/tcp \ + -p 3478:3478/udp -p 3479:3479/udp -p 3480:3480/udp -p 7882:7882/udp \ + piik: +``` + +The image's `/home/nonroot` is owned by UID/GID `65532:65532` with mode `0700`. +A new named volume inherits that directory; an existing volume or bind mount +must already be writable by that identity. Keep this volume when replacing the +container. Set `ROOM_DATABASE_PATH=:memory:` for memory-only room authority. Diagnostics +remain opt-in and use `/home/nonroot/logs`; [export and retention](../reference/configuration.md#diagnostics) +remain the operator's responsibility. Do not mount application files writable. +With diagnostics enabled, `docker kill --signal=USR1 piik` requests a local +export without stopping the container; the ZIP remains in the mounted log directory. + +Check `/healthz` from the host and public proxy, then verify configured STUN/SFU +UDP and room/media behavior using [operational verification](#operational-verification). +There is no shell-based healthcheck in the image. Retain the previous image, +environment and volume backup before replacement. Stop the old container before +reusing its listeners or SQLite volume, and recreate with the previous image and +environment if verification fails. The bare-metal `release-app.sh` wrapper does +not manage containers. This recipe changes no proxy or firewall itself. + +## Operational Verification + +- `/healthz` returns `{"status":"ok"}` locally and through the public origin. +- Piik and the HTTPS proxy are active with expected restart counts and bounded + logs/resources; Piik owns the configured STUN/SFU UDP listeners. +- The official Trickle ICE sample obtains an ordinary UDP `srflx` candidate from + the configured STUN service and no relay candidate. +- A direct room presents media on two networks when Peer ICE succeeds. +- A constrained route creates one managed Host publication and only admitted + subscriptions over embedded SFU UDP; Peer descendants remain ordinary P2P. +- Blocking all UDP reaches a clear bounded failure. +- Restarting Piik in stable mode retains room authority while Browser + sessions and media reconnect from fresh process state. + +Representative network, mobile, audio, 20-Viewer, and long-running evidence is +tracked in [verification status](../verification-status.md), not in this runbook. diff --git a/docs/operations/website.md b/docs/operations/website.md new file mode 100644 index 0000000..1f32a22 --- /dev/null +++ b/docs/operations/website.md @@ -0,0 +1,93 @@ +# Public website + +The website source is `site/`. It explains Piik; it does not run rooms, signaling +or media. The [public introduction guide](../design/public-introduction.md) owns +its content and design boundary. + +## Public Destinations + +| Entry | Destination | +| --- | --- | +| Website | [piik.tv](https://piik.tv) | +| P2P-only demo on the separate US server | [demo.piik.tv](https://demo.piik.tv) | +| App downloads and release notes | [GitHub Releases](https://github.com/TNTcraftHIM/Piik/releases) | +| Download mirror for domestic access | [Gitee Releases](https://gitee.com/TNTcraftHIM/Piik/releases) | +| Documentation index | [Repository documentation](../README.md) | + +The introduction is prepared with these links. DNS, deployment and release +publication must be verified when publishing. + +GitHub Pages can serve the complete website: it hosts +[static HTML, CSS and JavaScript](https://docs.github.com/en/pages/getting-started-with-github-pages/what-is-github-pages). +The demo also needs the Go service for room authority, WebSocket signaling and +UDP STUN, so it runs on the separate US server rather than Pages. + +## Public Demo Configuration + +The chosen demo allows entry without a site password and provides P2P sharing +without SFU fallback. Use the [self-hosting runbook](./self-hosting.md) with: + +```dotenv +PIIK_ENV=production +LISTEN_HOST=127.0.0.1 +PORT=8787 +PUBLIC_BASE_URL=https://demo.piik.tv +ALLOWED_ORIGINS=https://demo.piik.tv +SITE_ACCESS_PASSWORD= +ROOM_DATABASE_PATH=/var/lib/piik/rooms.sqlite +MAX_VIEWERS_PER_ROOM=20 +STUN_URLS=stun:demo.piik.tv:3478 +NAT_PREDICTION_ENABLED=true +SFU_UDP_PORT= +``` + +Point `demo.piik.tv` directly at the US server so UDP reaches its STUN listeners; +allow UDP 3478/3479/3480 and terminate HTTPS/WSS at Caddy. The demo uses the +standard [systemd service](./service-management.md#systemd), with its release +under `/opt/piik/current` and configuration at `/etc/piik/piik.env`. The service +unit provides the SQLite state directory. Room ownership, invitations and private-room +admission still apply. The [configuration reference](../reference/configuration.md) +owns each setting and its bounds. + +For updates, verify the release descriptor and manifest before placing a new +release under `/opt/piik/releases/`. Stop Piik, back up its SQLite state, switch +`current` to the verified release, and restart. Check local/public health, +WebSocket room access and STUN. Keep the previous release and state backup until +acceptance; restore both with Piik stopped if rollback is needed. The nginx-specific +maintainer wrapper does not manage this Caddy installation. + +## Preview + +With the repository's development dependencies installed: + +```sh +npm run build:website +npm run preview:website +``` + +Open `http://127.0.0.1:18890`. Rebuild after editing source. The build bundles the +film's real product components into an isolated demonstration frame; publishing +`build/site/` needs no Node runtime or backend. Vite preview supplies HTTP byte +ranges for seeking the soundtrack. Keep asset links relative so both a custom +domain and GitHub's `/Piik/` project path work. + +## Publish when approved + +1. Verify the public copy and destinations above, publish the release artifacts + and open the source repository as part of the approved public launch. +2. In repository **Settings > Pages**, choose **GitHub Actions** as the source. + Use the dedicated Pages workflow, not publication of the repository root or + all of `docs/`, which includes developer research and operational reference. +3. Set the approved `piik.tv` custom domain in Pages settings and configure its + DNS using GitHub's [current domain instructions](https://docs.github.com/en/pages/configuring-a-custom-domain-for-your-github-pages-site/managing-a-custom-domain-for-your-github-pages-site). + Actions publishing does not need a `CNAME` file in the source tree. +4. Dispatch **Website** from `main` after the reviewed site is merged. The + workflow builds and uploads only `build/site/`; no App/Server packaging is run. Verify the + domain, HTTPS, both languages, relative asset paths and primary links. + +There is no automatic deployment on branch pushes. The workflow is prepared, +but writing it does not enable Pages or change DNS. The private production +site and the US P2P-only demo remain separate deployments. + +For rollback, dispatch the previously accepted site revision from a reviewed +revert on `main`; runtime App/Server binaries and databases are unaffected. diff --git a/docs/product/media-quality.md b/docs/product/media-quality.md index f3b95e9..51ad88b 100644 --- a/docs/product/media-quality.md +++ b/docs/product/media-quality.md @@ -4,7 +4,8 @@ This file owns the current Browser and App media contract. Detailed measurements platform limits live in [realtime quality research](../research/realtime-quality-adaptation.md) and [screen-audio research](../research/browser-screen-audio-quality.md). [ADR-0007](../adr/0007-path-isolated-representation-quality.md) owns Browser -source intent and codec selection; [ADR-0013](../adr/0013-embedded-node-local-media.md) +source intent and codec selection, [ADR-0008](../adr/0008-window-scoped-audio-capture.md) +owns screen-audio scope, and [ADR-0013](../adr/0013-embedded-node-local-media.md) owns the current candidate's shared Native/SFU output model. This file describes that source contract. [Status](../status.md) separates it from the last known production behavior and remaining acceptance. diff --git a/docs/product/presentation-lifecycle.md b/docs/product/presentation-lifecycle.md index 14ae661..3894c5b 100644 --- a/docs/product/presentation-lifecycle.md +++ b/docs/product/presentation-lifecycle.md @@ -11,6 +11,13 @@ available, starting a share and joining a room are equally discoverable actions; joining expands the four-digit room form in place, while `/join` remains a direct entry route. Users never choose a media topology. +Host entry requires both site access and a validated capability description. +Initial entry, retry and password recovery load the same prerequisites; failed +authentication cannot leave a later successful entry using absent capability +state. A capability-load failure remains retryable and does not imply that a +service is available or disabled. Viewer/Join entry does not need this Host-only +description. + The Host workspace centers the capture preview, room code, invitation and common share controls. The Viewer workspace centers one persistent 16:9 video stage, room identity, current state, roster, and one manual media-reconnect action. @@ -25,6 +32,13 @@ Browser picker and any exact App-owned native windows. It never chooses a window automatically. A reproduced Browser-window, capture, or background failure is required before introducing an embedded Web runtime. +Server and App share one control layout and vocabulary. Deployment capabilities +may fix a control on or off, but do not remove its place in the interface; +disabled controls explain their reason in text and pure-visual modes. Real +workflow differences remain explicit: site authorization, opt-in diagnostics +and the capture/audio capabilities of the connected device are not alternate +versions of the product UI. + A remembered App activation permits discovery when choosing a source or receiving media; it does not make App a prerequisite for sharing. Idle Host pages hold no App control session. Ordinary Web entry opens the Browser picker @@ -38,47 +52,69 @@ retire another operation's media. Optional native ingress uses an already available connection without another scan. If it is not ready at Browser startup, that share keeps Browser senders. +A reachable Piik App with an incompatible control protocol is distinct from an +absent App. Discovery continues looking for a compatible process before reporting +the mismatch; the source selector retains Browser operation and offers update/ +refresh guidance. Discovery metadata may grow, while known identity and capability +types remain validated. Missing optional capabilities are unavailable; control +commands and responses retain their strict protocol contract. + ## Visual Language -The interface presents one small shared living room rather than an operations -dashboard. The television is the media stage, the couch and pawns make presence -spatial, the shelf separates watching from controls, and the room display keeps -the four-digit code prominent. This metaphor must clarify the same product -state on Host and Viewer pages; decoration never creates a second state model. - Chinese, English, and pure-visual modes are three expressions of the same typed -state and command catalog. A fresh Browser starts in visual mode and persists -an explicit later choice. Text modes use concise copy. Visual mode uses one -consistent stroke language for single concepts, small panel comics for causes -and sequences, and literal scene objects for people, rooms, and media. Glyphs do -not form sentences. Universal digits, transport symbols, URLs, and measured -values remain literal when users need the data. +state and command catalog. On first use, the App and Server UI follow the +Browser's primary system language: Chinese uses Chinese text, and other +languages use English. Pure-visual mode remains an explicit choice. An explicit +later choice is persisted and takes precedence over that default. Text modes +use concise copy. -Control hints and state comics use the same television for shared or watched -media. Browser chrome identifies the application UI; a window title bar or a -display stand identifies the capture target. A server in a media path means -actual SFU relay, not merely opening a configured Site. - -Motion explains entry, transition, progress, and control feedback. Operational -status stays still; ambient motion is limited to the App brand mark's -occasional idle wink. -All meaning remains available with reduced motion. Functional -controls remain native buttons or inputs with localized accessible names, and -hover, keyboard focus, and touch receive equivalent guidance. - -The visual hierarchy, interaction ownership, and information order remain the -same across themes and viewport sizes. Responsive layout may reflow or scroll a -bounded visualization, but it must not hide a primary action, truncate an -essential value, overlap controls, or change product semantics. +The [visual language](../design/visual-language.md) is the single owner of +illustrative roles and objects, semantic colour, panel grammar, motion, +accessible input and responsive hierarchy. ## Playback Ownership -One native `