feat: launch Piik with a unified interface and public website
Piik now has one public presentation and a coordinated App/Server release path. This phase unifies the room UI, rewrites the bilingual README and guides, adds a static website with an inline product film, and prepares matching GitHub/Gitee packages and update links. The film uses the current homepage and shared product controls on one seekable timeline. Runtime fixes preserve presentation when the App opens a room, normalize Site origins, and keep capture and media-recovery status in the television indicator. New visits use Chinese for a Chinese system language and English otherwise; explicit language and visual-mode choices remain available. An optional site passphrase controls site entry, while room authorization remains enforced. GitHub is the release authority. Gitee mirrors verified packages and provides update metadata when GitHub is unavailable. Reviewed PR release notes become the GitHub Release description. The separate public demo uses P2P only, with HTTPS and STUN verified. Validation: 756 Web tests, TypeScript and production build; Go core tests, formatting, vet and available target builds; bounded Windows capture and App/Site interoperability; browser film checks, audible playback and narrow layouts; anonymous Gitee Windows-package download, checksum and launcher/update-link checks. Public demo acceptance covers authenticated WebSocket signaling, synthetic P2P video and room cleanup on one network. macOS/Linux physical capture and the wider device/network/endurance matrix remain explicitly deferred. Publication builds every package from the merged source SHA. The repository is public, main protection is active, and Pages is configured for piik.tv. CI/publisher and Pages deployment records own publication results. ## Release notes ### 中文 Piik 是免费开源的屏幕共享工具。开个房间,发出邀请,最多 20 位朋友就能用浏览器加入观看。 - 使用浏览器或 Piik App 分享游戏、窗口和整个屏幕。 - 优先建立直连;自托管站点可开启媒体转发作为备用线路。 - Piik App 支持本地房间、临时公网邀请和已有站点。 - Piik Server 内置网页界面、房间管理和连接服务,方便自行部署。 - 提供中英文界面与使用指南,以及 GitHub、Gitee 两条下载线路。 [官网](https://piik.tv) · [在线体验](https://demo.piik.tv) · [使用指南](https://github.com/TNTcraftHIM/Piik/blob/main/docs/guide/getting-started.zh-CN.md) · [自行部署](https://github.com/TNTcraftHIM/Piik/blob/main/docs/operations/self-hosting.zh-CN.md) · [Gitee 下载](https://gitee.com/TNTcraftHIM/Piik/releases) Windows App 和桌面浏览器是本次主要验证的平台。macOS 和 Linux 尚未经过实机测试,欢迎有设备的朋友测试并反馈。演示站和 App 公网邀请使用 P2P,受限网络下可能无法连接;自托管站点可开启媒体转发。 ### English Piik is free, open-source screen sharing for you and up to 20 viewers. Start a room, send an invitation, and your friends can join in a browser. - Share games, windows or a whole screen from your browser or Piik App. - Direct connections come first, with optional media forwarding on self-hosted sites. - Piik App supports local rooms, temporary public invitations and existing sites. - Piik Server includes the web interface, rooms and connection services for self-hosting. - English and Chinese interfaces and guides are available, with GitHub and Gitee downloads. [Website](https://piik.tv) · [Online demo](https://demo.piik.tv) · [User guide](https://github.com/TNTcraftHIM/Piik/blob/main/docs/guide/getting-started.md) · [Self-hosting](https://github.com/TNTcraftHIM/Piik/blob/main/docs/operations/self-hosting.md) · [Gitee downloads](https://gitee.com/TNTcraftHIM/Piik/releases) Windows App and desktop browsers are the primary validated paths. macOS and Linux have not yet been tested on physical devices; testing and feedback are welcome. The demo and App public invitations rely on P2P and may not connect on restrictive networks. Self-hosted sites can enable media forwarding.
This commit is contained in:
261 files changed
+15305
-5328
No files matched your search
+13
-8
@@ -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):
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
*.ico binary
|
||||
*.jpg binary
|
||||
*.jpeg binary
|
||||
*.mp3 binary
|
||||
*.png binary
|
||||
*.webp binary
|
||||
*.woff binary
|
||||
|
||||
@@ -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 / 可选证据
|
||||
|
||||
<!-- Screenshots or a local Debug export may help. Review artifacts before attaching; remove credentials and private content. 可附截图或本地 Debug 导出;分享前移除凭据与隐私内容。 -->
|
||||
@@ -0,0 +1,12 @@
|
||||
---
|
||||
name: Feature request / 功能建议
|
||||
about: Describe a use case and desired outcome / 说明使用场景与期望结果
|
||||
---
|
||||
|
||||
## Problem or use case / 问题或场景
|
||||
|
||||
<!-- Who needs this, and what cannot they do today? 谁在什么场景遇到了什么限制? -->
|
||||
|
||||
## Desired outcome / 期望结果
|
||||
|
||||
<!-- Describe the behavior that would solve the problem; alternatives are optional. 说明怎样的行为能解决问题,可补充替代方案。 -->
|
||||
@@ -1,20 +1,17 @@
|
||||
## Summary
|
||||
<!-- Title: type(scope): outcome. One accepted phase per squash PR. 标题遵循提交命名,一次 PR 完成一个阶段。 -->
|
||||
|
||||
Describe the problem and the outcome of this change.
|
||||
## Problem and outcome / 问题与结果
|
||||
|
||||
## Verification
|
||||
<!-- Describe the trigger and resulting behavior. Link a related issue or decision when useful. 说明问题、结果及相关依据。 -->
|
||||
|
||||
List automated tests, manual checks, environments, and meaningful measurements.
|
||||
## Release notes
|
||||
|
||||
## Research And Decisions
|
||||
<!-- Public copy, automatically published in GitHub Releases. Keep this exact heading; use ### for subsections. Describe user benefits, fixes and any required upgrade action in concise Chinese and English. Omit internal logs, audit prompts and unverified claims. 更新说明将直接公开:写清用户可感知的变化与升级事项,提供简洁的中英文文案。首次发布介绍当前产品,不罗列内部开发历史。 -->
|
||||
|
||||
Link the relevant product owner, research note, issue, and ADR, or explain why none is needed.
|
||||
## Verification / 验证
|
||||
|
||||
## Checklist
|
||||
<!-- List exact local checks and results, relevant environments, and material untested cases. 列出检查命令、结果与未验证项。 -->
|
||||
|
||||
- [ ] 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 / 影响
|
||||
|
||||
<!-- Note relevant compatibility, release interruption/recovery, and updated owner docs. Explain any breaking change marked by ! in the title; write “None” if not applicable. 说明兼容、发布及文档影响,解释标题中 ! 标记的破坏性变更;不适用填“无”。 -->
|
||||
@@ -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 }}
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
+48
-12
@@ -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 `<type>/<short-kebab-case-description>`, 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
|
||||
|
||||
@@ -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 贡献者
|
||||
|
||||
特此免费授予任何获得本软件及相关文档文件(以下简称“软件”)副本的人,
|
||||
不受限制地处置本软件的权利,包括但不限于使用、复制、修改、合并、出版、
|
||||
分发、再许可和/或销售本软件副本,并允许获得软件的人这样做,
|
||||
但须遵守以下条件:
|
||||
|
||||
上述版权声明和本许可声明应包含在本软件的所有副本或实质部分中。
|
||||
|
||||
本软件按“原样”提供,不作任何明示或默示保证,包括但不限于适销性、
|
||||
特定用途适用性和不侵权的保证。在任何情况下,无论依据合同、侵权或其他
|
||||
法律理论,作者或版权持有人均不对因软件、软件的使用或其他处置而产生、
|
||||
源自或与之有关的任何索赔、损害或其他责任负责。
|
||||
@@ -1,83 +1,109 @@
|
||||
<img src="./public/favicon.svg" width="64" height="64" alt="Piik TV logo">
|
||||
<p align="center"><img src="./public/favicon.svg" width="64" height="64" alt="Piik mascot"></p>
|
||||
<h1 align="center">Piik</h1>
|
||||
<p align="center"><strong>Share the good stuff.</strong><br>Private screen sharing for you and up to 20 friends.</p>
|
||||
<p align="center">
|
||||
<a href="https://piik.tv">Website</a> ·
|
||||
<a href="https://github.com/TNTcraftHIM/Piik/releases">Download</a> ·
|
||||
<a href="https://gitee.com/TNTcraftHIM/Piik/releases">Gitee mirror</a> ·
|
||||
<a href="https://demo.piik.tv">Try the demo</a> ·
|
||||
<a href="./docs/README.md">Documentation</a>
|
||||
</p>
|
||||
<p align="center">
|
||||
<a href="./LICENSE"><img src="https://img.shields.io/badge/license-MIT-36564f?style=flat-square" alt="License: MIT"></a>
|
||||
<img src="https://img.shields.io/badge/viewers-up_to_20-36564f?style=flat-square" alt="Up to 20 viewers">
|
||||
<img src="https://img.shields.io/badge/media-P2P_first-36564f?style=flat-square" alt="P2P-first media">
|
||||
</p>
|
||||
<p align="center">English · <a href="./README.zh-CN.md">简体中文</a></p>
|
||||
|
||||
# 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)
|
||||
<details open>
|
||||
<summary>A little room · Show / hide animation</summary>
|
||||
|
||||
Share your screen. Bring your friends.
|
||||
<p align="center"><img src="./site/assets/living-room.svg" width="720" height="472" alt="A host with a small gold crown shares an RPG, a drawing, photos and an animated film while friends watch from the sofa."></p>
|
||||
|
||||
Piik is private screen sharing for one Host and up to 20 invited friends.
|
||||
Friends watch in their browsers, with no installation.
|
||||
</details>
|
||||
|
||||
```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.
|
||||
|
||||
<p align="center">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="./docs/assets/room-en-dark.png">
|
||||
<img src="./docs/assets/room-en-light.png" width="860" alt="Piik room: a shared game, playback controls and friends on a sofa. The host wears a small gold crown.">
|
||||
</picture><br>
|
||||
<sub>Interface preview · Sample room with a generated game scene</sub>
|
||||
</p>
|
||||
|
||||
## 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. |
|
||||
|
||||
<p align="center"><img src="./docs/assets/quickstart.svg" width="640" alt="Choose a screen, send an invitation, watch together."></p>
|
||||
|
||||
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).
|
||||
+91
-66
@@ -1,79 +1,104 @@
|
||||
<img src="./public/favicon.svg" width="64" height="64" alt="Piik 小电视标志">
|
||||
<p align="center"><img src="./public/favicon.svg" width="64" height="64" alt="Piik 吉祥物"></p>
|
||||
<h1 align="center">Piik</h1>
|
||||
<p align="center"><strong>来,看点<br>好康的。</strong><br>私密屏幕共享,邀请最多 20 位朋友观看。</p>
|
||||
<p align="center">
|
||||
<a href="https://piik.tv/?lang=zh-CN">官网</a> ·
|
||||
<a href="https://github.com/TNTcraftHIM/Piik/releases">下载</a> ·
|
||||
<a href="https://gitee.com/TNTcraftHIM/Piik/releases">Gitee 镜像</a> ·
|
||||
<a href="https://demo.piik.tv">在线演示</a> ·
|
||||
<a href="./docs/README.md">文档</a>
|
||||
</p>
|
||||
<p align="center">
|
||||
<a href="./LICENSE"><img src="https://img.shields.io/badge/license-MIT-36564f?style=flat-square" alt="MIT 许可"></a>
|
||||
<img src="https://img.shields.io/badge/viewers-up_to_20-36564f?style=flat-square" alt="最多 20 位观众">
|
||||
<img src="https://img.shields.io/badge/media-P2P_first-36564f?style=flat-square" alt="优先使用 P2P 连接">
|
||||
</p>
|
||||
<p align="center"><a href="./README.md">English</a> · 简体中文</p>
|
||||
|
||||
# Piik
|
||||
Piik 是一个免费开源的屏幕共享工具,适合游戏围观、一起看电影、展示绘画或分享照片。
|
||||
选择画面,把邀请链接发给朋友,对方用浏览器即可观看。
|
||||
|
||||
[English](./README.md) | 简体中文
|
||||
<details open>
|
||||
<summary>客厅小剧场 · 展开 / 收起动画</summary>
|
||||
|
||||
分享你的屏幕,叫上朋友一起看。
|
||||
<p align="center"><img src="./site/assets/living-room.svg" width="720" height="472" alt="戴着小金冠的房主分享 RPG 游戏、绘画、照片和动画电影,朋友们坐在沙发上观看。"></p>
|
||||
|
||||
Piik 是一个私密屏幕分享工具:一位房主,最多 20 位受邀好友。
|
||||
朋友用浏览器就能观看,无须安装。
|
||||
</details>
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
Browser[浏览器] --> Host[开房分享]
|
||||
App[Piik App] --> Host
|
||||
Host -->|邀请链接| Friends[好友用浏览器观看]
|
||||
[功能](#功能) · [开始使用](#开始使用) · [自行部署](#自行部署) · [参与贡献](#参与贡献)
|
||||
|
||||
## 功能
|
||||
|
||||
- **观看无须安装。** 朋友通过邀请链接,用电脑或手机浏览器加入。
|
||||
- **用客户端或已有 Piik 站点分享。** 支持屏幕、窗口和浏览器标签页;可选来源与音频能力取决于平台。
|
||||
- **优先直接连接。** 画面优先在设备之间传输(P2P),自建站点可启用媒体转发(SFU)自动兜底。
|
||||
- **房间由你管理。** 支持邀请链接、房间号和可选的房间密码,一位房主、最多 20 位观众。
|
||||
- **灵活的观看方式。** 支持深浅主题、播放控制、画中画和连接拓扑查看。
|
||||
- **一个程序就能建站。** 网页、房间管理和可选的媒体转发打包在同一服务端程序中。
|
||||
|
||||
<p align="center">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="./docs/assets/room-zh-dark.png">
|
||||
<img src="./docs/assets/room-zh-light.png" width="860" alt="Piik 房间:共享的游戏画面、播放控件和坐在沙发上的朋友,房主戴着小金冠。">
|
||||
</picture><br>
|
||||
<sub>界面预览 · 示例房间与生成的游戏画面</sub>
|
||||
</p>
|
||||
|
||||
## 开始使用
|
||||
|
||||
**观看分享:** 打开朋友发来的邀请链接。画面没有自动播放时,点击**播放**即可。
|
||||
|
||||
**分享画面:**
|
||||
|
||||
| 从这里开始 | 需要做什么 |
|
||||
| --- | --- |
|
||||
| [体验在线 Demo](https://demo.piik.tv) | 在电脑浏览器打开,开始分享。 |
|
||||
| [下载 Piik App](https://piik.tv/?lang=zh-CN#download) | 解压并启动客户端,选择**公网邀请**,点击**进入 Piik**,创建临时房间。 |
|
||||
| 使用已有 Piik 站点 | 打开朋友或管理员提供的地址。[自行部署](./docs/operations/self-hosting.zh-CN.md)属于进阶操作。 |
|
||||
|
||||
<p align="center"><img src="./docs/assets/quickstart.svg" width="640" alt="选择画面,发送邀请,一起观看。"></p>
|
||||
|
||||
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)。
|
||||
+28
-24
@@ -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
|
||||
|
||||
+33
-17
@@ -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
|
||||
|
||||
+188
-144
@@ -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 <REVISION> [--api-url <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)^<!-- piik-source: ([0-9a-f]{40}) -->\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/<that
|
||||
// revision> 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
|
||||
}
|
||||
+225
-220
@@ -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())
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
+65
-52
@@ -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.
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
@@ -0,0 +1,63 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="900" height="192" viewBox="0 0 900 192" role="img" aria-labelledby="title desc">
|
||||
<title id="title">Pick a screen, send an invitation, watch together</title>
|
||||
<desc id="desc">Three illustrated steps: choose a window, send its room invitation, and join friends on the sofa. Each loop pauses to let the scene settle.</desc>
|
||||
<style>
|
||||
:root { --drawing:#22303e; --panel:#fafaf3; --line:#c6d6cc; --label:#256b47; --art-mint:#c4e3ce; --art-paper:#fffdf4; --art-blue:#a2c8e8; --art-yellow:#f5d37a; --art-orange:#ee8955; --art-pink:#e9b1bb; --couch:#edbb72; --couch-dark:#d99a56; }
|
||||
@media (prefers-color-scheme:dark) { :root { --panel:#1b2d34; --line:#3b5155; --label:#9bdbb2; } }
|
||||
text { fill:var(--label); font:600 13px system-ui,sans-serif; letter-spacing:2px; }
|
||||
@media (prefers-reduced-motion:no-preference) {
|
||||
g[class] { transform-box:fill-box; transform-origin:center; }
|
||||
.screen-cursor { animation:choose-screen 6.4s ease-in-out infinite both; }
|
||||
.chosen-window { animation:window-response 6.4s ease-in-out infinite both; }
|
||||
.invite-plane { animation:send-invite 6.4s ease-in-out .2s infinite both; }
|
||||
.invite-note { animation:note-response 6.4s ease-in-out .2s infinite both; }
|
||||
.sofa-friend { animation:take-a-seat 6.4s ease-out .4s infinite both; }
|
||||
.sofa-friend + .sofa-friend { animation-delay:.52s; }
|
||||
}
|
||||
@keyframes choose-screen {
|
||||
0%, 8%, 48%, 100% { transform: none; }
|
||||
22%, 34% { transform: translate(-62px, -19px); }
|
||||
28% { transform: translate(-62px, -19px) scale(.85); }
|
||||
}
|
||||
@keyframes window-response { 0%, 22%, 46%, 100% { transform: none; } 30% { transform: scale(1.07); } 38% { transform: scale(.99); } }
|
||||
@keyframes send-invite {
|
||||
0%, 6%, 48%, 100% { transform: none; opacity: 1; }
|
||||
12% { transform: translate(-4px, 3px) rotate(7deg); opacity: 1; }
|
||||
32% { transform: translate(42px, -36px) rotate(-12deg); opacity: 0; }
|
||||
33% { transform: translate(-8px, 8px); opacity: 0; }
|
||||
}
|
||||
@keyframes note-response { 0%, 10%, 42%, 100% { transform: none; } 20% { transform: translateY(1px) rotate(-4deg); } 32% { transform: rotate(1deg); } }
|
||||
@keyframes take-a-seat { 0% { transform: translateY(-8px); } 22% { transform: translateY(2px); } 32%, 100% { transform: none; } }
|
||||
</style>
|
||||
<g transform="translate(0 2)">
|
||||
<rect x="1" y="1" width="278" height="186" rx="18" fill="var(--panel)" stroke="var(--line)" stroke-width="1.5"/>
|
||||
<text x="20" y="28">01</text>
|
||||
<g transform="translate(15 40)"><g fill="none" stroke="var(--drawing)" stroke-width="3" stroke-linejoin="round">
|
||||
<rect x="47" y="17" width="141" height="90" rx="12" fill="var(--art-mint)"/><path d="M105 108v14m-20 0h65"/>
|
||||
<g class="chosen-window"><rect x="64" y="31" width="64" height="57" rx="7" fill="var(--art-paper)"/><path d="m79 76 12-15 9 7 12-19"/></g>
|
||||
<rect x="137" y="31" width="34" height="23" rx="5" fill="var(--art-blue)"/><rect x="137" y="64" width="34" height="24" rx="5" fill="var(--art-yellow)"/>
|
||||
<g class="screen-cursor"><path d="m172 82 3 33 9-8 8 13 9-5-8-13 11-2Z" fill="var(--art-orange)"/></g>
|
||||
</g><path d="m33 35-8-4m12-8-4-8m169 22 9-5" stroke="var(--drawing)" stroke-width="3" stroke-linecap="round"/></g>
|
||||
</g>
|
||||
<g transform="translate(310 2)">
|
||||
<rect x="1" y="1" width="278" height="186" rx="18" fill="var(--panel)" stroke="var(--line)" stroke-width="1.5"/>
|
||||
<text x="20" y="28">02</text>
|
||||
<g transform="translate(15 40)"><g stroke="var(--drawing)" stroke-width="3" stroke-linecap="round" stroke-linejoin="round">
|
||||
<g class="invite-note"><rect x="44" y="31" width="143" height="73" rx="16" fill="var(--art-paper)" transform="rotate(-5 115 67)"/><path d="m80 77-6-6a13 13 0 0 1 0-18l10-10a13 13 0 0 1 18 0l6 6m-10 8 6 6a13 13 0 0 1 0 18L94 91a13 13 0 0 1-18 0l-6-6m11-10 16-16" fill="none"/><path d="M125 58h38m-38 15h24"/></g>
|
||||
<circle cx="191" cy="96" r="25" fill="var(--art-blue)"/><g class="invite-plane"><path d="m174 94 34-12-12 33-7-14Z" fill="var(--art-paper)"/><path d="m189 101 8-9"/></g>
|
||||
<path class="invite-spark" d="M190 24v10m-5-5h10M33 93v8m-4-4h8"/>
|
||||
</g></g>
|
||||
</g>
|
||||
<g transform="translate(620 2)">
|
||||
<rect x="1" y="1" width="278" height="186" rx="18" fill="var(--panel)" stroke="var(--line)" stroke-width="1.5"/>
|
||||
<text x="20" y="28">03</text>
|
||||
<g transform="translate(15 40)"><g stroke="var(--drawing)" stroke-width="3" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M55 112v9m139-9v9"/><rect x="46" y="63" width="157" height="50" rx="17" fill="var(--couch)" stroke="none"/>
|
||||
<g class="sofa-friend"><g fill="var(--art-blue)" stroke="none"><circle cx="99" cy="39" r="12"/><path d="M78 88c0-19.5 7.5-30 21-30s21 10.5 21 30q0 4.5-4.5 4.5h-33Q78 92.5 78 88Z"/></g><path d="M95 39v1m8-1v1"/></g>
|
||||
<g class="sofa-friend"><g fill="var(--art-pink)" stroke="none"><circle cx="150" cy="37" r="12"/><path d="M129 88c0-19.5 7.5-30 21-30s21 10.5 21 30q0 4.5-4.5 4.5h-33Q129 92.5 129 88Z"/></g><path d="M146 37v1m8-1v1"/></g>
|
||||
<path d="M125 99v10M46 73h18a10 10 0 0 1 10 10v26m129-36h-18a10 10 0 0 0-10 10v26" fill="none" stroke="var(--couch-dark)"/>
|
||||
<g class="sofa-spark"><path d="m185 20 3 8 8 3-8 3-3 8-3-8-8-3 8-3Z" fill="var(--art-yellow)"/></g>
|
||||
</g></g>
|
||||
</g>
|
||||
<path d="M288 102h15m-5-5 5 5-5 5m300-5h15m-5-5 5 5-5 5" fill="none" stroke="var(--label)" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.5 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 102 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 101 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 103 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 102 KiB |
+41
-64
@@ -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.
|
||||
@@ -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.
|
||||
@@ -1,239 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="color-scheme" content="light dark">
|
||||
<title>Piik · Brand study / 品牌预览</title>
|
||||
<style>
|
||||
:root { color-scheme: light; --wall:#e9f2ee; --wall2:#dcebe4; --ink:#22303e; --paper:#fff; --orange:#e4572e; --green:#1f7a4a; --spark:#d08a22; }
|
||||
:root[data-theme="dark"] { color-scheme:dark; --wall:#141e29; --wall2:#0e1620; --ink:#dde8f3; --paper:#22303e; --green:#9de8bf; --spark:#f4c14d; }
|
||||
* { box-sizing:border-box; }
|
||||
body { margin:0; background:var(--wall); color:var(--ink); font:16px/1.55 ui-sans-serif,system-ui,-apple-system,"Segoe UI",sans-serif; }
|
||||
a { color:inherit; text-underline-offset:3px; }
|
||||
main { max-width:1100px; margin:auto; padding:28px 28px 44px; }
|
||||
header, .controls, .section-head, .card-heading, footer { display:flex; align-items:center; justify-content:space-between; gap:16px; }
|
||||
header { padding-bottom:27px; border-bottom:1px solid color-mix(in srgb,var(--ink) 18%,transparent); }
|
||||
.identity { display:flex; align-items:center; gap:10px; font-weight:750; letter-spacing:-.03em; }
|
||||
.identity svg { width:32px; height:32px; }
|
||||
.eyebrow, .number, .small-label { font-size:12px; font-weight:650; letter-spacing:.09em; text-transform:uppercase; }
|
||||
.eyebrow { color:var(--green); }
|
||||
h1,h2,h3,p { margin:0; }
|
||||
h1 { font-size:clamp(32px,4.5vw,52px); line-height:1.1; letter-spacing:-.055em; max-width:520px; margin:12px 0 16px; }
|
||||
h2 { font-size:25px; letter-spacing:-.035em; }
|
||||
h3 { font-size:17px; letter-spacing:-.02em; }
|
||||
.intro { display:grid; grid-template-columns:1fr 1fr; align-items:center; gap:32px; margin:44px 0 32px; }
|
||||
.intro p { max-width:440px; }
|
||||
.muted { opacity:.73; }
|
||||
.hero { border:2px solid var(--ink); border-radius:22px; background:var(--paper); overflow:hidden; box-shadow:0 6px 0 var(--wall2); }
|
||||
.stage { position:relative; padding:12px 16px 0; background:radial-gradient(ellipse at center,var(--wall2),transparent 70%); }
|
||||
#morph { display:block; width:100%; height:auto; max-height:275px; overflow:visible; }
|
||||
.caption { text-align:center; font-size:13px; padding:0 18px 17px; }
|
||||
.controls { flex-wrap:wrap; justify-content:center; gap:8px; padding:15px; border-top:1px solid color-mix(in srgb,var(--ink) 15%,transparent); }
|
||||
button { font:inherit; font-size:13px; font-weight:650; cursor:pointer; color:var(--ink); background:var(--paper); border:1.5px solid var(--ink); border-radius:9px; min-height:38px; padding:7px 12px; }
|
||||
button:hover { background:var(--wall2); }
|
||||
button:active { transform:translateY(1px); }
|
||||
button:disabled { opacity:.45; cursor:default; }
|
||||
button.primary { background:var(--ink); color:var(--paper); }
|
||||
button:focus-visible,input:focus-visible { outline:3px solid var(--orange); outline-offset:3px; }
|
||||
.motion-option { display:flex; align-items:center; justify-content:center; gap:7px; font-size:12px; padding:0 12px 13px; }
|
||||
input { accent-color:var(--green); }
|
||||
.section-head { margin:39px 0 15px; align-items:baseline; }
|
||||
.section-head p { font-size:13px; }
|
||||
.choices { display:grid; grid-template-columns:repeat(3,minmax(0,1fr)); gap:16px; }
|
||||
article { padding:20px; border:1px solid color-mix(in srgb,var(--ink) 24%,transparent); border-radius:15px; background:var(--paper); }
|
||||
article.recommended { border:2px solid var(--ink); padding:19px; }
|
||||
.tag { padding:3px 8px; border-radius:20px; font-size:11px; color:var(--green); background:var(--wall); white-space:nowrap; }
|
||||
.word { display:block; width:100%; height:105px; margin:10px 0 8px; }
|
||||
article p { font-size:13px; margin-top:7px; }
|
||||
.detail { display:grid; grid-template-columns:1fr 1.2fr; gap:30px; align-items:center; padding:24px; margin-top:18px; background:var(--wall2); border-radius:15px; }
|
||||
.detail p { font-size:14px; margin-top:10px; }
|
||||
.samples { width:100%; border-collapse:collapse; table-layout:fixed; text-align:left; }
|
||||
th,td { border-bottom:1px solid color-mix(in srgb,var(--ink) 15%,transparent); padding:9px 6px; vertical-align:middle; }
|
||||
th { font-size:12px; font-weight:650; }
|
||||
th:first-child { width:58px; }
|
||||
td svg { display:block; max-width:100%; }
|
||||
.size { font-size:12px; white-space:nowrap; opacity:.7; }
|
||||
.aside { display:flex; align-items:center; gap:12px; margin-top:15px; padding-top:13px; border-top:1px solid color-mix(in srgb,var(--ink) 15%,transparent); }
|
||||
.aside svg { width:32px; height:32px; flex:none; }
|
||||
.aside span { font-size:12px; }
|
||||
.zh { display:block; margin-top:6px; font-size:13px; font-weight:400; letter-spacing:0; line-height:1.6; opacity:.75; }
|
||||
.application { margin-top:18px; border:2px solid var(--ink); border-radius:18px; overflow:hidden; background:var(--paper); }
|
||||
.application header { padding:16px 24px; }
|
||||
.lockup { display:flex; align-items:center; gap:12px; }
|
||||
.lockup .tv { width:36px; height:36px; }
|
||||
.lockup .word { width:68px; height:38px; margin:0; }
|
||||
.sample-nav { display:flex; flex-wrap:wrap; gap:16px; font-size:12px; opacity:.75; }
|
||||
.site-hero { display:grid; grid-template-columns:1fr auto; align-items:center; gap:24px; padding:36px 32px; }
|
||||
.site-hero h3 { font-size:clamp(25px,3vw,38px); line-height:1.2; letter-spacing:-.04em; }
|
||||
.site-hero p { max-width:430px; margin-top:12px; font-size:14px; }
|
||||
.site-hero > .tv { width:128px; height:128px; color:var(--ink); }
|
||||
.steps { display:grid; grid-template-columns:repeat(3,minmax(0,1fr)); border-top:1px solid color-mix(in srgb,var(--ink) 18%,transparent); }
|
||||
.steps > div { padding:18px 24px; }
|
||||
.steps > div + div { border-left:1px solid color-mix(in srgb,var(--ink) 18%,transparent); }
|
||||
.steps p { font-size:13px; margin-top:6px; }
|
||||
.implementation { display:flex; flex-wrap:wrap; gap:10px 24px; margin:14px 0 0; font-size:12px; }
|
||||
code { font-family:ui-monospace,"Cascadia Mono",monospace; }
|
||||
footer { margin-top:27px; padding-top:18px; border-top:1px solid color-mix(in srgb,var(--ink) 18%,transparent); font-size:12px; flex-wrap:wrap; }
|
||||
.fallback { font-weight:750; font-size:42px; letter-spacing:-.045em; }
|
||||
.js .fallback { display:none; }
|
||||
.no-js { font-size:13px; }
|
||||
@media(max-width:760px) { main { padding:20px 18px 32px; } .intro { grid-template-columns:1fr; gap:24px; margin-top:30px; } .intro p { max-width:560px; } .hero { max-width:550px; width:100%; } .detail { grid-template-columns:1fr; gap:15px; } }
|
||||
@media(max-width:500px) { .choices { grid-template-columns:1fr; gap:10px; } article { display:grid; grid-template-columns:1fr 1.3fr; column-gap:14px; align-items:center; } .card-heading { grid-column:1/-1; } .word { height:90px; grid-row:2/4; } article h3 { margin-top:12px; } article p { margin:4px 0; } .section-head { align-items:start; flex-direction:column; gap:3px; } .detail { padding:18px; } .eyebrow { font-size:11px; } .application header { padding:14px; } .sample-nav { gap:9px; font-size:11px; } .site-hero { padding:24px 18px; gap:12px; } .site-hero > .tv { width:70px; height:70px; } .steps { grid-template-columns:1fr; } .steps > div + div { border-left:0; border-top:1px solid color-mix(in srgb,var(--ink) 18%,transparent); } }
|
||||
@media(prefers-reduced-motion:reduce) { button:active { transform:none; } }
|
||||
</style>
|
||||
<main>
|
||||
<header>
|
||||
<div class="identity"><svg class="tv" aria-hidden="true" viewBox="0 0 32 32"></svg><span>Piik / 品牌预览</span></div>
|
||||
<button id="theme" type="button" aria-label="Switch to dark theme">Dark room</button>
|
||||
</header>
|
||||
<section class="intro" aria-labelledby="intro-title">
|
||||
<div>
|
||||
<div class="eyebrow">01 / A name taking shape</div>
|
||||
<h1 id="intro-title">A little peek.<br>A familiar face.</h1>
|
||||
<p>From <strong>“Peek” in games</strong> to a little TV for watching together. Keep the wink. Give it a name.</p>
|
||||
<p class="muted" style="font-size:14px;margin-top:14px" lang="zh-CN">探个头,看看朋友在玩什么。名字换了,小电视还是那个小电视。</p>
|
||||
</div>
|
||||
<div class="hero">
|
||||
<div class="stage">
|
||||
<span class="fallback">Piik → TV</span>
|
||||
<svg id="morph" viewBox="70 20 330 210" role="img" aria-labelledby="morph-title morph-desc">
|
||||
<title id="morph-title">Piik becomes the winking TV</title>
|
||||
<desc id="morph-desc">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.</desc>
|
||||
</svg>
|
||||
</div>
|
||||
<p class="caption">Same strokes. Familiar wink.<span class="zh" lang="zh-CN">P 撑开屏幕,ii 变成眼睛和脚,k 的两笔转成电视天线。</span></p>
|
||||
<div class="controls">
|
||||
<button id="replay" class="primary" type="button">重播 / Replay</button>
|
||||
<button id="pause" type="button">暂停 / Pause</button>
|
||||
<button id="word-state" type="button">字标 / Wordmark</button>
|
||||
<button id="tv-state" type="button">电视 / TV</button>
|
||||
</div>
|
||||
<label class="motion-option"><input id="reduce" type="checkbox"> 减少动态 / Reduced motion</label>
|
||||
</div>
|
||||
</section>
|
||||
<noscript><p class="no-js">Motion is optional. Piik is the recommended spelling; PiiK and piik are the two alternatives below. The original TV remains the small icon.</p></noscript>
|
||||
<div class="section-head"><h2>Three ways to write it<span class="zh" lang="zh-CN">三个名字写法,同一套笔画</span></h2><p class="muted">One drawing style. Only the casing changes.</p></div>
|
||||
<section class="choices" aria-label="Casing comparison">
|
||||
<article class="recommended">
|
||||
<div class="card-heading"><span class="number">01</span><span class="tag">Recommended</span></div>
|
||||
<span class="fallback">Piik</span><svg class="word" data-case="Piik" role="img" aria-label="Piik, with an uppercase P and lowercase i i k"></svg>
|
||||
<h3>Piik</h3><p>A clear name, with two curious eyes.<span class="zh" lang="zh-CN">主方案。首字母清楚,两个 i 留一点好奇心。</span></p>
|
||||
</article>
|
||||
<article>
|
||||
<div class="card-heading"><span class="number">02</span><span class="tag">More contrast</span></div>
|
||||
<span class="fallback">PiiK</span><svg class="word" data-case="PiiK" role="img" aria-label="PiiK, with uppercase P and K"></svg>
|
||||
<h3>PiiK</h3><p>A stronger K gives the name a sharper silhouette.<span class="zh" lang="zh-CN">两侧更高、更硬朗,也更抢眼。</span></p>
|
||||
</article>
|
||||
<article>
|
||||
<div class="card-heading"><span class="number">03</span><span class="tag">Softer</span></div>
|
||||
<span class="fallback">piik</span><svg class="word" data-case="piik" role="img" aria-label="piik, all lowercase"></svg>
|
||||
<h3>piik</h3><p>Relaxed and compact, with a lower centre of gravity.<span class="zh" lang="zh-CN">更轻松。小写 p 会伸到基线下方。</span></p>
|
||||
</article>
|
||||
</section>
|
||||
<section class="detail" aria-labelledby="detail-title">
|
||||
<div>
|
||||
<div class="eyebrow">02 / The two little details</div>
|
||||
<h2 id="detail-title">Two dots. Two curious eyes.</h2>
|
||||
<p>Round dots keep the <strong>ii</strong> easy to read, then become the familiar TV eyes.</p>
|
||||
<p class="muted">The wordmark below is shown at everyday sizes.</p>
|
||||
<p class="zh" lang="zh-CN">两个圆点保留 ii 的辨识度,再变成小电视的眼睛。下面按实际尺寸查看。</p>
|
||||
<div class="aside"><svg class="tv" aria-hidden="true" viewBox="0 0 32 32"></svg><span>Keep the existing TV for the tiny icon.<br>No four-letter favicon puzzle.</span></div>
|
||||
</div>
|
||||
<table class="samples" aria-label="Piik at actual sizes">
|
||||
<thead><tr><th>Height</th><th>Piik</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td class="size">64 px</td><td><svg data-size="64" role="img" aria-label="Piik at 64 pixels"></svg></td></tr>
|
||||
<tr><td class="size">32 px</td><td><svg data-size="32" role="img" aria-label="Piik at 32 pixels"></svg></td></tr>
|
||||
<tr><td class="size">24 px</td><td><svg data-size="24" role="img" aria-label="Piik at 24 pixels"></svg></td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
<div class="section-head"><h2>At home on a page<span class="zh" lang="zh-CN">放到页面上:官网首屏的方向草图</span></h2><p class="muted">A static website. The sharing app stays a tool.</p></div>
|
||||
<section class="application" aria-label="Static homepage composition, not a live sharing service">
|
||||
<header>
|
||||
<div class="lockup" role="img" aria-label="TV mascot and Piik wordmark"><svg class="tv" aria-hidden="true" viewBox="0 0 32 32"></svg><svg class="word" data-case="Piik" aria-hidden="true"></svg></div>
|
||||
<div class="sample-nav"><span>Download</span><span>Guide</span><span>GitHub</span></div>
|
||||
</header>
|
||||
<div class="site-hero">
|
||||
<div><h3>Your game.<br>Their front row.</h3><p>Share your screen. Bring your friends.<br>They watch right in their browsers.</p><p class="zh" lang="zh-CN">开一局,朋友坐前排。<br>发个邀请,打开浏览器就能看。</p></div>
|
||||
<svg class="tv" aria-hidden="true" viewBox="0 0 32 32"></svg>
|
||||
</div>
|
||||
<div class="steps">
|
||||
<div><span class="number">01 / Pick</span><p>A screen, a window, a game.<span class="zh" lang="zh-CN">选好想分享的画面。</span></p></div>
|
||||
<div><span class="number">02 / Invite</span><p>Send a link to your friends.<span class="zh" lang="zh-CN">把邀请链接发给朋友。</span></p></div>
|
||||
<div><span class="number">03 / Hang out</span><p>A little room for your people.<span class="zh" lang="zh-CN">人到齐,就一起看。</span></p></div>
|
||||
</div>
|
||||
</section>
|
||||
<div class="implementation"><span>Display / 显示名称 <strong>Piik</strong></span><span>Technical / 技术名称 <code>piik-app</code></span><span>Website / 官网 <code>piik.tv</code></span><span>Page title / 页签 <code>Piik | 6020 · 📺</code></span></div>
|
||||
<footer><span>Design study · 09 Sep 2026 · 待讨论,尚未替换产品</span><span>Layout references: <a href="https://localsend.org/">LocalSend</a> · <a href="https://rustdesk.com/">RustDesk</a></span></footer>
|
||||
</main>
|
||||
<script>
|
||||
const $ = (id) => document.getElementById(id);
|
||||
const ns = 'http://www.w3.org/2000/svg';
|
||||
const tv = '<g fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="m11 4 5 4 5-4"/><rect x="4" y="8" width="24" height="18" rx="4"/><circle cx="11.5" cy="16.5" r="1.25" fill="currentColor" stroke="none"/><path d="M19 17c1.5-1.8 3.2-1.8 4.7 0M10 29l2-3m10 3-2-3"/></g>';
|
||||
document.querySelectorAll('.tv').forEach((svg) => svg.innerHTML = tv);
|
||||
// The k diagonals become antennae; the outer stems join the screen edges.
|
||||
const parts = [
|
||||
['rect', {x:108,y:72,width:58,height:48,rx:20}, {x:172,y:76,width:136,height:102,rx:22.667}],
|
||||
['line', {x1:108,y1:72,x2:108,y2:164}, {x1:172,y1:98.667,x2:172,y2:155.333}],
|
||||
['line', {x1:194,y1:118,x2:194,y2:164}, {x1:217.333,y1:178,x2:206,y2:195}],
|
||||
['line', {x1:236,y1:118,x2:236,y2:164}, {x1:262.667,y1:178,x2:274,y2:195}],
|
||||
['line', {x1:282,y1:72,x2:282,y2:164}, {x1:308,y1:98.667,x2:308,y2:155.333}],
|
||||
['line', {x1:284,y1:139,x2:318,y2:111}, {x1:240,y1:76,x2:268.333,y2:53.333}],
|
||||
['line', {x1:294,y1:131,x2:326,y2:164}, {x1:211.667,y1:53.333,x2:240,y2:76}],
|
||||
];
|
||||
const attributes = (node, values) => Object.entries(values).forEach(([key,value]) => node.setAttribute(key,String(value)));
|
||||
function element(parent, tag, values) { const node = document.createElementNS(ns,tag); attributes(node,values); parent.append(node); return node; }
|
||||
function animate(node, name, from, to, begin=.65, dur=1.25) {
|
||||
return element(node,'animate',{attributeName:name,from,to,begin:begin+'s',dur:dur+'s',fill:'freeze',calcMode:'spline',keyTimes:'0;1',keySplines:'.4 0 .2 1'});
|
||||
}
|
||||
function drawWord(svg, casing='Piik', moving=false) {
|
||||
if (!moving) svg.setAttribute('viewBox','96 58 244 138');
|
||||
const group = element(svg,'g',{fill:'none',stroke:'currentColor','stroke-width':10,'stroke-linecap':'round','stroke-linejoin':'round'});
|
||||
parts.forEach(([tag,start,end],index) => {
|
||||
const shape = {...start};
|
||||
if (casing === 'piik') { if(index===0) {shape.y=112;shape.height=48;} if(index===1) {shape.y1=112;shape.y2=188;} }
|
||||
if (casing === 'PiiK') { if(index===5) {shape.y1=122;shape.y2=72;} if(index===6) {shape.x1=286;shape.y1=120;} }
|
||||
const node = element(group,tag,shape);
|
||||
if(moving) Object.keys(end).forEach(key => animate(node,key,shape[key],end[key]));
|
||||
});
|
||||
[194,236].forEach((cx,index) => {
|
||||
const eyeX = index?270.317:214.5;
|
||||
const eye = element(group,'ellipse',{cx,cy:95,rx:6.5,ry:6.5,fill:'currentColor',stroke:'none'});
|
||||
if(moving) { animate(eye,'cx',cx,eyeX); animate(eye,'cy',95,124.167); animate(eye,'rx',6.5,7.083); animate(eye,'ry',6.5,7.083); if(index) animate(eye,'ry',7.083,0,2.2,.16); }
|
||||
});
|
||||
if(moving) {
|
||||
animate(group,'stroke-width',10,13.6);
|
||||
const wink = element(group,'path',{d:'M257 127c8.5-10.2 18.133-10.2 26.633 0',pathLength:1,'stroke-dasharray':1,'stroke-dashoffset':1});
|
||||
animate(wink,'stroke-dashoffset',1,0,2.3,.23);
|
||||
const sparkles = element(group,'g',{stroke:'var(--spark)','stroke-width':7.933,opacity:0});
|
||||
element(sparkles,'path',{d:'M291 95.8L295.5 106.6L306.3 111.1L295.5 115.7L291 126.4L286.5 115.7L275.7 111.1L286.5 106.6Z'});
|
||||
element(sparkles,'path',{d:'M291 128.1L293.6 134.4L299.8 136.9L293.6 139.5L291 145.7L288.5 139.5L282.2 136.9L288.5 134.4Z'});
|
||||
animate(sparkles,'opacity',0,1,2.4,.15);
|
||||
animate(sparkles,'opacity',1,0,2.8,.4);
|
||||
}
|
||||
}
|
||||
drawWord($('morph'),'Piik',true);
|
||||
document.querySelectorAll('[data-case]').forEach(svg => drawWord(svg,svg.dataset.case));
|
||||
document.querySelectorAll('[data-size]').forEach(svg => { const height=Number(svg.dataset.size); attributes(svg,{height,width:height*244/138}); drawWord(svg); });
|
||||
const morph=$('morph'), reduce=$('reduce'), pause=$('pause'), motion=matchMedia('(prefers-reduced-motion: reduce)');
|
||||
const duration=3.3;
|
||||
let frame;
|
||||
function still(at) { cancelAnimationFrame(frame); morph.pauseAnimations(); morph.setCurrentTime(at); pause.textContent='继续 / Resume'; pause.disabled=reduce.checked||at>=duration; }
|
||||
function watch() { if(morph.getCurrentTime()>=duration) { still(duration); return; } frame=requestAnimationFrame(watch); }
|
||||
function replay() { still(0); if(reduce.checked) { still(duration); return; } pause.textContent='暂停 / Pause'; pause.disabled=false; morph.unpauseAnimations(); watch(); }
|
||||
$('replay').onclick=replay;
|
||||
pause.onclick=() => { if(morph.animationsPaused()) { pause.textContent='暂停 / Pause'; morph.unpauseAnimations(); watch(); } else still(morph.getCurrentTime()); };
|
||||
$('word-state').onclick=() => still(0);
|
||||
$('tv-state').onclick=() => still(duration);
|
||||
reduce.checked=motion.matches;
|
||||
reduce.onchange=() => still(reduce.checked?duration:0);
|
||||
motion.addEventListener('change',event => { reduce.checked=event.matches; reduce.onchange(); });
|
||||
const dark=matchMedia('(prefers-color-scheme: dark)');
|
||||
function theme(isDark) { document.documentElement.dataset.theme=isDark?'dark':'light'; $('theme').textContent=isDark?'Light room':'Dark room'; $('theme').setAttribute('aria-label',isDark?'Switch to light theme':'Switch to dark theme'); }
|
||||
theme(dark.matches);
|
||||
$('theme').onclick=() => theme(document.documentElement.dataset.theme!=='dark');
|
||||
document.documentElement.classList.add('js');
|
||||
if(reduce.checked) still(0); else replay();
|
||||
</script>
|
||||
</html>
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
|
||||
<details>
|
||||
<summary>Playback controls, at a glance</summary>
|
||||
|
||||
| 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. |
|
||||
|
||||
</details>
|
||||
|
||||
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).
|
||||
@@ -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. 将鼠标移到画面上,或轻点画面,显示播放控制栏。
|
||||
|
||||
播放控制栏只改变你自己看到的画面和听到的声音。
|
||||
|
||||
<details>
|
||||
<summary>播放按钮分别做什么?</summary>
|
||||
|
||||
| 按钮 | 作用 |
|
||||
| --- | --- |
|
||||
| **剧场模式** | 在浏览器标签页内展开画面。 |
|
||||
| **全屏观看** | 让画面铺满屏幕。 |
|
||||
| **悬浮小窗** | 浏览器支持时,把视频放进独立小窗。 |
|
||||
| **音量** | 支持增强时最高 200%,否则最高 100%;声音失真时调低一些。 |
|
||||
|
||||
</details>
|
||||
|
||||
只有四位房间号时,打开同一个 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) 找到。
|
||||
+79
-52
@@ -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.
|
||||
@@ -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)
|
||||
+68
-201
@@ -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:<full-revision> /path/to/extracted-runtime
|
||||
docker run --rm --env-file /path/to/piik.env \
|
||||
piik:<full-revision> --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:<full-revision>
|
||||
./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)
|
||||
@@ -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)
|
||||
@@ -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:<full-revision> /path/to/extracted-runtime
|
||||
docker run --rm --env-file /path/to/piik.env \
|
||||
piik:<full-revision> --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:<full-revision>
|
||||
```
|
||||
|
||||
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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
|
||||
@@ -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 `<video>` element owns Viewer play/pause, volume, mute, and fullscreen.
|
||||
Those actions are local and never change Host capture, room state, routing, or
|
||||
other Viewers. The Host preview has no media controls; explicit Host actions own
|
||||
authoritative share pause and stop.
|
||||
One `<video>` element owns Viewer playback and frame proof. The shared playback
|
||||
bar reflects that element and provides local play/pause, mute, volume, theater,
|
||||
picture-in-picture, fullscreen and the existing reconnect action. It stays reachable while waiting
|
||||
or disconnected. Theater and fullscreen are separate modes; fullscreen includes
|
||||
the playback bar. Identity, connection details and topology stay in the deck.
|
||||
|
||||
Autoplay rejection exposes the native play action. A connection, track object,
|
||||
The bar spans the screen's lower edge. Narrow screens separate audio controls
|
||||
from window actions into two rows and retain 44px action targets. Playing video hides the bar
|
||||
after two idle seconds, following [Media Chrome's default](https://github.com/muxinc/media-chrome/blob/main/docs/src/pages/docs/en/components/media-controller.md#autohide).
|
||||
Pause or unavailable playback keeps it visible. Control hover, keyboard focus,
|
||||
dragging and an open tooltip hold it open; mouse movement reveals it, and a
|
||||
touch tap on the picture toggles only visibility. One presentation-only idle
|
||||
timer owns this behavior; it never changes playback or route state. Controls
|
||||
follow the shared [motion grammar](../design/visual-language.md#motion-grammar).
|
||||
|
||||
Volume starts on native audio output. An explicit setting away from 100% activates
|
||||
a local Web Audio stream source and gain, from silence to 200% amplitude. This also
|
||||
avoids iPhone's system-owned `video.volume`. This output
|
||||
exclusively owns sound while running, follows video pause and user mute, and
|
||||
rebinds when the audio track changes. It never modifies the received track or
|
||||
the stream relayed downstream. Unmount closes the graph. Browsers without gain
|
||||
support retain 0–100%; video-only system fullscreen also hands audio back to the
|
||||
native controls at at most 100%, rather than running two outputs.
|
||||
|
||||
This uses the standard [Web Audio stream-source and gain nodes](https://www.w3.org/TR/webaudio/).
|
||||
The gain is amplitude scaling, so loud source peaks can clip above 100%.
|
||||
|
||||
Picture-in-picture uses the browser's native video window, including Safari's
|
||||
presentation-mode API. Native enter/leave events own its displayed state; closing
|
||||
the window or replacing the received stream does not create another playback
|
||||
owner. Successful entry exits this video's other fullscreen mode, and unmount
|
||||
closes only this video's window. Unavailable capabilities remain disabled with
|
||||
an explanation. The native window contains video and system controls, not page
|
||||
overlays or the custom bar. See the [PiP specification](https://w3c.github.io/picture-in-picture/)
|
||||
and [Safari integration](https://developer.apple.com/documentation/webkitjs/adding_picture_in_picture_to_your_safari_media_controls).
|
||||
|
||||
Local playback actions never change Host capture or another Viewer. Reconnect
|
||||
retains the existing route-operation owner. The Host preview remains muted and
|
||||
has no media controls; explicit Host actions own authoritative pause and stop.
|
||||
|
||||
Autoplay rejection exposes the play action. A connection, track object,
|
||||
or `playing` event alone is not proof that the current route is visible. The
|
||||
Viewer removes its blocking presentation only after a current-generation frame
|
||||
is composited, using `requestVideoFrameCallback()` where available and a decoded-
|
||||
@@ -97,9 +133,12 @@ a replacement controller restarted revision numbering; ordinary updates remain
|
||||
monotonic within that authority. A Viewer frame belongs to its local media generation and remains
|
||||
current across unrelated graph revisions until that exact binding is replaced,
|
||||
invalidated, or terminally failed. Pending candidates never own the primary
|
||||
badge, status line, or overlay while committed media remains proved. Those
|
||||
surfaces share the presentation reducer; raw transport snapshots provide route
|
||||
labels, reconnect authority, and diagnostics only.
|
||||
badge, status line, or overlay while committed media remains proved. A shared
|
||||
visual projection maps those facts to title, media status, notices and
|
||||
overlays. None of these views owns state for another. Connection availability,
|
||||
actual playback and quality limitation remain separate; raw transport snapshots
|
||||
do not prove playback or quality. The [status preview](../design/media-status.md)
|
||||
records the visual vocabulary and the evidence needed for participant lamps.
|
||||
|
||||
Excluding time spent in the Browser's capture/play authorization UI, a Viewer
|
||||
request targets a first visible frame within three seconds. Until then, the page
|
||||
@@ -107,9 +146,17 @@ must continuously show a truthful accessible connection stage rather than a
|
||||
black screen, ICE-connected state, or unproved `playing` event.
|
||||
|
||||
- A proved current frame remains visible behind non-terminal recovery state.
|
||||
- Host roster connection state follows the server's committed physical media
|
||||
path. A pending candidate or temporarily stale quality sample may annotate
|
||||
optimization, but cannot downgrade an active Viewer to routing.
|
||||
- The [status projection](../design/media-status.md#projection-rules) owns the
|
||||
television, Header, notices, participant lamps and title. These consumers
|
||||
cannot turn an unproved frame into playback or ordinary quality into failure.
|
||||
- The Host's couch and Viewer overview share the
|
||||
[participant projection](../design/media-status.md#projection-rules). The
|
||||
Host's own pawn has no additional lamp. Committed `mediaReady` owns readiness;
|
||||
transport diagnostics and quality samples cannot override it in either direction.
|
||||
- Viewers do not infer another Viewer's quality or diagnose missing readiness
|
||||
as terminal failure; roster readiness and quality evidence remain distinct.
|
||||
- An overlay explains the absence of a usable picture, authoritative Host pause,
|
||||
or a required user action. Ordinary quality warnings never cover playback.
|
||||
- Host pause retains the current frame and waits for resume.
|
||||
- Host signaling loss alone does not invalidate media that is still healthy.
|
||||
- Exact media failure or terminal route failure invalidates current-frame proof
|
||||
@@ -121,10 +168,11 @@ black screen, ICE-connected state, or unproved `playing` event.
|
||||
state never forces the user out of fullscreen.
|
||||
|
||||
Visibility, page freeze, and pagehide suppress application decoded-stall
|
||||
authority. Returning to the page rebaselines time and proves a current frame
|
||||
again before clearing recovery. SFU media is retained independently of transient
|
||||
room-signaling loss, but Browser or OS suspension and page reclamation remain
|
||||
outside Web guarantees.
|
||||
authority and invalidate quality observations, not already-proved playback.
|
||||
Returning to the page rebaselines time and re-arms frame observation; an existing
|
||||
recovery state still requires a fresh current frame to clear. SFU media is
|
||||
retained independently of transient room-signaling loss, but Browser or OS
|
||||
suspension and page reclamation remain outside Web guarantees.
|
||||
|
||||
Failure of the local Native media bridge disables Native reception for the
|
||||
current Viewer session before route recovery runs. Future peers in that session
|
||||
@@ -147,6 +195,13 @@ loss; deeper fields appear only when meaningful. Topology is a separate view.
|
||||
Native bandwidth or CPU limitation warnings describe the exact outbound sender
|
||||
that reported adaptation; they do not by themselves locate the physical
|
||||
bottleneck or describe the Viewer's receive path.
|
||||
The Host's participant freeze warning requires fresh receive evidence matching
|
||||
the current upstream and a positive freeze count or duration for that window.
|
||||
Window `*Delta` fields never inherit older values; other fields may retain their
|
||||
existing details-display cache. The existing evidence expiry removes stale
|
||||
warnings. No additional participant state, broadcast, timer or quality score is
|
||||
introduced. Detailed per-edge observations remain available without a global
|
||||
warning selected from the first limited child.
|
||||
Locally exposed selected-candidate addresses may be shown only on the Browser
|
||||
that owns that PeerConnection and are never uploaded, persisted, or used for
|
||||
identity or route selection. Explicit Debug mode can export a bounded diagnostic
|
||||
|
||||
@@ -35,8 +35,10 @@ the storage decision; implementation detail belongs in code and tests.
|
||||
|
||||
Three independent authorities exist:
|
||||
|
||||
1. **Site access.** Production requires an independent
|
||||
`SITE_ACCESS_PASSWORD`. Successful entry creates a stateless, HttpOnly,
|
||||
1. **Site access.** `SITE_ACCESS_PASSWORD` is optional in every environment,
|
||||
including production. Unset or empty makes site access immediate without a
|
||||
cookie; room ownership and Viewer admission still apply. When configured,
|
||||
successful password entry creates a stateless, HttpOnly,
|
||||
`SameSite=Strict` cookie with a 24-hour rolling idle lifetime. An already
|
||||
authorized page renews it through the site-access check while it remains
|
||||
active. It authorizes room creation, Host role, and code-only Viewer
|
||||
@@ -90,6 +92,9 @@ code-only attempts and does not silently revoke invitations.
|
||||
choice and optional Local site-access password, not rooms or media state. A
|
||||
blank password leaves that local site open.
|
||||
|
||||
Persistence evidence, limits and the earlier cross-restart evaluation live in
|
||||
[cross-restart recovery research](../research/cross-restart-room-recovery.md).
|
||||
|
||||
The App opens its own localhost Host page with the configured password, when
|
||||
present, in a fragment. The page removes the fragment and uses the existing
|
||||
SiteAccess endpoint; this does not create a fourth admission authority. LAN and
|
||||
|
||||
@@ -17,14 +17,17 @@ owns ports and [self-hosting](../operations/self-hosting.md) owns service setup.
|
||||
- A route ends in usable media, explicit bounded waiting, or clear failure.
|
||||
|
||||
Every deployment uses this controller. Leaving embedded SFU disabled keeps the
|
||||
same bounded P2P graph without its SFU fallback; there is no separate Host-star mode.
|
||||
same bounded P2P graph without its SFU fallback and fixes Privacy mode on for all
|
||||
rooms, including App Local and public-link rooms. The server enforces that policy
|
||||
for waiting and active participants; there is no separate Host-star mode.
|
||||
|
||||
## Per-Share Route Policy
|
||||
|
||||
The Host chooses route policy before sharing and it remains fixed for that
|
||||
share generation:
|
||||
|
||||
- default hybrid mode keeps P2P first and permits the bounded SFU suffix;
|
||||
- when SFU is available, default hybrid mode keeps P2P first and permits the
|
||||
bounded SFU suffix;
|
||||
- peer-only mode excludes SFU publication, subscription, bootstrap, and quality
|
||||
candidates, while retaining the same bounded Peer graph and clear exhausted
|
||||
failure; and
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# Project Memory
|
||||
|
||||
Last updated: 2026-09-10
|
||||
Last updated: 2026-09-11
|
||||
|
||||
Piik is private, low-latency game screen sharing for one Host and up to 20
|
||||
Piik is private, low-latency screen sharing for one Host and up to 20
|
||||
authenticated friends. The current product surface is Web Host, Web Viewer, and
|
||||
Browser relay; desktop and mobile Browsers are Viewer targets. It is not a
|
||||
public broadcast service. Hosted and App run one shared Go core
|
||||
@@ -94,8 +94,8 @@ implementation and routine UI detail.
|
||||
|
||||
## Current Snapshot
|
||||
|
||||
The current contract uses strict `piik-v23` Browser/server signaling,
|
||||
Native control v9 and capture v7 together. Embedded SFU SDP/ICE and demand travel over the
|
||||
The current contract tuple is owned by [status](./status.md#accepted-release-contract).
|
||||
Embedded SFU SDP/ICE and demand travel over the
|
||||
authenticated room WebSocket, without an external room service or media token.
|
||||
Committed first-frame readiness, bounded candidate-relative progress, durable
|
||||
room authority and the H.264/VP8 sender gate remain. [Status](./status.md)
|
||||
|
||||
@@ -4,6 +4,18 @@ The tracked [`.env.example`](../../.env.example) is the executable schema
|
||||
companion; `internal/server/config` is validation truth. Keep real values in the
|
||||
service secret store or an untracked access-restricted environment file.
|
||||
|
||||
## Ownership
|
||||
|
||||
- Deployment configuration owns listeners, advertised origins, persistence,
|
||||
admission, capacity and optional services. Change it before starting the process.
|
||||
- The Host owns share settings and the available per-share route switches in the
|
||||
shared Web UI. Server and App use the same controls, order and semantics;
|
||||
configuration changes availability, not which controls exist. A fixed switch
|
||||
remains visible and explains the reason in text or its pure-visual hint.
|
||||
- Protocol versions, queue bounds and routing/adaptation constants stay in code.
|
||||
They are not deployment tuning knobs. Debug and report destinations are
|
||||
explicit diagnostic options, not normal share settings.
|
||||
|
||||
## Application Environment
|
||||
|
||||
| Variable | Contract |
|
||||
@@ -13,19 +25,19 @@ service secret store or an untracked access-restricted environment file.
|
||||
| `PORT` | Positive TCP port, default `8787`; the tracked release wrapper supports only that default. |
|
||||
| `PUBLIC_BASE_URL` | Exact public HTTP(S) origin; production requires HTTPS. |
|
||||
| `ALLOWED_ORIGINS` | Comma-separated exact HTTP(S) origins; wildcard is invalid. |
|
||||
| `SITE_ACCESS_PASSWORD` | Production-required independent 8-128 visible-ASCII byte secret. |
|
||||
| `SITE_ACCESS_PASSWORD` | Optional in every environment. Unset or empty allows entry without a site password. A non-empty value must contain 8-128 visible ASCII bytes; room ownership and Viewer admission remain independent. |
|
||||
| `ROOM_DATABASE_PATH` | Hosted defaults to `rooms.sqlite` in its working directory when unset or blank. An explicit absolute file path selects another SQLite file; `:memory:` opts into process-memory room authority. App Local remains in memory. |
|
||||
| `MAX_VIEWERS_PER_ROOM` | `1..20`, default `8`. |
|
||||
| `ENDPOINT_MEDIA_COPY_CAPACITY` | Shared endpoint steady-copy cap `1..3`, default `2`. |
|
||||
| `STUN_URLS` | Comma-separated advertised `stun:` discovery URLs; at least one is required in production. These are not local bind addresses and may use an unproxied DNS name separate from the Web origin. |
|
||||
| `STUN_LISTEN_HOST` | Hosted IPv4 STUN bind address, default `0.0.0.0` when `STUN_URLS` is configured; independent of HTTP `LISTEN_HOST`. Local App construction creates no STUN listeners. |
|
||||
| `NAT_PREDICTION_ENABLED` | Optional bounded NAT prediction capability, default `false`; requires an ordinary `STUN_URLS` endpoint on UDP 3478. Hosted startup binds UDP 3479/3480 before advertising the capability. Firewall reachability remains an operator requirement. |
|
||||
| `NAT_PREDICTION_ENABLED` | Optional bounded NAT prediction capability, default `false`; requires an ordinary `STUN_URLS` endpoint on UDP 3478. Hosted startup binds UDP 3479/3480 before advertising the capability. When unavailable, the visible NAT switch is locked off; ordinary ICE remains. Firewall reachability remains an operator requirement. |
|
||||
|
||||
Automatic SFU fallback runs inside the Hosted process when `SFU_UDP_PORT` is set:
|
||||
|
||||
| Variable | Contract |
|
||||
| --- | --- |
|
||||
| `SFU_UDP_PORT` | Optional UDP media port `1..65535`; unset or blank disables SFU. Set `7882` for the standard public listener. |
|
||||
| `SFU_UDP_PORT` | Optional UDP media port `1..65535`; unset or blank disables SFU and fixes Privacy mode on for every room. Set `7882` for the standard public listener and allow the Host to choose Privacy mode. |
|
||||
| `SFU_LISTEN_HOST` | IPv4 bind address, default `0.0.0.0`; independent of HTTP `LISTEN_HOST`. Read only when SFU is enabled. |
|
||||
| `SFU_PUBLIC_IP` | Optional explicit IPv4 advertised-address override for a host behind NAT. Read only when SFU is enabled. |
|
||||
|
||||
@@ -34,6 +46,11 @@ separate control origin or infrastructure credentials are configured. Local and
|
||||
public-link App construction create no SFU listener. The relay does not
|
||||
provide application E2EE.
|
||||
|
||||
For a P2P-only Server, leave `SFU_UDP_PORT` blank. Room authority, signaling,
|
||||
configured STUN and peer relays remain; no media-server fallback is possible.
|
||||
The server enforces this even if a Host requests hybrid mode. There is no
|
||||
separate `SFU_ENABLED` flag to conflict with the listener configuration.
|
||||
|
||||
The SQLite parent directory must exist and be writable. The systemd template
|
||||
sets `/var/lib/piik/rooms.sqlite` under its managed state directory; the
|
||||
container image sets `/home/nonroot/rooms.sqlite` under its writable data
|
||||
@@ -45,10 +62,41 @@ Removed access, room TTL/lease, endpoint-tier, room-rollout, and TURN variables
|
||||
startup even when blank. A present `NODE_ENV` fails the same way, so a stale
|
||||
environment file cannot silently drop a deployment out of production. The
|
||||
private deployment is upgraded atomically; there are no compatibility aliases or
|
||||
dual configuration readers.
|
||||
dual configuration readers. [Versioning](./versioning.md) owns the planned public
|
||||
upgrade promise and the work required before its first release.
|
||||
|
||||
## Piik App Configuration
|
||||
|
||||
Piik App stores `Piik/client.json` under the operating system's user configuration
|
||||
directory (`%APPDATA%` on Windows, `~/Library/Application Support` on macOS,
|
||||
and `$XDG_CONFIG_HOME` or `~/.config` on Linux). The App manages its `version`
|
||||
schema field. The user settings are:
|
||||
|
||||
| Setting | Contract |
|
||||
| --- | --- |
|
||||
| `site` | Saved Piik Site origin; omit it for Local mode. The launcher or `--site` updates it. |
|
||||
| `localAccessPassword` | Empty by default. Optional password for the App's Local room authority, using the same 8-128 visible ASCII bounds. It is separate from a hosted site's password. |
|
||||
|
||||
Command-line options select entry and local runtime behavior:
|
||||
|
||||
| Option | Purpose |
|
||||
| --- | --- |
|
||||
| `--site <origin>` / `--local` | Save and use a Site, or select the self-contained Local mode. Mutually exclusive. |
|
||||
| `--link` | Create a public Viewer invitation for Local mode. |
|
||||
| `--config <path>` | Select another App configuration file. |
|
||||
| `--lan-address <IPv4>` / `--port <port>` | Override the Local invitation address or HTTP port (default `8787`). |
|
||||
| `--capture-process <path>` / `--tunnel-process <path>` | Override packaged native capture or public tunnel helpers. Ordinary installations use the packaged paths. |
|
||||
| `--debug` / `--log-dir <path>` | Enable diagnostics or choose their destination as described below. |
|
||||
|
||||
Share quality, room access policy, language, theme and motion are configured in
|
||||
the shared Web UI, not through Server environment variables or App JSON.
|
||||
|
||||
## Diagnostics
|
||||
|
||||
Normal Server logs record startup identity, listener and enabled services,
|
||||
unexpected failures, orderly shutdown and any available release found by the
|
||||
single background startup check. Detailed room/ICE/media traces require Debug.
|
||||
|
||||
Diagnostics are local and opt-in. Enable them **before** reproducing the problem:
|
||||
|
||||
| Surface | Enable | Export |
|
||||
@@ -57,6 +105,11 @@ Diagnostics are local and opt-in. Enable them **before** reproducing the problem
|
||||
| Browser Host/Viewer | Add `?debug=1` to the page URL, before any invitation fragment | Use the download button beside language/theme controls |
|
||||
| Hosted Server | Start with `--debug`, `PIIK_DEBUG=server` or `PIIK_DEBUG=route` | On Unix, `kill -USR1 <pid>`; also exported at orderly shutdown |
|
||||
|
||||
Server Debug is controlled by its startup environment or CLI, never by a remote
|
||||
page or room role. Browser `?debug=1` only enables that page's local collection;
|
||||
it cannot change Server logging or download Server reports. The Server exposes
|
||||
no HTTP diagnostic export or pprof endpoint.
|
||||
|
||||
App/Server ZIP and Browser JSON reports are separate: when investigating
|
||||
Browser/App cooperation, include both from the same reproduction. Neither
|
||||
action stops an active share or uploads anything. Browser export also remains
|
||||
|
||||
@@ -0,0 +1,199 @@
|
||||
# Engineering And Interface Boundaries
|
||||
|
||||
This is the shared engineering convention for Browser, App and Server work.
|
||||
[AGENTS.md](../../AGENTS.md) owns task authority and hard product constraints;
|
||||
[CONTRIBUTING.md](../../CONTRIBUTING.md) owns the delivery workflow. The required
|
||||
[ponytail guidance](../../.agents/skills/ponytail/SKILL.md) selects the smallest
|
||||
correct implementation. This file owns module responsibilities and interface
|
||||
discipline, not a second copy of the product contracts or wire fields.
|
||||
|
||||
## Repository Layout
|
||||
|
||||
Keep conventional project entry points and tool-discovered configuration at the
|
||||
root. Scope-specific configuration belongs with its owner: `src/tsconfig.json`
|
||||
checks the Browser, `scripts/tsconfig.json` checks tooling/tests, and root
|
||||
`tsconfig.json` only connects those projects.
|
||||
|
||||
| Location | What belongs here |
|
||||
| --- | --- |
|
||||
| Root README, contribution and agent files | Project introduction and shared working constraints; detailed documents live in `docs/` |
|
||||
| Root manifests, lockfiles, Vite/Vitest config, `index.html` | Go/npm entry points and Browser build/test startup; root `LICENSE` is authoritative |
|
||||
| `src/client`, `src/shared` | Browser UI/media orchestration and its shared wire types; the same UI serves Hosted and App modes |
|
||||
| `public/` | Static assets shipped with that Browser application |
|
||||
| `cmd/`, `internal/` | Go executable composition and private application, server, media and diagnostics packages |
|
||||
| `native/capture`, `native/fixtures`, `native/probes` | Platform capture implementations, synthetic workloads and standalone capability probes |
|
||||
| `site/` | Independently published static introduction; its assets do not implement product behavior |
|
||||
| `scripts/` | Development checks, browser/media gates, dependency assembly and release packaging/publishing tools |
|
||||
| `tests/` | TypeScript/tooling tests and cross-language fixtures; Go tests stay beside their packages |
|
||||
| `deploy/` | Operator scripts and container, proxy, service and certificate templates |
|
||||
| `docs/` | User guides, contracts, decisions and operations; [documentation ownership](../maintenance.md) defines each owner |
|
||||
| `licenses/` | Third-party license texts, pinned notice sources and their redistribution reference |
|
||||
| `.github/`, `.githooks/`, `.agents/`, `.codex/` | Platform automation and scoped agent/tool integration |
|
||||
| `build/`, `node_modules/`, `coverage/` | Ignored local output and dependencies; never current product truth |
|
||||
|
||||
`site/` introduces Piik; `src/client/` is Piik's interactive application.
|
||||
`internal/app/nativecapture` adapts the capture contract in Go; `native/capture/`
|
||||
implements it using platform APIs. `scripts/` prepares and verifies artifacts;
|
||||
`deploy/` operates them. Move files with their actual consumers, and update paths
|
||||
and checks together; a smaller root alone does not justify another abstraction.
|
||||
|
||||
## Module Map
|
||||
|
||||
| Owner | Responsibility and dependency boundary |
|
||||
| --- | --- |
|
||||
| `cmd/piik-server`, `cmd/piik-app` | Thin entry points: defaults, startup and shutdown; compose the same Go room service |
|
||||
| `internal/server/app` | HTTP, site access, runtime capabilities, static assets and listener lifecycle; room mutations go through `signal` |
|
||||
| `internal/server/signal` | Authenticated command/effect owner; serializes room, session and route mutations and executes controller decisions |
|
||||
| `internal/server/room` | Room authority, credentials and persistence; caller holds the signaling lock, durable writes precede in-memory changes |
|
||||
| `internal/server/route` | Synchronous room graph and single-operation decisions; returns resource effects to `signal`, owns no locks or timers, and calls its optional diagnostic sink synchronously |
|
||||
| `internal/server/sfu`, `internal/app/mediaedge` | Server forwarding and native peer adapters over the shared `internal/media/forwarding`; Pion/LiveKit own transport and adaptation |
|
||||
| `internal/app` | Local/public-link/Site composition, loopback service, native sessions and App lifecycle; never a second room backend |
|
||||
| `internal/app/nativecapture`, `native/capture/*` | App-side capture contract and minimal platform adapters; platform-specific capture/encoding stays here |
|
||||
| `src/client/pages`, `lib`, `media`, `native` | Browser orchestration and endpoint adapters; bind current media and retire owned resources |
|
||||
| `src/client/media/viewer-presentation.ts`, `ui/media-status.ts` | Playback facts and their derived status projection; UI indicators do not create recovery or quality policy |
|
||||
| `src/client/components`, `locales`, `ui` | Shared interaction, visual primitives and localized copy; same product interface for Server and App |
|
||||
| `internal/server/webassets` | One built Browser bundle embedded by both binaries; Node/Vite are development tooling |
|
||||
|
||||
Names follow [naming](./naming.md). Architecture decisions explain why these
|
||||
boundaries exist: [shared core](../adr/0012-shared-go-backend-core.md),
|
||||
[embedded media](../adr/0013-embedded-node-local-media.md) and
|
||||
[Browser pooling](../adr/0014-browser-node-local-encoding-pool.md).
|
||||
Directory names are a map, not a requirement to introduce another layer.
|
||||
|
||||
## Contract Map
|
||||
|
||||
| Boundary | Schema/validation owners |
|
||||
| --- | --- |
|
||||
| Room HTTP and Browser/Server signaling | `src/shared/protocol.ts` and `internal/server/protocol`; HTTP dispatch in `internal/server/app`, WS dispatch in `signal` |
|
||||
| Site access HTTP | `src/client/lib/api.ts` and `internal/server/app/json.go`; cookies and access behavior belong to [rooms/access](../product/rooms-access.md) |
|
||||
| Browser/App discovery and control | `src/client/native/wire.ts`, `internal/app/loopback/protocol.go`, `internal/app/nativecontrol/wire.go` |
|
||||
| App/capture sidecar | `internal/app/nativecapture` and each platform capture adapter; probe/commands and encoded-frame envelope are distinct formats |
|
||||
| Configuration and persistence | `internal/server/config`, `internal/app/config`, `internal/server/room/database.go`; operator semantics in [configuration](./configuration.md) |
|
||||
| Release and update metadata | Existing packaging scripts, `src/client/lib/release-update.ts`, `cmd/piik-server/release.go`; identity/compatibility in [versioning](./versioning.md), operations in [deployment](../deployment.md) |
|
||||
|
||||
The shared `tests/fixtures/wire-samples.json` is consumed by TypeScript and Go
|
||||
fixture tests. It covers room requests and signaling; it is not a universal
|
||||
schema for all HTTP, Native or capture traffic. Change the actual two ends and
|
||||
the relevant existing fixture/check together. Do not create another handwritten
|
||||
field list in documentation, parallel DTO hierarchy or schema generator without
|
||||
a demonstrated reduction in ownership or drift.
|
||||
|
||||
## Runtime Lifecycles
|
||||
|
||||
Read each runtime as acquisition, the authoritative commit that makes its work
|
||||
visible, and the retirement that ends it. Revalidate the acquiring identity
|
||||
after every await or callback; presentation derives from the committed fact
|
||||
instead of mirroring it.
|
||||
|
||||
| Runtime | Acquire | Commit | Retire |
|
||||
| --- | --- | --- | --- |
|
||||
| Browser Host share | `pages/HostPage.tsx` captures one source and creates its peers and route | current generation plus the owning identity: local-offer epoch, publisher slot revision, source-switch and quality-change tokens | `disposeResources` stops owned tracks, disposes peers and sends `stop-sharing`; a replaced source retires through `retiringStreamRef` |
|
||||
| Browser Viewer route | `pages/ViewerPage.tsx` binds one parent peer (Browser or Native) and its relay children | parent, connection and message identity, then playback facts | a new viewer generation retires the old binding; relay children follow `activateChildren` revisions |
|
||||
| Browser relay fanout | `webrtc/viewer-relay.ts` prepares a child before it is needed | `activateChildren(revision, childPeerIds)` commits the prepared set | replaced or emptied sets disconnect the child peers they own |
|
||||
| Browser local encoding pool | `webrtc/host-peer.ts` attaches a pool carrier when capture and APIs support it ([ADR-0014](../adr/0014-browser-node-local-encoding-pool.md)) | the carrier binding owns its pooled producer while attached | source change or disposal releases the carrier; unsupported APIs keep ordinary senders |
|
||||
| App native session | the loopback service admits native control sessions and the Web UI selects the native path | session identity plus the explicit media-path choice | session end, share end or process exit retires that edge and its capture sidecar |
|
||||
| Server room authority | `signal` authenticates one Host or Viewer command | `room` writes durably before the in-memory commit; the signaling lock serializes mutations | explicit replacement or deletion, grant rotation or revocation, or process exit for memory-mode rooms |
|
||||
| Server route and SFU | `signal` feeds `route` one serial child operation; `sfu/admission.go` accepts one bounded publication | one committed graph per controller and the forwarding publication | superseded assignments retire inside the same operation; deactivate retires the publication |
|
||||
|
||||
## Source Index
|
||||
|
||||
Start a change at the owning entry point, then follow the interfaces listed
|
||||
above rather than searching the tree.
|
||||
|
||||
| Change | Start at |
|
||||
| --- | --- |
|
||||
| Host workflow, share lifecycle, notices | `src/client/pages/HostPage.tsx`, `src/client/pages/host-page-notices.ts` |
|
||||
| Viewer workflow, playback, status projection | `src/client/pages/ViewerPage.tsx`, `src/client/media/viewer-presentation.ts`, `src/client/ui/media-status.ts` |
|
||||
| Browser peer and route mechanics | `src/client/webrtc/host-peer.ts`, `viewer-peer.ts`, `viewer-relay.ts`, `src/client/media/host-sfu-route.ts`, `viewer-sfu-route.ts`, `route-transition.ts` |
|
||||
| Capture, profiles and sender parameters | `src/client/media/quality.ts`, `src/client/media/browser-encoding-pool.ts`, `browser-encoding-output.ts` |
|
||||
| SFU endpoints | `src/client/sfu/publisher.ts`, `subscriber.ts`, `peer.ts`, `internal/server/sfu` |
|
||||
| Localized copy and failure facts | `src/client/locales`, `src/client/ui/copy.ts`, `src/client/ui/media-failure.ts` |
|
||||
| Native bridge, capture and media edges | `src/client/native`, `internal/app/loopback`, `internal/app/nativecontrol`, `nativehost`, `nativeviewer`, `nativecapture` |
|
||||
| Room authority, persistence and graph | `internal/server/signal`, `internal/server/room`, `internal/server/route` |
|
||||
| Shared wire contract | `src/shared/protocol.ts`, `internal/server/protocol`, `tests/fixtures/wire-samples.json` |
|
||||
| Diagnostics and logging | `src/client/lib/debug.ts`, `internal/diagnostics`, [configuration](./configuration.md#diagnostics) |
|
||||
| Configuration and deployment | `internal/server/config`, `internal/app/config`, [configuration](./configuration.md), [deployment](../deployment.md) |
|
||||
|
||||
## Implementation Rules
|
||||
|
||||
- Reuse a current owner, standard API or mature dependency before adding a
|
||||
mechanism. Extract around a responsibility shared by real callers; file size
|
||||
alone does not justify wrappers, a generic manager or a framework.
|
||||
Remove superseded paths in the same change; retain no unused legacy copy.
|
||||
Keep test-only state seeding and setup in test harnesses.
|
||||
Do not export mutable resources solely for tests; preserve the production
|
||||
construction path when verifying defaults or restoration.
|
||||
- Review dependency direction for source/type imports as well as runtime
|
||||
imports. Shared types belong to a common owner. A type-only cycle is an
|
||||
ownership concern; it does not by itself establish a runtime or performance
|
||||
defect.
|
||||
- State has one writer and a clear lifetime. Separate permission, capability,
|
||||
connection readiness, requested settings, applied settings and observation.
|
||||
Derive presentation from facts; do not synchronize parallel booleans or let
|
||||
a cached failure outlive its evidence.
|
||||
- The operation that acquires a connection, clone, listener or queue owns its
|
||||
retirement. After an await/callback, and inside cleanup, validate the original
|
||||
operation/resource identity before changing current state. Cancellation,
|
||||
failure, missing observation and success must retain distinct meanings.
|
||||
- Keep decisions separate from effects. Respect documented lock ordering and
|
||||
revalidate after unlocked work. Local native capability never grants room
|
||||
authority; the shared Go service remains authoritative.
|
||||
Injected callbacks still execute in their caller's context; include their
|
||||
blocking I/O in the caller's lock and latency analysis.
|
||||
- Validate input at the boundary, keep domain error meanings stable, and map
|
||||
them into shared localized UI. [Versioning](./versioning.md) determines which
|
||||
extensions are compatible; an optional field is not automatically compatible.
|
||||
Retained media state carries failure facts; the presentation layer resolves
|
||||
localized copy during render. Keep raw exceptions in diagnostics. Transient
|
||||
event notices may resolve once when the event occurs.
|
||||
Values with a defined canonical form, such as HTTP origins, use one shared
|
||||
normalizer before storage and comparison. Verify the actual producer and
|
||||
consumer together instead of weakening an authorization comparison.
|
||||
- Deployment configuration describes available services; room preferences
|
||||
request allowed behavior. The server enforces the policy even when a client
|
||||
requests more. The UI retains the same controls and explains locked choices.
|
||||
See [configuration](./configuration.md); avoid a second flag with the same job.
|
||||
- Browser/App/Server share [visual language](../design/visual-language.md) and
|
||||
[status projection](../design/media-status.md). A new page, locale or deployment
|
||||
does not invent a new palette, status model or user-role metaphor.
|
||||
- Keep package comments about current responsibilities and invariants. Historical
|
||||
migration paths belong in ADR/research unless they explain a surviving constraint.
|
||||
Support Windows, macOS and Linux; new text files use LF and new filenames use
|
||||
ASCII, with format exceptions in [`.gitattributes`](../../.gitattributes).
|
||||
Scripts use stable executable paths; physical confidence stays explicit in
|
||||
[verification status](../verification-status.md).
|
||||
- Retained tools declare their direct package dependencies. Documented TypeScript
|
||||
tools join the normal type-check entry point; a transitive dependency or a
|
||||
one-off manual check is not an owned toolchain contract.
|
||||
Shipped templates use reserved example values. Checks provision isolated
|
||||
resources or require explicit targets; they must not depend on a maintainer's
|
||||
workstation, private deployment or machine-specific infrastructure names.
|
||||
|
||||
## Ablation And Review
|
||||
|
||||
Judge a change by verified user value against implementation, maintenance,
|
||||
compatibility, runtime and failure cost. After a material module, remove any
|
||||
new state, dependency, branch or abstraction that the accepted behavior does
|
||||
not need. A small performance gain does not justify permanent complexity;
|
||||
do not reduce viewing quality or requested functionality merely to save lines.
|
||||
|
||||
A concrete reduction in duplicated writers, cross-module dependencies or
|
||||
repeated edit sites can justify structural refactoring without a reproduced
|
||||
runtime failure. Name that gain and its regression risk. A completed audit is
|
||||
bounded evidence, not a claim that every current module boundary is optimal.
|
||||
|
||||
Before changing production recovery or state, trace the real request producers
|
||||
and their guards. A test that bypasses disabled controls or writes private state
|
||||
does not alone establish a reachable product defect. Reference scans, including
|
||||
hits from tests and previews, are leads rather than proof of product use or
|
||||
absence. Follow the actual producer-to-consumer path and check the changed
|
||||
boundary; isolated helper tests can miss a mismatch between the two ends.
|
||||
|
||||
Trace acquisition, use, commit and retirement, including replacement and failure.
|
||||
For async ownership, check A starting, B replacing it, then A completing or
|
||||
cleaning up. Where compatibility is promised or being established for public
|
||||
release, check actual old/new readers as well as the happy path. Ordinary private
|
||||
changes do not require support for stale private clients. Run proportionate checks through
|
||||
[the standard entry points](../../CONTRIBUTING.md#verification-entrypoints).
|
||||
This discipline does not authorize an unrelated repository rewrite or a new
|
||||
test suite for every helper.
|
||||
+129
-17
@@ -1,8 +1,116 @@
|
||||
# Naming
|
||||
# Naming And Copy
|
||||
|
||||
This is the naming convention for new and changed Piik code, UI and packaging.
|
||||
Use the brand for people and predictable lowercase identifiers for tools.
|
||||
|
||||
## Voice And Terminology
|
||||
|
||||
Use the same role and action vocabulary in the App, Browser, website and guides.
|
||||
Role labels describe permission; a person's chosen nickname stays unchanged.
|
||||
|
||||
| Meaning | English | Chinese |
|
||||
| --- | --- | --- |
|
||||
| Person sharing and controlling the room | Host | 房主 |
|
||||
| Person watching | Viewer | 观众 |
|
||||
| Product capability | Screen sharing | 屏幕共享 |
|
||||
| Start sending a chosen screen/window | Start sharing | 开始分享 |
|
||||
| Enter another person's room | Join a room | 加入房间 |
|
||||
| Site-wide access secret | Site passphrase | 站点口令 |
|
||||
| Room-specific access secret | Room password | 房间密码 |
|
||||
|
||||
Write each language for its own audience. Chinese headings and welcomes may
|
||||
sound like friends inviting each other: “叫朋友来一起看。” or “沙发给你留着呢。”
|
||||
Do not translate English sentence structure or possessive screen metaphors
|
||||
literally (“和朋友一起看你的屏幕”). Use familiar Chinese verbs and a light touch;
|
||||
forced memes, baby talk and repeated slogans quickly become tiring.
|
||||
|
||||
The brand line is “Share the good stuff.” in English and “来,看点好康的。” in
|
||||
Chinese. Display the Chinese headline on two lines: “来,看点” / “好康的。”
|
||||
Keep metadata and ordinary prose as the continuous sentence.
|
||||
|
||||
Keep the public brand line consistent in the homepage, film ending and README
|
||||
opening, beside a plain screen-sharing description. Scene titles should name
|
||||
the action or benefit; do not turn every section into another slogan. Games, drawings, photos and
|
||||
movie nights show the range of screen sharing without implying voice chat,
|
||||
shared input or synchronized playback controls. Each locale must read naturally
|
||||
as a complete sequence, without depending on a pun from the other language.
|
||||
|
||||
Set the register by the reader's task:
|
||||
|
||||
| Surface | Register |
|
||||
| --- | --- |
|
||||
| Website | Light, everyday language in introductions and illustration captions; keep the current restraint |
|
||||
| README | A friendly introduction, followed by direct explanations of features, limits and first steps |
|
||||
| Documentation index | Clear topic names and short descriptions that help readers choose a guide |
|
||||
| Deployment, configuration and maintenance guides | Formal, precise instructions: prerequisites, actions, expected results and recovery steps |
|
||||
|
||||
Formal means clear and factual, not bureaucratic. Keep detailed procedures free
|
||||
of jokes and promotional slogans; do not carry the website's playful headings
|
||||
into technical instructions.
|
||||
|
||||
State the requirement, action or result directly. Omit negative comparisons that
|
||||
answer an abandoned design or an internal discussion: “解压后运行” is enough;
|
||||
listing the runtimes a packaged App does not require adds no useful next step.
|
||||
Include an exclusion only when it resolves a real reader choice or limitation.
|
||||
|
||||
Write tutorials from an explicit starting point: what the reader needs, which
|
||||
visible action to take, and what appears next. Distinguish watching an invitation,
|
||||
creating an App room, using an existing site, and deploying a server. Say where
|
||||
the site or download comes from; “follow the prompts” cannot stand in for a
|
||||
missing prerequisite. Mark passwords and audio options as conditional when they
|
||||
are optional. Keep App and Server package names distinguishable at download links.
|
||||
Public platform notes say what has been tested and invite useful feedback;
|
||||
internal phrases such as “this phase's acceptance target” belong in status.
|
||||
|
||||
Use progressive disclosure on the website and in the film:
|
||||
|
||||
| Information layer | Reader's question | Content |
|
||||
| --- | --- | --- |
|
||||
| First view: navigation, headings, feature cards, film titles | What does it do, how do I start, what does it cost? | Familiar actions and concrete benefits: “开箱即用”, “免费使用”, “朋友点开就能看” |
|
||||
| Expanded details, supporting text and FAQ answers | Will this work for me, and why? | Platform/network conditions, explained technical terms and links to the relevant guide |
|
||||
| README, wiki and operational guides | How do I configure, verify or troubleshoot it? | Exact action names, prerequisites, commands, configuration and evidence limits |
|
||||
|
||||
The layer follows the reader's task, not font size alone. Keep material costs,
|
||||
availability and limitations beside the claim or action they qualify; do not
|
||||
hide them behind a collapse. Never promote an implementation mechanism such as
|
||||
encoding reuse or STUN as an unexplained headline. Explain technical terms on
|
||||
first use even in detailed documentation. Plain wording must preserve the
|
||||
original bounds: “开箱即用” does not promise offline use or guaranteed connection,
|
||||
and reduced repeated work does not establish a universal CPU saving.
|
||||
Lead performance copy with the user benefit (low latency, efficient sharing),
|
||||
then explain the supporting mechanism and its conditions. “Lowest latency”,
|
||||
“zero delay” and comparative resource savings require matching measurements;
|
||||
an architectural preference alone does not establish a benchmark result.
|
||||
|
||||
Write Chinese and English headings independently. Break lines at complete
|
||||
phrases, never inside a short action such as “画上两笔”. A decorative number must
|
||||
have an adjacent meaning/unit (“使用费用 0 元”), and its label must remain visible
|
||||
in both languages and at the smallest supported width. Check the actual rendered
|
||||
headline after rotation and animation settle: all meaningful words need room
|
||||
inside the frame. Oversized background lettering may crop; primary copy may not.
|
||||
|
||||
Download/availability labels, platform requirements, instructions, permission
|
||||
prompts, status and errors stay plain and specific. Use the exact visible action
|
||||
names in tutorials; a playful caption never replaces “Download”, “Join a room”
|
||||
or a platform/connection limitation. Explain what happened, what the user can do
|
||||
next, and any real limitation. Jokes belong
|
||||
in the welcome or illustration caption. Do not soften a failure into a joke,
|
||||
invent connection progress or promise guaranteed connectivity or absolute privacy.
|
||||
Screen sharing includes games, creative work and showing a useful discovery.
|
||||
|
||||
Operational pages omit introductory sentences that repeat their controls.
|
||||
Helper text should explain a meaningful choice or prevent an error.
|
||||
|
||||
Use short, contextual references in opening lines and illustration reactions.
|
||||
A familiar expression may work when its ordinary meaning still makes sense to
|
||||
someone who does not recognize it. Keep each language independent and avoid
|
||||
long dialogue, borrowed brand slogans or a stream of unrelated memes. Match a
|
||||
celebration to a visible successful action; it must not precede the result.
|
||||
Keep the small bilingual set local, without a quotation service.
|
||||
The [visual language](../design/visual-language.md) owns where these lines appear.
|
||||
|
||||
## Product And Tool Names
|
||||
|
||||
| Surface | Convention | Example |
|
||||
| --- | --- | --- |
|
||||
| Product and repository | `Piik` | `TNTcraftHIM/Piik` |
|
||||
@@ -18,11 +126,9 @@ Use the brand for people and predictable lowercase identifiers for tools.
|
||||
| Environment variables | Uppercase snake case | `PIIK_DEBUG`, `PIIK_LOG_DIR` |
|
||||
| Go module path | Exact repository spelling | `github.com/TNTcraftHIM/Piik` |
|
||||
|
||||
An `.exe` is lowercase on Windows too; `.app` is a user-facing bundle, not the
|
||||
raw executable. There is no universal OS rule requiring lowercase binaries:
|
||||
this project chooses one spelling across platforms for predictable commands.
|
||||
Archive contents and directory layout are owned by the existing packager; a
|
||||
name change does not change that layout.
|
||||
Use the same lowercase binary names across platforms for predictable commands.
|
||||
`Piik App.app` is the user-facing macOS bundle. The packager owns archive contents
|
||||
and directory layout.
|
||||
Keep platform/standard spellings such as `Info.plist`, `LICENSE`, `README.md`
|
||||
and `REVISION`. Do not change third-party names or notices to fit this table.
|
||||
Immutable measurement records keep the paths and hashes of their measured
|
||||
@@ -30,25 +136,31 @@ revision; they are evidence rather than current command examples.
|
||||
|
||||
## Code ownership and vocabulary
|
||||
|
||||
- `internal/app` owns the standalone application and its native/launcher
|
||||
components; `cmd/piik-app` is its thin command entry.
|
||||
- `internal/server` owns the reusable room service and Hosted runtime;
|
||||
`cmd/piik-server` is its command entry. The App runs that same service for
|
||||
Local/public-link rooms, without a second backend implementation.
|
||||
- `src/client` is the Browser frontend. HTTP/WebSocket clients, signaling
|
||||
`clientId`, protocol clients and similar connection roles remain `client`.
|
||||
The [engineering module map](./engineering.md#module-map) owns directory
|
||||
responsibilities. HTTP/WebSocket clients, signaling `clientId`, protocol clients
|
||||
and similar connection roles remain `client`; they do not name the standalone App.
|
||||
|
||||
- Follow each language's semantics: lowercase Go packages, exported `PascalCase`
|
||||
and unexported `camelCase`; existing TypeScript component/type conventions.
|
||||
Brand casing must not change visibility or create unnecessary name prefixes.
|
||||
|
||||
Renaming the standalone product is not a protocol or persistence change.
|
||||
Keep `piik-client-v9`, the discovery service name, Browser activation/storage
|
||||
keys, the `Piik` configuration directory and diagnostic categories at their
|
||||
current exact contracts. Change such identifiers only with a separate behavior
|
||||
or contract decision; do not add aliases or dual readers for display renames.
|
||||
Keep discovery, wire and stored identifiers at their current contracts. Their
|
||||
change rules belong to [versioning](./versioning.md); do not add aliases or dual
|
||||
readers merely for a display rename.
|
||||
|
||||
## Basis
|
||||
|
||||
Content layering reviewed 2026-09-12:
|
||||
[NN/g's progressive disclosure](https://www.nngroup.com/articles/progressive-disclosure/)
|
||||
defers secondary detail while preserving the primary task;
|
||||
[GOV.UK's clear-language guidance](https://guidance.publishing.service.gov.uk/writing-to-gov-uk-standards/writing-guidelines/clear-language/)
|
||||
keeps specialist content understandable. The layer assignments and Chinese
|
||||
examples above are Piik's application of those principles.
|
||||
[NN/g's microcopy guidance](https://www.nngroup.com/articles/3-cs-microcopy/)
|
||||
puts clarity before concision and character; this also guides the balance
|
||||
between Piik's playful introductions and factual instructions.
|
||||
|
||||
Reviewed 2026-09-10. Desktop standards explicitly separate display names from
|
||||
executable identifiers; lowercase executable names are our portability convention.
|
||||
|
||||
|
||||
@@ -0,0 +1,238 @@
|
||||
# Versions And Compatibility
|
||||
|
||||
Reviewed 2026-09-12. Release identity, update comparisons and descriptive metadata
|
||||
rules are implemented; the public compatibility promise starts with the first
|
||||
declared public release. Use `v1.0.0` for it, with one product version for App,
|
||||
Server and the embedded Web build, plus the full Git SHA for source provenance.
|
||||
Automatic publication remains disabled until the readiness boundary below.
|
||||
|
||||
## Give Each Identifier One Job
|
||||
|
||||
| Identifier | Meaning and owner | Change rule |
|
||||
| --- | --- | --- |
|
||||
| `vMAJOR.MINOR.PATCH` | Public Git tag/product release | Patch: compatible fixes; minor: compatible features; major: incompatible public contract |
|
||||
| Full Git SHA / `REVISION` | Exact source revision | Changes with the source commit; never use SHA inequality as release ordering or compatibility proof |
|
||||
| Artifact SHA-256 | Exact packaged bytes in the release descriptor | Each artifact; one source SHA can produce different platform/toolchain binaries |
|
||||
| Platform application version fields | Generated package metadata | Product version in the platform's numeric format; source SHA stays separate. XML/desktop-format versions remain specification values |
|
||||
| Browser/Server signaling identifier | WS contract in `src/shared/protocol.ts` and Go protocol constants | Change for an incompatible wire contract, not UI edits or every release |
|
||||
| Browser/App control identifier | `src/client/native/wire.ts` and App loopback/control | Independent compatibility boundary; installed App and Site can update separately |
|
||||
| Capture probe and encoded-frame envelope | `internal/app/nativecapture` and platform adapters | Internal matched bundle; change only the affected format |
|
||||
| SQLite schema, App config format, Browser storage-key format | Their storage/parser owners | Change with the actual stored representation; preserve accepted authority across a compatible upgrade |
|
||||
| Release descriptor schema | Packager and descriptor readers | Describes metadata format, not the product release |
|
||||
| Room authorization generation, share/route/publication/media generations, connection IDs | Current authority and resource owners | Runtime identity/freshness only; never reset or align them with release numbers |
|
||||
|
||||
Current numeric values are in source and [status](../status.md#accepted-release-contract).
|
||||
Keep those internal values at the first public release: resetting schema or
|
||||
authorization counters for visual consistency can invalidate data or stale-work
|
||||
fences. A room number is a user-facing address, not a software version.
|
||||
The private tooling-only npm package carries no independent product version.
|
||||
|
||||
## Public Compatibility Promise
|
||||
|
||||
The protected surface is documented App/Server startup and configuration, room
|
||||
and invitation behavior, persisted authority, and the Browser/Server/App
|
||||
interfaces needed for established sharing and viewing. Internal Go/TypeScript
|
||||
symbols, capture sidecar details within a matched bundle, and undocumented
|
||||
diagnostic fields are not separately supported public APIs.
|
||||
|
||||
- Within a public major, newer releases preserve established workflows and
|
||||
accepted configuration/data for older public releases in that major. New features
|
||||
require support at the receiving end; they are not sent speculatively.
|
||||
- A Site serves its own Web build, but an already-open page or installed App
|
||||
can be older. Compatibility is checked at their actual contracts, not by
|
||||
requiring identical product versions or SHAs. New App with older Site keeps
|
||||
established compatible functionality; newly added features may be unavailable.
|
||||
- A patch/minor cannot remove a documented command, require a new field from an
|
||||
old peer, change an existing field's meaning, revoke valid room authority, or
|
||||
turn an incompatible HTTP response into a supposedly compatible WS release.
|
||||
Deliberate public breakage needs a major and clear upgrade instructions.
|
||||
- Same-major forward upgrades preserve existing durable authority. Arbitrary
|
||||
binary/database downgrades, cross-major interoperability, process restart
|
||||
without media interruption, and identical performance on every OS/browser
|
||||
are not promised. A security repair may restrict previously unsafe behavior;
|
||||
explain the impact rather than silently redefining compatibility.
|
||||
|
||||
This does not require retaining every historical implementation. Keep one stable
|
||||
baseline contract; introduce a small optional capability only for a concrete
|
||||
new feature. Do not add a version-range solver, general adapter framework,
|
||||
parallel authority writers or blanket old-client fallback paths.
|
||||
|
||||
## Extending Interfaces
|
||||
|
||||
Command/event JSON objects still reject unknown keys and enums, and required
|
||||
fields reject omissions. Therefore even adding a field can break the other side.
|
||||
The former required `sfu` addition to strict `/api/capabilities` demonstrated
|
||||
both directions of this problem; an unchanged signaling identifier does not
|
||||
cover HTTP compatibility.
|
||||
|
||||
Runtime capabilities and App discovery now ignore unknown descriptive fields
|
||||
while validating known types, bounds and required identity. An absent optional
|
||||
capability means unavailable. Commands, credentials and authority checks remain
|
||||
strict. A new
|
||||
message type, enum value or behavior-changing field is emitted only after the
|
||||
other side advertises support; accepting unknown metadata is not permission to
|
||||
execute unknown commands. Verify both readers before calling a change additive.
|
||||
|
||||
Native discovery distinguishes an incompatible Piik App from an absent App and
|
||||
reports protocol numbers to local diagnostics; the source picker offers update/
|
||||
refresh guidance and Browser operation. Do not reload, reinstall or interrupt
|
||||
an active share just because an unrelated product/build label changed.
|
||||
|
||||
## Release And Recovery
|
||||
|
||||
A single release plan computes the next tag and full source SHA. Packagers inject
|
||||
both into binaries, embedded Web/debug metadata and the schema-2 release descriptors.
|
||||
Local candidates use an exact release tag, `development` or an explicit candidate
|
||||
version. An explicit version labels an unpublished test artifact; it does not
|
||||
declare or freeze a public release. It cannot reuse a tag owned by another source
|
||||
revision. Do not maintain independent hand-edited App/Server/Web version files.
|
||||
Archive layout and checks stay in the
|
||||
existing packagers; a new build never replaces an already-published package.
|
||||
|
||||
Update checks compare actual release precedence and link to the matching release.
|
||||
Newer version wins even when the source SHA is unchanged. Same version with a
|
||||
different known SHA reports a different official build, not a newer one. A
|
||||
development build with a known differing SHA offers the official release as a
|
||||
manual choice; it makes no ordering claim. Older releases never cause an update
|
||||
notice. Unknown SHA is not inferred from a branch name in GitHub metadata.
|
||||
The publisher records a full SHA in `target_commitish` and verifies actual tag
|
||||
identity separately. Stable update notices ignore prereleases.
|
||||
|
||||
## Automatic Publication
|
||||
|
||||
After one-time activation, a complete squash PR merged to main runs the existing
|
||||
CI and packaging pipeline. `scripts/release-version.mjs` examines unreleased
|
||||
first-parent commit messages: `feat` selects minor, `!` or `BREAKING CHANGE`
|
||||
selects major, and other accepted changes select patch. The first release is
|
||||
`v1.0.0`. PR/commit naming is owned by [CONTRIBUTING](../../CONTRIBUTING.md#commit-pr-and-branch-names).
|
||||
Code size or an internal refactor does not itself imply a breaking release.
|
||||
|
||||
No version/changelog commit is written back to main. The workflow queues main
|
||||
runs, uses the same source SHA for Server and all App targets, and publishes only
|
||||
after all required artifacts and checks pass. The publisher verifies matching
|
||||
version, source SHA and checksums, then creates/uploads/publishes one GitHub draft.
|
||||
Failed draft uploads can resume; published artifacts are never overwritten.
|
||||
An older draft retried after a newer release cannot take over `latest`.
|
||||
|
||||
`PIIK_RELEASES_ENABLED` is a GitHub repository activation variable, not a product
|
||||
setting. Until the first release is explicitly enabled,
|
||||
main runs validation and manual `client_checks` dispatches create candidates only.
|
||||
Ordinary branches and PRs do not run cloud CI. [GitHub operations](../operations/github.md)
|
||||
owns activation and main protection; no workflow changes repository visibility.
|
||||
|
||||
Updates remain explicit, outside an active share, using complete matching
|
||||
packages. [Deployment](../deployment.md) owns artifact verification, cutover and
|
||||
recovery. A changed storage format needs its own upgrade and rollback treatment;
|
||||
restoring an old authority database after newer grants/revocations is not a safe
|
||||
routine downgrade. No automatic updater or generic migration framework is
|
||||
introduced by this policy.
|
||||
|
||||
## Release Notes
|
||||
|
||||
GitHub Releases owns the published changelog. Each accepted PR supplies reviewed
|
||||
Chinese and English user-facing copy under the exact `## Release notes` heading
|
||||
in its squash commit; `###` headings structure that copy. Explain benefits,
|
||||
fixes and required upgrade actions, including breaking changes. Keep internal
|
||||
work logs, audit handoffs and unverified claims outside this public section.
|
||||
|
||||
`scripts/release-notes.mjs` collects these sections from all unreleased
|
||||
first-parent commits since the previous stable tag. The first release uses
|
||||
only its launch commit's product introduction; private development history is
|
||||
not a launch changelog. Missing, empty or duplicate sections fail validation
|
||||
before packaging/publication. GitHub Actions shows the generated text in its
|
||||
run summary. A manual candidate with an explicit version also previews it, so
|
||||
prepare its commit text before dispatching a release rehearsal.
|
||||
|
||||
The publisher passes the same text through a temporary notes file to GitHub,
|
||||
then removes that file. Gitee copies the GitHub description. Retrying the same
|
||||
release preserves its draft/published description; an exact tag does not move
|
||||
the changelog's starting point. Keep no `CHANGELOG.md`, versioned note directory
|
||||
or generated history commits. PRs and Git retain editorial provenance.
|
||||
|
||||
## Release Sources
|
||||
|
||||
GitHub Releases is the primary publication; Gitee is an optional official mirror
|
||||
for domestic access, with a minimal README pointing development and documentation
|
||||
back to GitHub. The Gitee repository need not copy the source tree. Its README/tag
|
||||
commit is not the Piik source revision; release metadata retains the original
|
||||
GitHub SHA. Build each target once and copy the same archives, notices,
|
||||
version, full source SHA and artifact hashes to both. A Git repository mirror
|
||||
alone does not synchronize Release attachments. Keep one release description and
|
||||
machine-readable metadata, generated by the existing packaging pipeline.
|
||||
|
||||
The two providers only adapt API fields and download locations into the same
|
||||
release model. Share version comparison, channel filtering and package validation.
|
||||
The normal background check tries GitHub, then Gitee if the primary is unavailable;
|
||||
download links may expose both verified locations. A lagging mirror must not
|
||||
cause a downgrade or a version/hash disagreement to be silently accepted.
|
||||
Gitee's documented latest endpoint means last updated, so provider normalization
|
||||
must select stable release precedence explicitly, not equate provider ordering.
|
||||
|
||||
Synchronize only an explicitly published release; branch pushes stay quiet.
|
||||
Retrying a failed mirror upload reuses the original package. Advertise a mirrored
|
||||
package as available only after its required attachments and matching metadata
|
||||
are present. Tokens belong only to publishing, never the distributed Browser/App.
|
||||
The public mirror is [TNTcraftHIM/Piik](https://gitee.com/TNTcraftHIM/Piik).
|
||||
The publisher reuses the shared artifact validator, checks every local file
|
||||
against GitHub's published asset digest, then uploads and verifies anonymous
|
||||
Gitee downloads. Gitee has no draft API: an incomplete mirror remains a preview
|
||||
release, and becomes stable only after every file passes. A source marker in
|
||||
the release body (`<!-- piik-source: <full-SHA> -->`) preserves the GitHub revision;
|
||||
the mirror's `target_commitish` identifies only its own README commit.
|
||||
App notices and the operator check share their existing version comparison
|
||||
across providers. Each provider has its own deadline; the mirror checks at most
|
||||
ten pages of 100 releases and rejects an incomplete listing. An explicit operator
|
||||
`--api-url` override checks that endpoint alone. A valid primary result, including
|
||||
an up-to-date result, does not consult the mirror.
|
||||
|
||||
Gitee currently documents 100 MB per attachment and 1 GB total repository
|
||||
attachments for ordinary projects. The mirror rejects files above 100,000,000
|
||||
bytes before writing. Check aggregate capacity before each release; exhausted
|
||||
storage leaves a pending preview rather than deleting older downloads.
|
||||
Target authentication, uploads, anonymous metadata/CORS and a small download's
|
||||
SHA-256 have been verified. Acceptance of the full matching release packages
|
||||
remains pending with the first public release.
|
||||
Use API metadata in the Browser; ordinary asset-link redirects are not guaranteed
|
||||
to permit cross-origin fetch, even when a normal download works.
|
||||
|
||||
## First Public Release Readiness
|
||||
|
||||
The current private single-contract rule remains until this boundary is ready:
|
||||
|
||||
1. Accept real target packages and the queued GitHub publication path before
|
||||
enabling automatic releases. Current local checks exercise planning, identity
|
||||
injection, update decisions and publisher failure/retry paths without publishing.
|
||||
2. Freeze the first public baseline and explicit feature-support rules after
|
||||
checking the implemented discovery and descriptive-metadata behavior above.
|
||||
3. Exercise the first supported old/new App-Site pair in both directions,
|
||||
a stale open page, incompatible-contract recovery and retained room/config
|
||||
authority. Bundled capture checks remain within their existing package gate.
|
||||
4. Freeze the supported baseline and release notes, then explicitly declare
|
||||
`v1.0.0`. Do not manufacture compatibility by merely renumbering private builds.
|
||||
|
||||
These implementation tasks are tracked in [TODO](../todo.md). Their absence is
|
||||
a public-release gap, not evidence that the current matched private build fails.
|
||||
|
||||
## References
|
||||
|
||||
- [SemVer 2.0.0](https://semver.org/spec/v2.0.0.html): release meaning follows a
|
||||
declared public API; build metadata does not determine precedence.
|
||||
- [Syncthing releases](https://docs.syncthing.net/users/releases.html) and
|
||||
[BEP](https://docs.syncthing.net/specs/bep-v1.html): product versions, peer
|
||||
protocol and storage/upgrade behavior are distinct. Piik does not adopt its
|
||||
minor-release REST-breaking exception for the protected interfaces above.
|
||||
- [Go compatibility](https://go.dev/doc/go1compat): name the protected contract
|
||||
and its limits instead of promising universal binary/runtime compatibility.
|
||||
- [Debug Adapter Protocol evolution](https://microsoft.github.io/debug-adapter-protocol/overview.html):
|
||||
stable baseline messages and explicit optional capabilities allow independent
|
||||
endpoints to evolve. Piik uses that principle within its existing WebSocket/JSON
|
||||
contracts; it does not replace them with DAP or a generic negotiation framework.
|
||||
- [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) provides
|
||||
the small release-intent vocabulary; standard SemVer libraries perform ordering.
|
||||
- Apple's [bundle build version](https://developer.apple.com/documentation/bundleresources/information-property-list/cfbundleversion)
|
||||
and [release version](https://developer.apple.com/documentation/bundleresources/information-property-list/cfbundleshortversionstring)
|
||||
are numeric; package generation derives both from the release and keeps SHA separately.
|
||||
- [GitHub Releases API](https://docs.github.com/en/rest/releases/releases) and
|
||||
[Gitee's official API SDK reference](https://gitee.com/sdk/gitee5j/blob/main/docs/RepositoriesApi.md):
|
||||
provider endpoints and metadata. [Gitee Release documentation](https://help.gitee.com/repository/release/intro)
|
||||
owns its current attachment limits. Provider checks were made on 2026-09-12.
|
||||
@@ -18,12 +18,6 @@ cross-links, and mechanical freshness/structure checks. Codex concatenates the
|
||||
applicable project instruction chain until `project_doc_max_bytes`, whose
|
||||
default is 32 KiB.
|
||||
|
||||
Anthropic likewise recommends concise, specific project instructions and moves
|
||||
multi-step procedures or path-specific rules to on-demand skills or scoped rule
|
||||
files. Claude Code's stated target is under 200 lines per `CLAUDE.md`; imported
|
||||
files still enter the startup context, so an import is organization rather than
|
||||
context reduction.
|
||||
|
||||
Hermes distinguishes project conventions (`AGENTS.md`) from user preference,
|
||||
personality, and learned memory. Its persistent memory is deliberately bounded
|
||||
and curated; full session history remains searchable on demand rather than being
|
||||
@@ -34,10 +28,8 @@ below that value and using nested context for genuinely distinct subtrees. Its
|
||||
bounded-memory guidance starts consolidation at 80% rather than treating the
|
||||
hard limit as the normal operating target.
|
||||
|
||||
Piik therefore warns at the published recommendation or 80% of a loader
|
||||
limit, then fails only at a real loading boundary or a small explicit buffer:
|
||||
32 KiB for the Codex chain, 20,000 characters for Hermes, and 250 effective
|
||||
Claude lines above the 200-line recommendation. The same envelope applies to the
|
||||
Piik therefore warns at 80% of a loader limit, then fails at the loading boundary:
|
||||
32 KiB for the Codex chain and 20,000 characters for Hermes. The same envelope applies to the
|
||||
repo Ponytail skill because `AGENTS.md` requires it for coding work. A failure tells the
|
||||
maintainer to use nested/path-scoped context, on-demand skills or linked docs,
|
||||
remove duplication and completed history, and delete stale conclusions; raising
|
||||
@@ -47,7 +39,6 @@ Sources:
|
||||
|
||||
- [OpenAI Harness engineering](https://openai.com/index/harness-engineering/)
|
||||
- [OpenAI Codex `AGENTS.md`](https://learn.chatgpt.com/docs/agent-configuration/agents-md)
|
||||
- [Claude Code memory](https://code.claude.com/docs/en/memory)
|
||||
- [Hermes file ownership](https://github.com/NousResearch/hermes-agent/blob/main/website/docs/user-guide/which-file-does-what.md)
|
||||
- [Hermes context files](https://github.com/NousResearch/hermes-agent/blob/main/website/docs/user-guide/features/context-files.md)
|
||||
- [Hermes persistent memory](https://github.com/NousResearch/hermes-agent/blob/main/website/docs/user-guide/features/memory.md)
|
||||
|
||||
@@ -1,106 +0,0 @@
|
||||
# Backend Audit Reconciliation
|
||||
|
||||
- Reviewed: 2026-09-06
|
||||
- External evidence baseline: `1b01048e558cfa69b2c8216e658374d06037bfe4`.
|
||||
- Integration baseline: `d3232cfd2929eafc94a0e3f003cd2bcdcd47089c`.
|
||||
- Scope: verified lifecycle fixes and removal of the optional Host-star route.
|
||||
This is not the Go server rewrite or a new physical-media acceptance claim.
|
||||
|
||||
The external package contains 244 raw claims, not 244 confirmed defects.
|
||||
Its handoff, source patch, and machine-readable findings remain external
|
||||
evidence. This document records the independent reconciliation; the
|
||||
[TODO ledger](../todo.md) alone owns remaining work.
|
||||
|
||||
## Accepted Corrections
|
||||
|
||||
- A quality candidate rejected before preparation consumes its existing
|
||||
opportunity, preventing an identical prepare/deny loop.
|
||||
- Disposing a Native media bridge settles its pending start. Native relay
|
||||
callbacks bind the actual peer, including prepared children.
|
||||
- A failed Native pause remains paused locally; a failed resume returns to the
|
||||
paused intent. No new pause protocol or parallel state owner is introduced.
|
||||
- Prepared Native receive deadlines follow the server operation. Viewer teardown
|
||||
retires pending candidates and their recovery timers; terminal signaling
|
||||
teardown uses its existing stop owner.
|
||||
- Authentication is an authoritative route snapshot, even when a replacement
|
||||
controller restarts revision numbering. Ordinary route updates retain their
|
||||
monotonic fence; session and media identities still reject stale callbacks.
|
||||
- Only explicit `committed: true` publishes a newly committed connection
|
||||
identity. Stop-sharing presence is emitted after graph teardown.
|
||||
- Room expiry and explicit abandonment share termination cleanup. Shutdown
|
||||
closes Vite before its shared listener and attempts every SFU drain even when
|
||||
one fails. Hosted shutdown reports failure explicitly.
|
||||
- Native Receiver creation releases its unused GCC observer-map entry. Local
|
||||
startup rejects an already occupied health port; this is a duplicate-instance
|
||||
guard, not atomic socket handoff or cryptographic process identity.
|
||||
- Native control keeps one reader, a bounded queue, and serial handling so socket
|
||||
close can cancel an outstanding system-picker operation. Explicit share close
|
||||
is a clean end; natural capture-process EOF still needs cause attribution.
|
||||
- Gates use current loopback constants and explicit test STUN configuration.
|
||||
Packagers reject in-repository output. Deployment templates specify production
|
||||
mode and failed cutover cleanup removes its temporary links.
|
||||
- Hygiene skips only GitHub's all-zero base SHA; a missing nonzero commit remains
|
||||
an error. Uncalled exports and obsolete gate aliases are removed.
|
||||
|
||||
## Resolved Decisions
|
||||
|
||||
The owner removed `PEER_ASSISTED_MEDIA`. Hosted and Local now use the same
|
||||
bounded graph controller; absent LiveKit configuration still means P2P-only
|
||||
service. Room privacy and topology-optimization controls remain independent.
|
||||
The old environment variable must be removed before the next deployment.
|
||||
There is no Host-star compatibility path to port. Removing the unused
|
||||
authentication fields required the coordinated private signaling v21 release;
|
||||
rollback to the prior server must also restore its environment configuration.
|
||||
|
||||
[ADR-0009](../adr/0009-optional-nat-prediction.md) distinguishes server-owned
|
||||
three-attempt NAT acquisition from active Browser edge recovery. Active recovery
|
||||
first restarts ICE, then rebuilds the connection if needed. Uncommitted candidates
|
||||
return failure to their route operation; the unreachable extra local restart
|
||||
branch was removed. Neither kind of restart guarantees a new NAT mapping.
|
||||
|
||||
The App launch marker remains origin activation, not proof that the process
|
||||
is alive. One failed discovery can mean contention or a permission delay, so
|
||||
automatically deleting the marker would break the accepted saved-Site workflow.
|
||||
Browser capture remains available in the existing picker.
|
||||
|
||||
## Claims Not Adopted Wholesale
|
||||
|
||||
- Catching every SQLite exception and continuing is not a complete recovery
|
||||
strategy: failed disconnect persistence can leave an active room indefinitely.
|
||||
Keep failure observable; design storage failure handling with its authority,
|
||||
rather than adding scattered catch-and-continue guards.
|
||||
- A Local public invitation URL does not identify the transport used by a LAN
|
||||
visitor. Cookie issuance must support both HTTP LAN and HTTPS public access;
|
||||
globally dropping Secure or trusting arbitrary forwarded headers is not an
|
||||
acceptable shortcut.
|
||||
- Audio capture can end silently while video continues. Ending healthy video is
|
||||
not accepted as the fix; explicit audio availability/recovery needs a coherent
|
||||
media contract. Native prepared-bridge fallback likewise needs a shared
|
||||
session-level failure owner, not a per-candidate flag that is immediately lost.
|
||||
- Registering Receivers with Engine, deduplicating media ledgers, splitting page
|
||||
owners, and replacing full graph snapshots are refactor candidates, not proven
|
||||
present-day leaks or measured performance wins.
|
||||
- No replacement media pipeline, custom scoring, periodic retries, TURN, IDL
|
||||
framework, compatibility readers, or new revision namespace was accepted.
|
||||
|
||||
## Refactor Direction
|
||||
|
||||
The [consolidation research](./server-consolidation.md) owns the shared Go module
|
||||
layout that replaced the single TypeScript backend. Hosted and Local preserve
|
||||
one graph/operation owner, with the Node runtime and superseded server packaging
|
||||
removed in that same boundary.
|
||||
Browser UI and native capture adapters are reuse boundaries, not rewrite targets.
|
||||
|
||||
Use the current behavioral tests as an oracle, not a requirement to preserve
|
||||
private class structure. Extract cohesive packages without duplicating mutable
|
||||
state; neither a file split nor one interface per old file is inherently an
|
||||
improvement. Measure package size, startup, memory, and contract parity.
|
||||
|
||||
## Sources
|
||||
|
||||
- [WebRTC restartICE](https://www.w3.org/TR/webrtc/#dom-rtcpeerconnection-restartice)
|
||||
defines ICE restart, not a promise of different network mappings.
|
||||
- [coder/websocket](https://pkg.go.dev/github.com/coder/websocket)
|
||||
documents the connection read and cancellation contract.
|
||||
- [Go module layout](https://go.dev/doc/modules/layout)
|
||||
supports multiple commands and shared internal packages.
|
||||
@@ -71,6 +71,13 @@ A local page completed discovery and the full control exchange in Chromium
|
||||
its `loopback-network` permission; without that permission the cross-origin
|
||||
request was blocked. The gate checks both expected outcomes.
|
||||
|
||||
Run `npm run probe:client-loopback` with `PIIK_CLIENT_LOOPBACK_GATE=true`,
|
||||
`CHROME_PATH`, `PIIK_CLIENT_EXE`, and `PIIK_CLIENT_GATE_SITE_URL` pointing to
|
||||
the reachable HTTPS Site being tested. Use `PIIK_CLIENT_GATE_GRANT_LOOPBACK=true`
|
||||
for the permission-granted run; omit it for the permission-blocked run. The gate
|
||||
uses an isolated App configuration and Browser profile, and rejects navigation
|
||||
failures or redirects to another origin as acceptance evidence.
|
||||
|
||||
A self-contained LAN room uses a localhost Host URL and a selected LAN IPv4
|
||||
Viewer URL backed by the same local App process. The App persists an
|
||||
optional user-chosen access password and passes it only to its Host page in a
|
||||
|
||||
@@ -83,8 +83,8 @@ For the permanent-room revision, an offline `DROP COLUMN` transaction on a
|
||||
copy of the prior synthetic database preserved both active and formerly expired
|
||||
rooms. All six remaining authority fields matched exactly; the source backup
|
||||
was unchanged. The current reader accepted the converted schema without a
|
||||
compatibility path. The [deployment procedure](../deployment.md#permanent-room-schema-cutover)
|
||||
owns the operational steps and recovery boundary.
|
||||
compatibility path. The completed cutover is retained in Git; current
|
||||
[deployment recovery](../deployment.md#recovery) owns the operational boundary.
|
||||
|
||||
## Acceptance Matrix
|
||||
|
||||
|
||||
@@ -1,141 +0,0 @@
|
||||
# Piik rename
|
||||
|
||||
Status: accepted rename and coordinated cutover plan.
|
||||
Last reviewed: 2026-09-10.
|
||||
|
||||
## Accepted identity
|
||||
|
||||
Display `Piik`; use `piik` for executable, package, environment and service
|
||||
identifiers. GitHub is `TNTcraftHIM/Piik`; the Go module follows that exact case.
|
||||
The [naming convention](../reference/naming.md) owns current App display and package names.
|
||||
The product keeps its TV mascot. The wordmark and morph animation remain in the
|
||||
[brand study](../design/piik-brand.html) for the future public website.
|
||||
|
||||
This is a private pre-release rename. It changes names and deployment identity,
|
||||
not media policy, routing, capture behavior or SQLite room semantics.
|
||||
|
||||
## Recovery and local metadata
|
||||
|
||||
The external `Piik-recovery/rename-20260910` directory contains a verified
|
||||
`repository.bundle` and a complete `git-metadata` copy. The latter preserves
|
||||
reflogs and all ten stash entries, which a bundle of ordinary refs alone does
|
||||
not capture. Earlier recovery bundles also remain outside the repository.
|
||||
Backups keep their original names and contents so they remain identifiable.
|
||||
|
||||
The main directory is `Piik`; preparation worktrees are `Piik-appearance` and
|
||||
`Piik-rename`, with `Piik-external-audit` retained separately. Their private Git
|
||||
registration names, backlinks and shared remote are updated. The hook path is
|
||||
relative `.githooks`. Retire integrated preparation worktrees after cutover.
|
||||
The audit worktree's branch and source revision remain untouched. Integration
|
||||
keeps one complete phase commit on main.
|
||||
|
||||
On Windows, a terminal can hold the repository directory open. Move the entries
|
||||
into an empty verified destination and run `git worktree repair` from the new
|
||||
main directory. Do not terminate the user's terminal to release a path.
|
||||
Existing firewall rules name executable paths, so moved build binaries require
|
||||
their corresponding path rules to be reviewed before network acceptance.
|
||||
|
||||
## Runtime and data boundary
|
||||
|
||||
`piik-v23` and `piik-client-v9` are new exact protocol labels. Their message
|
||||
schemas retain versions 23 and 9; the full label, not only its numeric suffix,
|
||||
is checked. Native capture remains v7. Release Web, Server, App and capture
|
||||
artifacts together; old pages and installed Apps cannot interoperate across
|
||||
the renamed control boundary. No alternate protocol reader is introduced.
|
||||
|
||||
SQLite schema 2, its application ID, room codes, token/grant digests and password
|
||||
material remain unchanged. The four-byte database file identity is a format
|
||||
constant, not display branding. Copy the database only while the old process is
|
||||
stopped, verify integrity and retained columns, and preserve the original for
|
||||
rollback. Do not recreate rooms or repeat the earlier schema-1 cutover.
|
||||
|
||||
The implementation renames Browser storage prefixes, the site-access cookie
|
||||
and the App configuration/cache directory. The owner accepts a clean break:
|
||||
copy directly compatible, accessible settings once during cutover; discard
|
||||
unavailable or incompatible settings. The product reads only Piik names, with
|
||||
no migration code, alternate reader or old-name alias.
|
||||
|
||||
The local App configuration is unchanged JSON version 1 and can be copied
|
||||
byte-for-byte into the Piik directory without replacing an existing new config.
|
||||
Browser credentials and settings are origin/session-owned. Transfer them only
|
||||
through an available original Browser session; do not modify a live Browser's
|
||||
storage database or introduce a product bridge to recover them. Otherwise the
|
||||
new namespace starts empty, requiring login, room creation and preference setup.
|
||||
Old room authority remains in SQLite, but an unavailable Host credential cannot
|
||||
be reconstructed from its stored digest. Site-access cookies and remembered
|
||||
App activation are renewed normally under the new contract.
|
||||
|
||||
## Git history
|
||||
|
||||
The owner authorized replacing the old name in Git history. Use the maintained
|
||||
`git-filter-repo` tool in a fresh, separate bare clone, never in the working
|
||||
repository. Scope this to commit/tag **messages**: historical source snapshots,
|
||||
authorship, dates and topology remain factual. Current tracked prose and code
|
||||
use the accepted identity; old snapshots remain recoverable at their mapped
|
||||
commits and in the original archive.
|
||||
|
||||
The initial inventory found two matching commit bodies and no matching commit
|
||||
subjects or tag names. A small text change still changes descendant SHAs and
|
||||
invalidates their commit signatures. Preserve `commit-map` and `ref-map`; verify
|
||||
equal commit counts, identical source trees, mapped parents, authors and dates,
|
||||
no dropped commits and a clean repository integrity check. No replacement refs,
|
||||
duplicate compatibility branches or history-rewriting tool enter the product.
|
||||
|
||||
The rehearsal verified all 546 reachable branch/tag commits: exactly two messages
|
||||
changed, all file trees and mapped parent relationships match, and authors/dates
|
||||
are preserved. Standard filtering rewrote 545 IDs and removed 374 signatures.
|
||||
Those signatures remain verifiable only in the original archive; a rewritten
|
||||
commit must not be presented as preserving its earlier GitHub verification.
|
||||
|
||||
Apply a reviewed message rewrite only with the coordinated brand integration,
|
||||
using explicit ref targets and old-SHA leases. Do not use `push --mirror` from
|
||||
the rehearsal: it could overwrite unrelated refs. Preserve the external audit
|
||||
branch and stash history in the original repository. Old audit/deployment SHAs
|
||||
remain evidence identifiers, interpreted through the archive and commit map.
|
||||
|
||||
## Operational cutover
|
||||
|
||||
The following old identifiers are retained here only as operator recovery
|
||||
inputs, not as runtime aliases:
|
||||
|
||||
| Surface | Before | After |
|
||||
| --- | --- | --- |
|
||||
| Service, user and group | `screener` | `piik` |
|
||||
| Installation root | `/opt/screener` | `/opt/piik` |
|
||||
| Environment file | `/etc/screener/screener.env` | `/etc/piik/piik.env` |
|
||||
| Branded environment prefix | `SCREENER_` | `PIIK_` |
|
||||
| Browser storage prefix | `screener:` | `piik:` |
|
||||
| App config directory | `Screener` | `Piik` |
|
||||
|
||||
Prepare the new immutable artifacts, service/environment/proxy configuration,
|
||||
permissions and database path before stopping the old service. Record original
|
||||
configuration and ownership for recovery. Do not run two room authorities on
|
||||
the same database or two media services on the same UDP ports.
|
||||
|
||||
The routine `deploy/release-app.sh` assumes an existing **Piik** installation.
|
||||
It is not the first brand migration tool: its old-release checks and recovery
|
||||
target the Piik service/binary. Perform the first name cutover as one explicit
|
||||
infrastructure operation, then use the ordinary wrapper for future releases.
|
||||
On failure, stop the candidate and restore the old binary, service, environment,
|
||||
proxy and data together before reopening ingress.
|
||||
|
||||
GitHub is already renamed and the remote works. There are no published releases
|
||||
or Pages site to migrate. Keep repository visibility private; `piik.tv` DNS and
|
||||
Pages publication remain a separate website task. Old GitHub deployment/PR
|
||||
records describe earlier operations and are not rewritten as new releases.
|
||||
|
||||
## Validation and references
|
||||
|
||||
Acceptance covers the tracked-name inventory, Web type/build checks, relevant
|
||||
contract/storage tests, native compilation, App/Server packaging and a
|
||||
controlled local launch. Production release additionally requires the accepted
|
||||
best-effort settings transfer, configuration/database recovery and postflight.
|
||||
|
||||
Primary references, checked 2026-09-10:
|
||||
|
||||
- [Git worktree repair](https://git-scm.com/docs/git-worktree): repair both
|
||||
directions after moving the main repository or linked worktrees.
|
||||
- [git-filter-repo](https://github.com/newren/git-filter-repo/blob/main/Documentation/git-filter-repo.txt):
|
||||
message replacement, fresh clones and old/new commit maps.
|
||||
- [GitHub repository rename](https://docs.github.com/en/repositories/creating-and-managing-repositories/renaming-a-repository):
|
||||
ordinary URL redirects do not cover repository-hosted Actions or project Pages.
|
||||
@@ -32,9 +32,8 @@ remove.
|
||||
and exact direct/peer-relayed evidence sources.
|
||||
|
||||
These are structural simplifications, not reproduced functional failures.
|
||||
The [audit reconciliation](./backend-audit-1b01048.md) separates verified fixes
|
||||
from disputed recommendations. Its external staged plan is not an instruction
|
||||
to perform extractions for their own sake.
|
||||
The [engineering reference](../reference/engineering.md#ablation-and-review)
|
||||
requires a concrete ownership gain before further extraction.
|
||||
|
||||
## Product Choice
|
||||
|
||||
|
||||
+31
-14
@@ -1,6 +1,6 @@
|
||||
# Current Status
|
||||
|
||||
Last updated: 2026-09-10
|
||||
Last updated: 2026-09-12
|
||||
|
||||
This is the compact execution/deployment index. Product modules own behavior,
|
||||
[verification status](./verification-status.md) owns unresolved physical limits,
|
||||
@@ -8,9 +8,9 @@ and Git/PRs own completed history.
|
||||
|
||||
## Accepted Release Contract
|
||||
|
||||
- Browser/server v23, Native control v9 and capture v7 form one private contract.
|
||||
The release requires matching Web/App/Server artifacts and a reload of old
|
||||
active pages at cutover.
|
||||
- Browser/server v23, Native control v9 and capture v7 form the current contract.
|
||||
The coordinated brand/protocol cutover requires matching Web/App/Server
|
||||
artifacts and a reload of incompatible active pages.
|
||||
- Hosted Server defaults to SQLite schema 2. Room authority has no inactivity
|
||||
expiry; explicit replacement/deletion or grant rotation/revocation ends the
|
||||
corresponding authority. Explicit memory mode and App Local end rooms at
|
||||
@@ -24,23 +24,40 @@ and Git/PRs own completed history.
|
||||
## Deployment
|
||||
|
||||
Source, package identity, GitHub repository and local worktree paths use Piik.
|
||||
The strict wire labels and stored keys move together at the coordinated
|
||||
[brand cutover](./research/piik-rename-plan.md). Directly compatible, accessible
|
||||
settings are transferred once; unavailable settings start fresh. Runtime code
|
||||
keeps only the Piik contract.
|
||||
Runtime code keeps only the Piik wire labels and stored keys. The coordinated
|
||||
private cutover requires matching App/Server/Web packages; unavailable old
|
||||
Browser credentials cannot be reconstructed from stored digests.
|
||||
The immutable release descriptor, runtime `REVISION` and operator deployment
|
||||
record own exact identity and postflight results. The
|
||||
[schema cutover](./deployment.md#permanent-room-schema-cutover) preserved existing
|
||||
room authority and Browser credential keys. Old active pages reload at the
|
||||
signaling v23 boundary.
|
||||
record own exact identity and postflight results. Old active pages reload at
|
||||
the signaling v23 boundary.
|
||||
|
||||
The existing deployment uses one Go process for Web, room authority/signaling,
|
||||
Binding-only STUN on UDP 3478/3479/3480 and optional SFU on UDP 7882. nginx owns
|
||||
HTTPS; external LiveKit/coturn services remain disabled. Participants, routes and
|
||||
media remain process-only. This private service is not a public demonstration.
|
||||
|
||||
[TODO](./todo.md) owns rename acceptance, distribution and ablation work.
|
||||
The public website is a later phase.
|
||||
The separate US [public demo](https://demo.piik.tv) is deployed with open site
|
||||
entry, persistent room authority and P2P-only media. Caddy owns HTTPS, and the
|
||||
standard systemd service runs the verified Server test candidate. Public DNS,
|
||||
HTTPS, runtime assets, all three STUN listeners and a bounded browser P2P
|
||||
sharing/stop check passed; the browser check used synthetic video on one network.
|
||||
The operator record owns exact identity and postflight evidence.
|
||||
|
||||
[TODO](./todo.md) owns remaining publication work. The owner accepted the public
|
||||
introduction and authorized launch on 2026-09-12. The source repository is public,
|
||||
main protection is active, and GitHub Pages is configured for `piik.tv`. The
|
||||
private service remains a separate deployment. Release descriptors and GitHub
|
||||
deployment records identify published artifacts and website revisions.
|
||||
|
||||
The Windows test archive passed complete anonymous Gitee download, checksum,
|
||||
App entry/presentation and update-link acceptance. Bounded two-build
|
||||
interoperability also passed. The complete publisher rehearsal previously hit
|
||||
the private repository's Actions artifact quota and must complete on the public
|
||||
repository before package publication. GitHub/Gitee release comparison, mirror
|
||||
publishing and PR-sourced release notes are implemented under
|
||||
[versioning](./reference/versioning.md). [Verification status](./verification-status.md#candidate-evidence-boundary)
|
||||
owns the remaining physical limits; shared lessons live in
|
||||
[engineering](./reference/engineering.md).
|
||||
|
||||
## Media Evidence And Limits
|
||||
|
||||
|
||||
+79
-111
@@ -1,95 +1,48 @@
|
||||
# Current TODO Ledger
|
||||
|
||||
Last reviewed: 2026-09-10
|
||||
Last reviewed: 2026-09-12
|
||||
|
||||
Only **Now** is executable. Product modules own behavior, research owns evidence,
|
||||
and Git/PRs own completed history. A parked idea is not implementation authority.
|
||||
Only **Now** is executable. Product modules own behavior; Git/PRs own completed
|
||||
history. A parked idea is not implementation authority.
|
||||
|
||||
## Now
|
||||
|
||||
After the accepted App naming release, draft the English/Chinese README,
|
||||
onboarding and GitHub Pages website as one coherent public introduction.
|
||||
Use the [naming convention](./reference/naming.md), the existing mascot and
|
||||
the accepted website-only brand animation. Preview locally before publication;
|
||||
keep private deployment details out of the public site. Preserve the rename
|
||||
recovery archive and external-audit worktree. Release metadata owns deployed identity.
|
||||
- [ ] **Release and repository readiness.** Review the prepared Windows test
|
||||
candidate. Run native CI on the now-public repository, then
|
||||
rehearse the publisher with genuine matching Server and App packages. Old cloud
|
||||
artifacts have already been removed; if quota rejection persists, inspect
|
||||
account billing and Packages usage with credentials that can read those scopes.
|
||||
Run the final source's required regression checks, prepare one coherent squash
|
||||
PR, accept the [first public release](./reference/versioning.md#first-public-release-readiness),
|
||||
and finish [publishing activation](./operations/github.md).
|
||||
Test-candidate checks do not accept the public release. Keep the owner's
|
||||
[device/network deferrals](./verification-status.md#candidate-evidence-boundary).
|
||||
- [ ] **Gitee package acceptance.** The public README mirror, publisher secret,
|
||||
upload pipeline and App/Server update fallback are implemented. The complete
|
||||
Windows test archive passed anonymous download, checksum and App entry
|
||||
acceptance. Verify the complete matching Server and three-platform
|
||||
App release, total attachment capacity and real update/download flow
|
||||
under the [release model](./reference/versioning.md#release-sources) before launch.
|
||||
- [ ] **Website launch.** Publish the accepted website through GitHub Pages
|
||||
and verify piik.tv under
|
||||
[website operations](./operations/website.md). Verify all public destinations,
|
||||
including the deployed password-free, P2P-only demo.piik.tv;
|
||||
the current private production service remains private.
|
||||
|
||||
## Next — Awaiting Owner Direction
|
||||
Keep implementation on the existing phase branch. Main and the private service stay
|
||||
unchanged until the related acceptance and delivery actions are complete.
|
||||
|
||||
The owner owns **piik.tv**.
|
||||
Review deployment feature switches, starting with an explicit P2P-only mode:
|
||||
the operator must be able to disable SFU in configuration while retaining STUN,
|
||||
room authority/signaling and P2P viewing. Current `SFU_UDP_PORT` already controls
|
||||
SFU availability; inspect that contract and the forced-private-mode UI before
|
||||
adding a second boolean with potentially conflicting meaning. Review which
|
||||
other options belong to deployment configuration, runtime parameters or fixed
|
||||
implementation constants. Keep this separate from public-facing documentation drafting.
|
||||
## Next: P2P Connection And Feedback Review
|
||||
|
||||
The owner proposes GitHub Pages for the public website and the dedicated US
|
||||
test server for a separate P2P-only demonstration site. Plan this after naming
|
||||
and configuration review; no public demo is deployed yet. The existing private
|
||||
production site remains private. P2P-only has no SFU fallback when direct/peer
|
||||
paths cannot connect; the demonstration must describe that actual capability.
|
||||
|
||||
Redesign the README, onboarding guides and Wiki/documentation navigation as one
|
||||
coherent release-preparation task. The owner is dissatisfied with the current
|
||||
structure; do not keep polishing it piecemeal. Until then, change only necessary
|
||||
names, runnable commands and links. Keep the new public entry brief, visual and
|
||||
easy to follow, with deeper technical detail in developer guides.
|
||||
|
||||
Design the GitHub Pages website around the accepted brand study. Keep one
|
||||
current internal contract; third-party names/notices remain accurate. DNS/Pages
|
||||
and public publication are separate from the repository rename. The private
|
||||
production service is not a public demo.
|
||||
|
||||
Include the owner-requested frontend/backend ablation review in that phase.
|
||||
Review duplicated behavior/state, module boundaries, interface clarity, lifecycle
|
||||
ownership, cohesion and coupling. Reuse mature modules and stable shared behavior.
|
||||
The primary goal is lower total complexity, not perfect performance: small gains
|
||||
do not justify permanent mechanisms or larger maintenance cost. Preserve viewing
|
||||
quality and product behavior; fewer lines alone do not justify a rewrite.
|
||||
|
||||
Use the App discovery and settings-ownership findings as leads for a
|
||||
repository-wide lifecycle audit across Web UI, shared Go server, App and
|
||||
media adapters. This is future work, not an expansion of the current entry fix.
|
||||
Look for these recurring patterns:
|
||||
|
||||
- Permission, remembered activation, capability, connection readiness and live
|
||||
operation ownership represented by the same flag or inferred from UI state.
|
||||
- Cached promises retaining `null`, rejection or a closed resource indefinitely;
|
||||
failure latches whose lifetime exceeds the evidence that justified them.
|
||||
- Results after `await`, callbacks and `finally` writing shared state or closing
|
||||
the current resource without checking the original operation/resource owner.
|
||||
- Cancellation, failure and absent observation sharing a return value that a
|
||||
caller interprets as success; review the complete caller chain.
|
||||
- Connections, slots, listeners and queues acquired before actual demand, kept
|
||||
after their consumer ends, or retired while another consumer still owns them.
|
||||
- Draft, requested, applied and server-acknowledged settings overwriting one
|
||||
another; duplicated writers or status models that can contradict real media.
|
||||
|
||||
Search results are leads, not defect evidence. Trace acquisition through use,
|
||||
commit and retirement, including error paths and shared consumers. Reproduce
|
||||
representative interleavings: A starts, is cancelled/replaced, B starts, then A
|
||||
completes or fails. Check both obsolete writes and obsolete cleanup, plus
|
||||
stop/restart and transient-failure recovery. Fix the owning boundary and remove
|
||||
redundant state rather than add case-specific flags, timers or a generic manager.
|
||||
Keep confirmed repairs separate from documented tradeoffs and unsupported
|
||||
suspicions; retain only focused checks that demonstrate a meaningful failure.
|
||||
|
||||
English/Chinese Quick Start, documentation map and license guidance are prepared.
|
||||
Keep the README short and friendly, with the logo and a small usage illustration;
|
||||
detailed deployment/development guidance belongs in the linked guides. Draft a
|
||||
static GitHub Pages homepage with tutorials and self-contained demonstrations
|
||||
matching the living-room UI. Review final release assets, update/replacement
|
||||
guidance and public distribution alongside the new brand; existing packagers and
|
||||
Docker recipe are the baseline.
|
||||
Keep expensive packaging manual and branch CI quiet. No broad media redesign is
|
||||
part of naming or documentation work.
|
||||
|
||||
[Verification status](./verification-status.md) owns broader physical limits.
|
||||
NAT and Auto are complete features; further statistical/hardware research is not
|
||||
required to finish this phase. Run any new physical workload serially, with stable
|
||||
executable paths and cleanup before the next.
|
||||
After the current phase, measure connection success, time to first picture and
|
||||
failure causes on representative networks, especially App and P2P-only sites.
|
||||
Review user progress/failure feedback and useful privacy-conscious diagnostics.
|
||||
Trace the existing ICE and P2P/SFU handoffs, including background P2P attempts
|
||||
behind working SFU media; compare SFU-first startup with measured current behavior
|
||||
before choosing changes. Preserve one graph and one operation under the
|
||||
[routing contract](./product/routing-transport.md) and
|
||||
[ADR-0005](./adr/0005-automatic-hybrid-media-routing.md). Prior ownership audits do
|
||||
not establish better connection success or speed; this note adds no retry policy.
|
||||
|
||||
## Parked Product Work
|
||||
|
||||
@@ -101,41 +54,56 @@ reproduce them. Missing NAT attempt text alone does not prove skipped attempts.
|
||||
If Native adaptation is implicated, compare actual VSE limitations with sender
|
||||
quality evidence before changing policy. Do not add retry budgets, visibility
|
||||
resets or resolution heuristics without proof. Sanitized aggregates and the
|
||||
isolated reconnect reproduction remain locally in `build/room7534-investigation/`.
|
||||
isolated reconnect reproductions are diagnostic evidence, not a recovery policy.
|
||||
Game/background settings behavior also remains an unconfirmed incident lead;
|
||||
reopen from actual control actions and matching requested/applied media evidence.
|
||||
Clarify whether Viewer-local pause should survive replacement media before
|
||||
adding playback-intent state; the current binding starts new media unless the
|
||||
Host is paused. No preservation policy has been accepted for that transition.
|
||||
|
||||
1. **Public distribution and updates.** Candidate packagers, immutable descriptors,
|
||||
notices, the runtime-only OCI recipe and release checks exist. Choose the
|
||||
public asset set and release notes before publishing a full-SHA GitHub Release
|
||||
or image. Do not add automatic installation, container self-update, Watchtower,
|
||||
compatibility ranges or active-share interruption without distribution and
|
||||
recovery evidence. Ordinary branch pushes must not run expensive CI packaging.
|
||||
2. **Representative device/network acceptance.** Exercise public-network direct
|
||||
and relay paths, two-room SFU, real-game A/V, twenty-Viewer endurance,
|
||||
sustained loss/recovery and all-UDP-blocked bounded failure. Correlate freezes
|
||||
with publisher/receiver evidence before changing representation policy.
|
||||
Physical Android/iOS work includes autoplay, background/lock, rotation,
|
||||
network migration and relay survival. Secondary desktop platform matrices
|
||||
remain explicitly deferred; compilation does not prove capture.
|
||||
3. **Broader quality work.** Reopen from measured benefit at acceptable complexity.
|
||||
1. **Representative device/network acceptance.** Resume the remaining matrix in
|
||||
[verification status](./verification-status.md#remaining-device-and-network-acceptance)
|
||||
when directed, preserving the owner's platform deferrals. Correlate freezes
|
||||
with publisher/receiver evidence before changing media policy. Run physical
|
||||
workloads serially from stable executable paths with cleanup between runs.
|
||||
2. **Broader quality work.** Reopen from measured benefit at acceptable complexity.
|
||||
Preserve chosen profiles, bitrate ceilings, endpoint capacity and P2P-first
|
||||
routing unless a new accepted decision supports changing them. No weighted
|
||||
score, all-pairs probes, periodic rebalancing, parent-wide prediction or
|
||||
room-wide minimum. Check whether a quality move merely shifts pressure to
|
||||
another parent's siblings before widening policy.
|
||||
4. **Control/resource fairness and input review.** Reassess the authenticated
|
||||
3. **Control/resource fairness and input review.** Reassess the authenticated
|
||||
WebSocket/SFU owner, HTTP/body/resource bounds, authorization and error/log
|
||||
handling before adding a queue or limiter. No parallel security framework,
|
||||
accounts, risk score or speculative policy layer.
|
||||
5. **Reachable ownership/refactor work.** Extract page media-session owners only
|
||||
alongside behavior changes; file size alone is not a rewrite reason. Reopen
|
||||
C=3 structural-intent retention, SFU failure during unrelated prepare and
|
||||
multi-child evidence ownership only with current-contract reproductions.
|
||||
Measure signaling-lock contention and synchronous diagnostic I/O before
|
||||
changing the lock or execution model; their cost remains a tradeoff.
|
||||
4. **Reachable ownership/refactor work.** Retain Host/Viewer page media-session
|
||||
extraction as a candidate alongside related behavior changes. Evaluate clear
|
||||
resource owners, fewer shared writers and a smaller change surface under
|
||||
[engineering review](./reference/engineering.md#ablation-and-review); file
|
||||
size alone does not justify a split. The completed audits do not close this
|
||||
candidate. Reopen C=3 structural-intent retention, SFU failure during unrelated
|
||||
prepare and multi-child evidence ownership only with current-contract
|
||||
reproductions.
|
||||
No new revision namespace, failure-state mirror or topology queue by default.
|
||||
6. **Storage fault recovery.** Ordinary transactions persist before memory
|
||||
changes; failures must preserve authority. Message-handler panics must unwind
|
||||
locks. Damaged-disk/COMMIT/ROLLBACK recovery is not established; choose its
|
||||
policy explicitly rather than adding catch-and-continue or retries.
|
||||
7. **Platform output.** System/tab mirroring needs no adapter. Remote Playback,
|
||||
Cast and AirPlay do not provide a portable live MediaStream receiver.
|
||||
Reopen for a registered receiver acting as an ordinary Viewer after the
|
||||
[platform-output gate](./research/platform-output.md) passes.
|
||||
5. **Storage fault recovery.** Choose and verify a damaged-disk/COMMIT/ROLLBACK
|
||||
recovery policy before adding catch-and-continue or retries. This failure
|
||||
boundary remains unestablished after ordinary persistence checks.
|
||||
6. **Platform output.** Reopen for a registered receiver acting as an ordinary
|
||||
Viewer only after the [platform-output gate](./research/platform-output.md)
|
||||
passes.
|
||||
7. **Full UI themes.** After the base version, research themes that can change
|
||||
layout, composition and motion, including a restrained graphic/cinematic
|
||||
direction with original Piik assets. Assess extension boundaries and cost
|
||||
before scheduling any theme/plugin API. Design experiments remain in Git
|
||||
history, outside the main source tree.
|
||||
8. **Visual presentation and community translations.** In a later version,
|
||||
explore bringing the pure-visual mode's illustrations and motion into the
|
||||
Chinese and English interfaces. Evaluate whether a separate pure-visual mode
|
||||
remains useful, and prepare the existing locale catalogs for community
|
||||
translations. No removal or new translation framework is scheduled for this
|
||||
release.
|
||||
9. **Website illustration polish.** Match the Host's held prop to the four
|
||||
existing hero activities: gamepad, paintbrush, camera and remote. Keep the
|
||||
accepted bright cast, floating hands and shared activity timing.
|
||||
@@ -1,6 +1,6 @@
|
||||
# Verification Status
|
||||
|
||||
Last updated: 2026-09-09
|
||||
Last updated: 2026-09-11
|
||||
|
||||
This file owns physical evidence limits that change how the product may be
|
||||
described. [Status](./status.md) owns the execution/deployment index.
|
||||
@@ -11,8 +11,7 @@ implementation and obsolete package history.
|
||||
|
||||
Windows App and Browser are the owner's primary acceptance targets. Current
|
||||
Go packages embed their Web assets and do not run a Node backend. The current
|
||||
contract is Browser/server v22, Native control v9 and capture v7; pre-port
|
||||
App/Node package checks do not prove this package.
|
||||
contract is Browser/server v23, Native control v9 and capture v7.
|
||||
|
||||
Bounded current checks cover:
|
||||
|
||||
@@ -41,6 +40,29 @@ Neither a statistical NAT campaign nor exhaustive Auto hardware benchmarking
|
||||
is required to close this phase. Investigate further only from a new failure
|
||||
or a measured improvement worth its implementation and maintenance cost.
|
||||
|
||||
## Candidate Evidence Boundary
|
||||
|
||||
The 2026-09-11 functional test acceptance used two genuine Windows App packages
|
||||
and matching Server source/Web builds under the same private contract. Both App/Site version
|
||||
directions delivered decoded 1080p H264 from an isolated native test window.
|
||||
Replacing the Server process preserved SQLite Host authority, private code-entry
|
||||
policy, Viewer grant/password authorization and App configuration. Already-open
|
||||
Host/Viewer pages reauthenticated without reloading and completed subsequent
|
||||
sharing. Exact measured identity belongs to the package descriptors and local
|
||||
structured acceptance result; Git retains the preparation history.
|
||||
|
||||
This is bounded single-machine Windows evidence. The local Server executables
|
||||
were built for Windows from the matching source; this run did not execute the
|
||||
Linux release archives. Process replacement used a forced stop on Windows and
|
||||
does not promise uninterrupted media. It does not cover game/audio endurance,
|
||||
storage faults, the wider device/network matrix or Go race detection.
|
||||
|
||||
The full publisher dry run with genuine matching target artifacts remains
|
||||
unverified. The native CI candidate run passed validation and Server packaging
|
||||
but hit the Actions artifact-storage quota before App jobs could start. Native
|
||||
macOS tooling remains necessary. [TODO](./todo.md) owns the remaining publisher
|
||||
rehearsal and owner acceptance; the broader platform deferrals below still apply.
|
||||
|
||||
## Remaining Device And Network Acceptance
|
||||
|
||||
The following are broader coverage and endurance tasks, not automatically new
|
||||
|
||||
@@ -28,6 +28,7 @@ require (
|
||||
github.com/thesyncim/gopus v0.1.1
|
||||
go.uber.org/zap v1.28.0
|
||||
golang.org/x/crypto v0.54.0
|
||||
golang.org/x/mod v0.38.0
|
||||
golang.org/x/text v0.41.0
|
||||
google.golang.org/protobuf v1.36.11
|
||||
modernc.org/sqlite v1.58.0
|
||||
|
||||
+11
-5
@@ -8,11 +8,17 @@
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<title>Piik</title>
|
||||
<script>
|
||||
try {
|
||||
var theme = localStorage.getItem("piik:ui-theme") ||
|
||||
(matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light");
|
||||
document.documentElement.dataset.theme = theme;
|
||||
} catch (e) {}
|
||||
var systemTheme = matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
|
||||
var theme;
|
||||
try { theme = localStorage.getItem("piik:ui-theme"); } catch (e) {}
|
||||
var launch = new URLSearchParams(location.hash.slice(1));
|
||||
var launchTheme = launch.get("piik-theme");
|
||||
if (launch.get("piik-client") === "1" &&
|
||||
(launchTheme === "light" || launchTheme === "dark" || launchTheme === "system")) {
|
||||
theme = launchTheme === "system" ? systemTheme : launchTheme;
|
||||
}
|
||||
document.documentElement.dataset.theme =
|
||||
theme === "light" || theme === "dark" ? theme : systemTheme;
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
+13
-1
@@ -28,6 +28,7 @@ import (
|
||||
serverapp "github.com/TNTcraftHIM/Piik/internal/server/app"
|
||||
serverconfig "github.com/TNTcraftHIM/Piik/internal/server/config"
|
||||
"github.com/TNTcraftHIM/Piik/internal/server/webassets"
|
||||
"golang.org/x/mod/semver"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -41,8 +42,17 @@ const (
|
||||
localShutdownTimeout = 5 * time.Second
|
||||
)
|
||||
|
||||
var BuildVersion = "development"
|
||||
var BuildRevision = "development"
|
||||
|
||||
func buildVersion() string {
|
||||
version := strings.TrimSpace(BuildVersion)
|
||||
if version != "" && semver.Canonical(version) == version && semver.Prerelease(version) == "" {
|
||||
return version
|
||||
}
|
||||
return "development"
|
||||
}
|
||||
|
||||
type Options struct {
|
||||
Site string
|
||||
SiteSet bool
|
||||
@@ -87,6 +97,7 @@ func Run(ctx context.Context, options Options) (returnedErr error) {
|
||||
if err != nil {
|
||||
return fmt.Errorf("Piik App diagnostics are unavailable: %w", err)
|
||||
}
|
||||
recorder.Context("build", map[string]any{"version": buildVersion(), "revision": BuildRevision})
|
||||
previous, previousWriter, previousFlags := slog.Default(), log.Writer(), log.Flags()
|
||||
var dependencyLog *diagnostics.LineWriter
|
||||
restoreLogger = func() {
|
||||
@@ -102,7 +113,7 @@ func Run(ctx context.Context, options Options) (returnedErr error) {
|
||||
log.SetFlags(previousFlags)
|
||||
options.console.send(consoleDebug{logPath: recorder.LogPath(), export: recorder.Export})
|
||||
}
|
||||
slog.Debug("piik-client", "event", "start", "revision", BuildRevision)
|
||||
slog.Debug("piik-client", "event", "start", "version", buildVersion(), "revision", BuildRevision)
|
||||
options.console.show(consoleView{state: "starting"})
|
||||
if err := validateMode(options); err != nil {
|
||||
return err
|
||||
@@ -192,6 +203,7 @@ func runLauncher(
|
||||
ctx,
|
||||
webassets.FS(),
|
||||
config.Site,
|
||||
buildVersion(),
|
||||
BuildRevision,
|
||||
config.LocalAccessPassword,
|
||||
)
|
||||
|
||||
@@ -3,20 +3,74 @@ package app
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"log/slog"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
appconfig "github.com/TNTcraftHIM/Piik/internal/app/config"
|
||||
"github.com/TNTcraftHIM/Piik/internal/app/loopback"
|
||||
serverconfig "github.com/TNTcraftHIM/Piik/internal/server/config"
|
||||
)
|
||||
|
||||
func TestSavedSiteAllowsBrowserOriginAtNativeControl(t *testing.T) {
|
||||
for _, site := range []struct{ input, origin string }{
|
||||
{"https://Share.Example:443", "https://share.example"},
|
||||
{"http://share.example:80", "http://share.example"},
|
||||
{"http://share.example:8787", "http://share.example:8787"},
|
||||
} {
|
||||
t.Run(site.input, func(t *testing.T) {
|
||||
path := filepath.Join(t.TempDir(), "client.json")
|
||||
payload := fmt.Sprintf(`{"version":1,"localAccessPassword":"","site":%q}`, site.input)
|
||||
if err := os.WriteFile(path, []byte(payload), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
configuration, err := appconfig.LoadOrCreate(path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
control, err := loopback.Start(t.Context(), loopback.Options{
|
||||
AllowedOrigins: allowedOrigins(configuration.Site, 8787),
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() { _ = control.Close() })
|
||||
client := &http.Client{Timeout: 3 * time.Second}
|
||||
for _, probe := range []struct {
|
||||
method, origin string
|
||||
status int
|
||||
}{
|
||||
{http.MethodOptions, site.origin, http.StatusNoContent},
|
||||
{http.MethodGet, site.origin, http.StatusOK},
|
||||
{http.MethodOptions, "https://other.example", http.StatusForbidden},
|
||||
} {
|
||||
request, err := http.NewRequest(probe.method, control.Endpoint().URL+"/health", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
request.Header.Set("Origin", probe.origin)
|
||||
response, err := client.Do(request)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
response.Body.Close()
|
||||
if response.StatusCode != probe.status {
|
||||
t.Fatalf("%s health from %q = %d, want %d", probe.method, probe.origin, response.StatusCode, probe.status)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestOccupiedPortRejectsLinkBeforeStartingATunnel(t *testing.T) {
|
||||
listener, err := net.ListenTCP("tcp4", &net.TCPAddr{IP: net.IPv4zero})
|
||||
if err != nil {
|
||||
@@ -83,6 +137,7 @@ func TestAppDiagnosticsUseFilesOnlyWhenEnabled(t *testing.T) {
|
||||
continue
|
||||
}
|
||||
if readErr != nil || !strings.Contains(string(content), `"event":"start"`) ||
|
||||
!strings.Contains(string(content), `"version":"`+buildVersion()+`"`) ||
|
||||
!strings.Contains(string(content), `"revision":`) ||
|
||||
!strings.Contains(string(content), `"event":"stopped","failed":true`) {
|
||||
t.Fatalf("missing file lifecycle events: %s, %v", content, readErr)
|
||||
|
||||
@@ -11,6 +11,8 @@ import (
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
serverconfig "github.com/TNTcraftHIM/Piik/internal/server/config"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -101,7 +103,7 @@ func NormalizeSite(value string) (string, error) {
|
||||
parsed.RawQuery != "" || parsed.Fragment != "" {
|
||||
return "", errors.New("Piik Site must be an HTTP or HTTPS origin")
|
||||
}
|
||||
return parsed.Scheme + "://" + parsed.Host, nil
|
||||
return serverconfig.Origin(parsed), nil
|
||||
}
|
||||
|
||||
// NormalizeLocalAccessPassword keeps the local authority open by default while
|
||||
|
||||
@@ -57,12 +57,17 @@ func TestSavePersistsOneNormalizedSiteChoice(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestNormalizeSiteAcceptsOnlyAPlainWebOrigin(t *testing.T) {
|
||||
for _, value := range []string{
|
||||
"https://share.example",
|
||||
"http://192.168.1.4:8787/",
|
||||
for value, want := range map[string]string{
|
||||
"https://share.example": "https://share.example",
|
||||
"https://Share.Example:443/": "https://share.example",
|
||||
"http://Share.Example:80": "http://share.example",
|
||||
"https://share.example:0443": "https://share.example",
|
||||
"http://192.168.1.4:8787/": "http://192.168.1.4:8787",
|
||||
"http://share.example:443": "http://share.example:443",
|
||||
"https://[::1]:443": "https://[::1]",
|
||||
} {
|
||||
if _, err := NormalizeSite(value); err != nil {
|
||||
t.Fatalf("NormalizeSite(%q) = %v", value, err)
|
||||
if got, err := NormalizeSite(value); err != nil || got != want {
|
||||
t.Fatalf("NormalizeSite(%q) = %q, %v; want %q", value, got, err, want)
|
||||
}
|
||||
}
|
||||
for _, value := range []string{
|
||||
|
||||
@@ -281,7 +281,11 @@ func (model consoleModel) content(styled bool) string {
|
||||
revision = revision[:7]
|
||||
}
|
||||
var out strings.Builder
|
||||
heading := "Piik\nApp\n" + muted.Render(revision)
|
||||
version := buildVersion()
|
||||
heading := "Piik\nApp\n" + muted.Render(version)
|
||||
if revision != "" && revision != version {
|
||||
heading = "Piik App\n" + muted.Render(version) + "\n" + muted.Render(revision)
|
||||
}
|
||||
animated := styled && model.animating()
|
||||
mascot := accent.Render(consoleTV(model.frame, animated, compact || width < 34))
|
||||
if styled && model.colors && width >= 36 && !compactDebug {
|
||||
|
||||
@@ -111,6 +111,27 @@ func TestConsolePresentationAndShutdown(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestConsoleBuildVersionAndRevision(t *testing.T) {
|
||||
previousVersion, previousRevision := BuildVersion, BuildRevision
|
||||
t.Cleanup(func() { BuildVersion, BuildRevision = previousVersion, previousRevision })
|
||||
BuildRevision = strings.Repeat("a", 40)
|
||||
for _, test := range []struct{ version, want string }{
|
||||
{"v1.2.3", "v1.2.3"},
|
||||
{"development", "development"},
|
||||
{"", "development"},
|
||||
{"v01.2.3", "development"},
|
||||
{"v1.2", "development"},
|
||||
{"v1.2.3-preview.1", "development"},
|
||||
{"v1.2.3+build", "development"},
|
||||
} {
|
||||
BuildVersion = test.version
|
||||
content := (consoleModel{width: 80, language: "en", view: consoleView{state: "ready"}}).content(false)
|
||||
if !strings.Contains(content, test.want) || !strings.Contains(content, "aaaaaaa") || strings.Contains(content, BuildRevision) {
|
||||
t.Fatalf("console identity for %q: %s", test.version, content)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestConsoleDiagnosticExportRunsOutsideUpdate(t *testing.T) {
|
||||
called := 0
|
||||
path := filepath.Join(t.TempDir(), "client-report.zip")
|
||||
|
||||
@@ -48,6 +48,7 @@ type Server struct {
|
||||
static http.Handler
|
||||
site string
|
||||
localAccessPassword string
|
||||
version string
|
||||
revision string
|
||||
selection chan Selection
|
||||
resultReady chan struct{}
|
||||
@@ -68,7 +69,7 @@ type Server struct {
|
||||
// Start serves the launcher page and its API on a loopback port. assets is the
|
||||
// built Browser UI, which the App embeds; a nil file system means the binary
|
||||
// carries no build and the launcher cannot run.
|
||||
func Start(parent context.Context, assets fs.FS, site, revision, localAccessPassword string) (*Server, error) {
|
||||
func Start(parent context.Context, assets fs.FS, site, version, revision, localAccessPassword string) (*Server, error) {
|
||||
if parent == nil {
|
||||
parent = context.Background()
|
||||
}
|
||||
@@ -100,6 +101,7 @@ func Start(parent context.Context, assets fs.FS, site, revision, localAccessPass
|
||||
static: http.FileServer(http.FS(assets)),
|
||||
site: site,
|
||||
localAccessPassword: normalizedPassword,
|
||||
version: version,
|
||||
revision: strings.TrimSpace(revision),
|
||||
selection: make(chan Selection, 1),
|
||||
resultReady: make(chan struct{}),
|
||||
@@ -201,6 +203,7 @@ func (server *Server) handleState(response http.ResponseWriter, request *http.Re
|
||||
writeJSON(response, http.StatusOK, map[string]any{
|
||||
"site": server.site,
|
||||
"localAccessPassword": server.localAccessPassword,
|
||||
"version": server.version,
|
||||
"revision": server.revision,
|
||||
"defaultMode": func() Mode {
|
||||
if server.site != "" {
|
||||
|
||||
@@ -31,11 +31,12 @@ func TestLauncherServesStateAndCompletesOneSelection(t *testing.T) {
|
||||
Site string `json:"site"`
|
||||
LocalAccessPassword string `json:"localAccessPassword"`
|
||||
DefaultMode Mode `json:"defaultMode"`
|
||||
Version string `json:"version"`
|
||||
Revision string `json:"revision"`
|
||||
}
|
||||
if response.StatusCode != http.StatusOK || json.NewDecoder(response.Body).Decode(&state) != nil ||
|
||||
state.Site != "https://share.example" || state.DefaultMode != ModeSite ||
|
||||
state.Revision != "" || state.LocalAccessPassword != "" {
|
||||
state.Version != "development" || state.Revision != "" || state.LocalAccessPassword != "" {
|
||||
t.Fatalf("launcher state = %d, %+v", response.StatusCode, state)
|
||||
}
|
||||
|
||||
@@ -80,9 +81,10 @@ func TestLauncherServesStateAndCompletesOneSelection(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestLauncherIncludesTheInjectedBuildRevision(t *testing.T) {
|
||||
func TestLauncherIncludesTheInjectedBuildVersionAndRevision(t *testing.T) {
|
||||
const version = "v1.2.3"
|
||||
const revision = "0123456789abcdef0123456789abcdef01234567"
|
||||
server := startFixtureWithRevision(t, "", revision)
|
||||
server := startFixtureWithBuildAndPassword(t, "", version, revision, "")
|
||||
origin := strings.TrimSuffix(server.URL(), "/client")
|
||||
response, err := http.Get(origin + "/api/client-launcher")
|
||||
if err != nil {
|
||||
@@ -90,11 +92,12 @@ func TestLauncherIncludesTheInjectedBuildRevision(t *testing.T) {
|
||||
}
|
||||
defer response.Body.Close()
|
||||
var state struct {
|
||||
Version string `json:"version"`
|
||||
Revision string `json:"revision"`
|
||||
}
|
||||
if response.StatusCode != http.StatusOK || json.NewDecoder(response.Body).Decode(&state) != nil ||
|
||||
state.Revision != revision {
|
||||
t.Fatalf("launcher revision = %d, %+v", response.StatusCode, state)
|
||||
state.Version != version || state.Revision != revision {
|
||||
t.Fatalf("launcher build = %d, %+v", response.StatusCode, state)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -147,17 +150,17 @@ func TestLauncherCarriesAndNormalizesAnOptionalLocalPassword(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestLauncherRejectsAnInvalidSavedSite(t *testing.T) {
|
||||
if _, err := Start(t.Context(), appAssets(), "https://example.test/path", "", ""); err == nil {
|
||||
if _, err := Start(t.Context(), appAssets(), "https://example.test/path", "development", "", ""); err == nil {
|
||||
t.Fatal("launcher accepted a Site path")
|
||||
}
|
||||
}
|
||||
|
||||
// A binary built without the Browser build has nothing to launch.
|
||||
func TestLauncherRequiresTheEmbeddedBrowserBuild(t *testing.T) {
|
||||
if _, err := Start(t.Context(), nil, "", "", ""); err == nil {
|
||||
if _, err := Start(t.Context(), nil, "", "development", "", ""); err == nil {
|
||||
t.Fatal("launcher started without embedded assets")
|
||||
}
|
||||
if _, err := Start(t.Context(), fstest.MapFS{}, "", "", ""); err == nil {
|
||||
if _, err := Start(t.Context(), fstest.MapFS{}, "", "development", "", ""); err == nil {
|
||||
t.Fatal("launcher started without an index document")
|
||||
}
|
||||
}
|
||||
@@ -238,23 +241,19 @@ func TestLauncherRejectsCrossOriginControl(t *testing.T) {
|
||||
}
|
||||
|
||||
func startFixture(t *testing.T, site string) *Server {
|
||||
return startFixtureWithRevisionAndPassword(t, site, "", "")
|
||||
}
|
||||
|
||||
func startFixtureWithRevision(t *testing.T, site, revision string) *Server {
|
||||
return startFixtureWithRevisionAndPassword(t, site, revision, "")
|
||||
return startFixtureWithBuildAndPassword(t, site, "development", "", "")
|
||||
}
|
||||
|
||||
func startFixtureWithPassword(t *testing.T, site, password string) *Server {
|
||||
return startFixtureWithRevisionAndPassword(t, site, "", password)
|
||||
return startFixtureWithBuildAndPassword(t, site, "development", "", password)
|
||||
}
|
||||
|
||||
func startFixtureWithRevisionAndPassword(
|
||||
func startFixtureWithBuildAndPassword(
|
||||
t *testing.T,
|
||||
site, revision, password string,
|
||||
site, version, revision, password string,
|
||||
) *Server {
|
||||
t.Helper()
|
||||
server, err := Start(t.Context(), appAssets(), site, revision, password)
|
||||
server, err := Start(t.Context(), appAssets(), site, version, revision, password)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ import (
|
||||
"github.com/coder/websocket/wsjson"
|
||||
)
|
||||
|
||||
const testOrigin = "https://share.bonfire.icu"
|
||||
const testOrigin = "https://share.example.com"
|
||||
|
||||
func TestStartServesHealthAndStrictControlHandshake(t *testing.T) {
|
||||
expectedMedia := NativeMediaCapabilities{
|
||||
|
||||
@@ -17,20 +17,17 @@ import (
|
||||
"github.com/TNTcraftHIM/Piik/internal/server/protocol"
|
||||
)
|
||||
|
||||
// Ported from src/server/access-session.ts.
|
||||
const (
|
||||
defaultSessionTTLSeconds = 24 * 60 * 60
|
||||
cookieVersion = "v1"
|
||||
)
|
||||
|
||||
var (
|
||||
// TS: /^\d+$/ over the expiry segment.
|
||||
accessExpiresPattern = regexp.MustCompile(`^[0-9]+$`)
|
||||
// TS: /^[A-Za-z0-9_-]{43}$/ — base64url of a 32-byte HMAC, unpadded.
|
||||
accessExpiresPattern = regexp.MustCompile(`^[0-9]+$`)
|
||||
accessSignaturePattern = regexp.MustCompile(`^[A-Za-z0-9_-]{43}$`)
|
||||
)
|
||||
|
||||
// siteAccessOptions mirrors SiteAccessOptions in access-session.ts.
|
||||
// siteAccessOptions configures cookie lifetime, signing and transport policy.
|
||||
type siteAccessOptions struct {
|
||||
// Password empty means site access is not required.
|
||||
Password string
|
||||
@@ -38,7 +35,7 @@ type siteAccessOptions struct {
|
||||
Secure bool
|
||||
// Now returns Unix milliseconds; nil uses the wall clock.
|
||||
Now func() int64
|
||||
// TTLSeconds of 0 stands for TS's absent `ttlSeconds`, i.e. 24 hours.
|
||||
// TTLSeconds of 0 selects the default 24-hour lifetime.
|
||||
TTLSeconds int
|
||||
}
|
||||
|
||||
@@ -75,7 +72,6 @@ func newSiteAccess(options siteAccessOptions) (*siteAccessGate, error) {
|
||||
if ttlSeconds == 0 {
|
||||
ttlSeconds = defaultSessionTTLSeconds
|
||||
}
|
||||
// TS: !Number.isSafeInteger(ttlSeconds) || ttlSeconds <= 0.
|
||||
if ttlSeconds < 0 || int64(ttlSeconds) > protocol.MaxSafeInteger {
|
||||
return nil, errors.New("Access session TTL must be a positive integer")
|
||||
}
|
||||
@@ -111,9 +107,6 @@ func (a *siteAccessGate) isAuthenticated(cookieHeader string) bool {
|
||||
if value == "" {
|
||||
return false
|
||||
}
|
||||
// TS: const [version, expiresText, signature, ...extra] = value.split(".")
|
||||
// Fewer than three segments fail the two regexes below (an absent segment
|
||||
// is tested as ""), more than three fill `extra`.
|
||||
segments := strings.Split(value, ".")
|
||||
if len(segments) != 3 ||
|
||||
segments[0] != cookieVersion ||
|
||||
@@ -121,8 +114,7 @@ func (a *siteAccessGate) isAuthenticated(cookieHeader string) bool {
|
||||
!accessSignaturePattern.MatchString(segments[2]) {
|
||||
return false
|
||||
}
|
||||
// TS: Number(expiresText) then Number.isSafeInteger. The segment is all
|
||||
// digits, so only an out-of-range value can fail here.
|
||||
// Expiry is already decimal; parsing still enforces integer bounds.
|
||||
expiresAt, err := strconv.ParseInt(segments[1], 10, 64)
|
||||
if err != nil || expiresAt > protocol.MaxSafeInteger {
|
||||
return false
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
// Package app ports src/server/app.ts: the HTTP composition, the startup and
|
||||
// shutdown lifecycle, and the request router. access.go ports
|
||||
// src/server/access-session.ts and static.go the sirv frontend handler.
|
||||
// Package app composes the shared HTTP service, site access, embedded assets,
|
||||
// media listeners, and startup/shutdown lifecycle for Hosted Server and App.
|
||||
//
|
||||
// # Locking
|
||||
//
|
||||
@@ -208,13 +207,6 @@ func newRoomStore(options Options) (*room.Store, error) {
|
||||
})
|
||||
}
|
||||
|
||||
// Handler is the composed router, exposed so a host process can mount it.
|
||||
func (s *Server) Handler() http.Handler { return s }
|
||||
|
||||
// Store is PiikServer.roomStore. The signaling server's mutex guards it, so
|
||||
// callers outside that lock may only read it while no request is in flight.
|
||||
func (s *Server) Store() *room.Store { return s.store }
|
||||
|
||||
// Listen binds application sockets before recovering room authority and accepting traffic.
|
||||
func (s *Server) Listen(ctx context.Context) (int, error) {
|
||||
s.mu.Lock()
|
||||
|
||||
@@ -348,11 +348,11 @@ func expectGoroutinesSettled(t *testing.T, before int) {
|
||||
|
||||
// --- runtime capabilities -------------------------------------------------
|
||||
|
||||
func TestCapabilitiesKeepNATPredictionAbsentByDefault(t *testing.T) {
|
||||
func TestCapabilitiesReportServicesDisabledByDefault(t *testing.T) {
|
||||
server := start(t, Options{Config: testConfig(t)})
|
||||
|
||||
server.do(http.MethodGet, "/api/capabilities").
|
||||
expect(http.StatusOK, `{"natPrediction":false}`)
|
||||
expect(http.StatusOK, `{"sfu":false,"natPrediction":false}`)
|
||||
}
|
||||
|
||||
func TestCapabilitiesReportOptionalNATPredictionWithoutExposingConfiguration(t *testing.T) {
|
||||
@@ -363,7 +363,7 @@ func TestCapabilitiesReportOptionalNATPredictionWithoutExposingConfiguration(t *
|
||||
server := start(t, Options{Config: configuration})
|
||||
|
||||
server.do(http.MethodGet, "/api/capabilities").
|
||||
expect(http.StatusOK, `{"natPrediction":true}`).
|
||||
expect(http.StatusOK, `{"sfu":false,"natPrediction":true}`).
|
||||
expectHeader("Cache-Control", "no-store").
|
||||
expectHeader("X-Content-Type-Options", "nosniff")
|
||||
|
||||
@@ -405,6 +405,8 @@ func TestSFUListenerFollowsApplicationCloseAndEnd(t *testing.T) {
|
||||
if server.signaling.Load() == nil || server.media == nil {
|
||||
t.Fatal("missing runtime owner")
|
||||
}
|
||||
server.do(http.MethodGet, "/api/capabilities").
|
||||
expect(http.StatusOK, `{"sfu":true,"natPrediction":false}`)
|
||||
var err error
|
||||
if ending {
|
||||
err = server.End(context.Background())
|
||||
@@ -671,16 +673,21 @@ func TestSiteAccessRejectsAnExpiredOrModifiedCookie(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestSiteAccessIsImmediateWhenTheAccessPasswordIsEmpty(t *testing.T) {
|
||||
configuration := testConfig(t)
|
||||
configuration.SiteAccessPassword = ""
|
||||
server := start(t, Options{Config: configuration})
|
||||
for _, environment := range []config.Environment{config.EnvironmentDevelopment, config.EnvironmentProduction} {
|
||||
t.Run(string(environment), func(t *testing.T) {
|
||||
configuration := testConfig(t)
|
||||
configuration.Env = environment
|
||||
configuration.SiteAccessPassword = ""
|
||||
server := start(t, Options{Config: configuration})
|
||||
|
||||
server.do(http.MethodGet, "/api/site-access").
|
||||
expect(http.StatusOK, `{"required":false,"authenticated":true}`).
|
||||
expectHeader("Set-Cookie", "")
|
||||
server.do(http.MethodPost, "/api/site-access", withOrigin(allowedOrigin)).
|
||||
expect(http.StatusOK, `{"required":false,"authenticated":true}`).
|
||||
expectHeader("Set-Cookie", "")
|
||||
server.do(http.MethodGet, "/api/site-access").
|
||||
expect(http.StatusOK, `{"required":false,"authenticated":true}`).
|
||||
expectHeader("Set-Cookie", "")
|
||||
server.do(http.MethodPost, "/api/site-access", withOrigin(allowedOrigin)).
|
||||
expect(http.StatusOK, `{"required":false,"authenticated":true}`).
|
||||
expectHeader("Set-Cookie", "")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// --- room HTTP API --------------------------------------------------------
|
||||
@@ -749,7 +756,7 @@ func TestRoomCreationPassesTheSharedViewerCeilingToRoomAdmission(t *testing.T) {
|
||||
server := start(t, Options{Config: configuration})
|
||||
|
||||
server.createRoom(roomRequest{}).expectStatus(http.StatusCreated)
|
||||
if got := server.Store().MaxViewersPerRoom(); got != protocol.MaxViewersPerRoomLimit {
|
||||
if got := server.store.MaxViewersPerRoom(); got != protocol.MaxViewersPerRoomLimit {
|
||||
t.Fatalf("maxViewersPerRoom = %d, want %d", got, protocol.MaxViewersPerRoomLimit)
|
||||
}
|
||||
}
|
||||
@@ -774,8 +781,9 @@ func TestRoomCreationCreatesPrivateRoomsWithOptionalPasswordsAtomically(t *testi
|
||||
}
|
||||
}
|
||||
|
||||
func TestRoomCreationAllowsExplicitOpenCreationWithoutSiteAccess(t *testing.T) {
|
||||
func TestProductionAllowsRoomCreationWithoutSiteAccessButRequiresRoomOwnership(t *testing.T) {
|
||||
configuration := testConfig(t)
|
||||
configuration.Env = config.EnvironmentProduction
|
||||
configuration.SiteAccessPassword = ""
|
||||
server := start(t, Options{Config: configuration})
|
||||
|
||||
@@ -787,6 +795,14 @@ func TestRoomCreationAllowsExplicitOpenCreationWithoutSiteAccess(t *testing.T) {
|
||||
if !strings.Contains(created.InviteURL, "#v=") {
|
||||
t.Fatalf("inviteUrl = %q", created.InviteURL)
|
||||
}
|
||||
server.updateRoomAccess(accessRequest{
|
||||
roomID: created.RoomID, hostToken: "wrong-token",
|
||||
body: `{"action":"set-code-entry-policy","policy":"private"}`,
|
||||
}).expectStatus(http.StatusNotFound)
|
||||
server.updateRoomAccess(accessRequest{
|
||||
roomID: created.RoomID, hostToken: created.HostToken,
|
||||
body: `{"action":"set-code-entry-policy","policy":"private"}`,
|
||||
}).expectStatus(http.StatusOK)
|
||||
}
|
||||
|
||||
func TestRoomCreationAllocatesUniqueFourDigitCodesConcurrently(t *testing.T) {
|
||||
@@ -840,7 +856,7 @@ func TestRoomReplacementReplacesAuthorityAndClosesOldMembership(t *testing.T) {
|
||||
password: &password, preferredRoomID: "4321",
|
||||
}).expectStatus(http.StatusCreated).room()
|
||||
|
||||
store := server.Store()
|
||||
store := server.store
|
||||
host, err := store.ConnectParticipant(room.ConnectParticipantInput{
|
||||
RoomID: original.RoomID, Role: protocol.RoleHost, Token: original.HostToken,
|
||||
ClientID: "host-client", SessionID: "host-session",
|
||||
@@ -999,7 +1015,7 @@ func TestRoomAccessManagesADormantRoomWithoutStartingSharing(t *testing.T) {
|
||||
!strings.Contains(revoked.body, `"type":"viewer-grant-updated"`) {
|
||||
t.Fatalf("revocation body = %s", revoked.body)
|
||||
}
|
||||
if _, connected := server.Store().GetConnectedHost(created.RoomID); connected {
|
||||
if _, connected := server.store.GetConnectedHost(created.RoomID); connected {
|
||||
t.Fatal("managing access started a sharing session")
|
||||
}
|
||||
|
||||
@@ -1153,7 +1169,7 @@ func TestRestoresStableRoomAuthorityAcrossAnApplicationRestart(t *testing.T) {
|
||||
|
||||
now.Store(30 * 24 * 60 * 60 * 1_000)
|
||||
second := start(t, Options{Config: configuration, Now: now.Load})
|
||||
if size := second.Store().Size(); size != 1 {
|
||||
if size := second.store.Size(); size != 1 {
|
||||
t.Fatalf("restored room count = %d, want 1", size)
|
||||
}
|
||||
second.updateRoomAccess(accessRequest{
|
||||
|
||||
@@ -10,10 +10,7 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Request and response helpers ported from src/server/app.ts.
|
||||
|
||||
// maxJSONRequestBytes is the readJsonBody(request, 1_024) budget every JSON
|
||||
// route shares.
|
||||
// maxJSONRequestBytes bounds the body every JSON route accepts.
|
||||
const maxJSONRequestBytes = 1024
|
||||
|
||||
// errorBody is the { error } shape every failing route answers with.
|
||||
@@ -26,13 +23,13 @@ type healthBody struct {
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
// siteAccessBody is siteAccessStatus(); the key order is the TS literal's.
|
||||
// siteAccessBody reports whether site access is required and already granted.
|
||||
type siteAccessBody struct {
|
||||
Required bool `json:"required"`
|
||||
Authenticated bool `json:"authenticated"`
|
||||
}
|
||||
|
||||
// sendJSON always sets the exact Content-Length and the charset, as app.ts did.
|
||||
// sendJSON sets the exact Content-Length and the JSON charset.
|
||||
// Encoding one of this package's own values cannot fail, so a marshal error is
|
||||
// a programming error and reaches the recover wrapper.
|
||||
func sendJSON(writer http.ResponseWriter, status int, body any) {
|
||||
|
||||
@@ -13,16 +13,15 @@ import (
|
||||
"github.com/TNTcraftHIM/Piik/internal/server/signal"
|
||||
)
|
||||
|
||||
// The request router of src/server/app.ts. Route order is a contract (map
|
||||
// section 4.3, O34): the first match wins and the /api/ catch-all sits after
|
||||
// every concrete route.
|
||||
// The first matching route wins. The /api/ catch-all follows every concrete
|
||||
// route so unknown API requests cannot fall through to the frontend.
|
||||
|
||||
var (
|
||||
roomReplacementPath = regexp.MustCompile(`^/api/rooms/([1-9][0-9]{3})/replacement$`)
|
||||
roomAccessPath = regexp.MustCompile(`^/api/rooms/([1-9][0-9]{3})/access$`)
|
||||
)
|
||||
|
||||
// ServeHTTP is the createServer request listener of app.ts.
|
||||
// ServeHTTP dispatches requests and contains handler panics at the HTTP boundary.
|
||||
func (s *Server) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
|
||||
recorder := &responseRecorder{ResponseWriter: writer}
|
||||
defer func() {
|
||||
@@ -77,6 +76,7 @@ func (s *Server) route(
|
||||
return
|
||||
}
|
||||
sendJSON(writer, http.StatusOK, protocol.RuntimeCapabilities{
|
||||
Sfu: s.config.SFU != nil,
|
||||
NatPrediction: s.config.NATPredictionEnabled,
|
||||
})
|
||||
return
|
||||
|
||||
@@ -10,11 +10,11 @@ import (
|
||||
"github.com/TNTcraftHIM/Piik/internal/server/protocol"
|
||||
)
|
||||
|
||||
// staticContentTypes freezes mrmime's answers for the extensions a Vite build
|
||||
// of this repo can emit. Go's mime.TypeByExtension overlays the Windows
|
||||
// staticContentTypes fixes the MIME types for this repository's Web assets.
|
||||
// Go's mime.TypeByExtension overlays the Windows
|
||||
// registry or /etc/mime.types, so the served Content-Type would otherwise
|
||||
// depend on the machine; with X-Content-Type-Options: nosniff a wrong type on a
|
||||
// font or a webmanifest is user visible. See evidence/runtime.md section 4.2.
|
||||
// font or a webmanifest is user visible.
|
||||
var staticContentTypes = map[string]string{
|
||||
".html": "text/html;charset=utf-8", // sirv appends ";charset=utf-8" with no space
|
||||
".htm": "text/html;charset=utf-8",
|
||||
@@ -42,20 +42,16 @@ var staticContentTypes = map[string]string{
|
||||
".ico": "", // mrmime has no .ico, so sirv sends an EMPTY Content-Type
|
||||
}
|
||||
|
||||
// staticHandler reproduces `sirv(directory, { single: true, setHeaders })` as
|
||||
// src/server/app.ts used it, over an fs.FS instead of a directory walk.
|
||||
// notFound is the app's JSON 404, which sirv reached through its next()
|
||||
// callback. Verified against sirv@3.0.2 in evidence/runtime.md section 4.4.
|
||||
// staticHandler serves embedded assets and falls back to index.html for SPA
|
||||
// routes. Missing files use the application's JSON 404 handler.
|
||||
func staticHandler(assets fs.FS, notFound http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
|
||||
// sirv never inspects req.method: app.ts routes /healthz and /api/**
|
||||
// first, so everything else reaches the frontend handler as-is.
|
||||
// API and health routes have already been handled by ServeHTTP.
|
||||
cleaned := path.Clean("/" + request.URL.Path)
|
||||
name := strings.TrimPrefix(cleaned, "/")
|
||||
|
||||
serve := func(candidate string) bool {
|
||||
// path.Clean already dropped any "..", and totalist never indexed a
|
||||
// dot-prefixed segment, so neither can be served.
|
||||
// Clean traversal segments and exclude hidden assets before opening.
|
||||
if candidate == "" || hasDotSegment(candidate) {
|
||||
return false
|
||||
}
|
||||
@@ -72,15 +68,12 @@ func staticHandler(assets fs.FS, notFound http.Handler) http.Handler {
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
// app.ts setHeaders.
|
||||
writer.Header().Set("X-Content-Type-Options", "nosniff")
|
||||
writer.Header().Set("Referrer-Policy", "no-referrer")
|
||||
// Set Content-Type explicitly, possibly to "", so ServeContent
|
||||
// never sniffs; the empty name keeps it from guessing either.
|
||||
writer.Header()["Content-Type"] = []string{staticContentTypes[strings.ToLower(path.Ext(candidate))]}
|
||||
// embed.FS reports a zero ModTime, so no Last-Modified is emitted
|
||||
// and no conditional request is honoured. sirv sends
|
||||
// Last-Modified but never answers 304 either; do not fabricate one.
|
||||
// embed.FS reports a zero ModTime; do not fabricate Last-Modified.
|
||||
http.ServeContent(writer, request, "", info.ModTime(), seeker)
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
// Package config ports src/server/config.ts, src/server/local-config.ts and
|
||||
// src/server/ice.ts: the Hosted environment contract, the Client's local
|
||||
// composition, and the ICE configuration derived from either. The caller
|
||||
// supplies the environment as a map; loading opens no runtime resources.
|
||||
// Package config owns Hosted and App Local configuration and ICE discovery.
|
||||
// Callers provide configuration inputs; loading opens no runtime resources.
|
||||
package config
|
||||
|
||||
import (
|
||||
@@ -18,8 +16,8 @@ import (
|
||||
"github.com/TNTcraftHIM/Piik/internal/server/protocol"
|
||||
)
|
||||
|
||||
// Environment ports RuntimeEnvironment. DECISIONS D8 renames NODE_ENV to
|
||||
// PIIK_ENV and drops "test".
|
||||
// Environment selects development-only behavior. A stale unit file must fail
|
||||
// closed rather than fall back to development.
|
||||
type Environment string
|
||||
|
||||
// The two accepted PIIK_ENV values.
|
||||
@@ -28,7 +26,7 @@ const (
|
||||
EnvironmentProduction Environment = "production"
|
||||
)
|
||||
|
||||
// Bounds and defaults from src/server/config.ts.
|
||||
// Bounds and defaults for Hosted configuration.
|
||||
const (
|
||||
minSiteAccessPasswordBytes = 8
|
||||
maxSiteAccessPasswordBytes = 128
|
||||
@@ -42,9 +40,8 @@ var visibleASCIIPattern = regexp.MustCompile(`^[\x21-\x7e]+$`)
|
||||
|
||||
const removedTurnReason = "ordinary ICE accepts STUN_URLS only"
|
||||
|
||||
// removedEnvironmentVariables ports REMOVED_ENVIRONMENT_VARIABLES together with
|
||||
// the two names the TypeScript checked separately, and NODE_ENV (DECISIONS D8:
|
||||
// a stale unit file must not fail open). An empty reason takes the TURN default.
|
||||
// removedEnvironmentVariables names settings with no current equivalent. An
|
||||
// empty reason takes the TURN default.
|
||||
// The order is the report order when several removed names are present.
|
||||
var removedEnvironmentVariables = []struct{ name, reason string }{
|
||||
{"TURN_URLS", ""},
|
||||
@@ -74,17 +71,14 @@ type SFUConfig struct {
|
||||
PublicIP string
|
||||
}
|
||||
|
||||
// Config ports ServerConfig. Fields the TypeScript left `undefined` are the
|
||||
// zero value here: an empty SiteAccessPassword means site access is open, an
|
||||
// empty RoomDatabasePath means memory mode, and a nil SFU means no SFU
|
||||
// fallback.
|
||||
// Config describes runtime configuration. Zero values mean open site
|
||||
// access, process-memory rooms, and no SFU fallback.
|
||||
type Config struct {
|
||||
Env Environment
|
||||
Port int
|
||||
ListenHost string
|
||||
PublicBaseURL *url.URL
|
||||
// AllowedOrigins is a membership test only; Go map order is not observable
|
||||
// here (src/server/app.ts only ever calls `allowedOrigins.has(origin)`).
|
||||
// AllowedOrigins is a membership test only; map order is not observable.
|
||||
AllowedOrigins map[string]struct{}
|
||||
SiteAccessPassword string
|
||||
RoomDatabasePath string
|
||||
@@ -96,15 +90,14 @@ type Config struct {
|
||||
// discovery URLs do not make the Client an externally reachable STUN server.
|
||||
STUNListenAddresses []string
|
||||
NATPredictionEnabled bool
|
||||
// NATPredictionSTUNURLs is nil where the TypeScript left it `undefined`,
|
||||
// which IceConfig distinguishes from a configured empty list.
|
||||
// NATPredictionSTUNURLs is nil when unset, which IceConfig distinguishes
|
||||
// from a configured empty list.
|
||||
NATPredictionSTUNURLs []string
|
||||
}
|
||||
|
||||
// Load ports loadConfig. env is the whole process environment: cmd passes
|
||||
// os.Environ() as a map, tests pass a literal. A key present with an empty
|
||||
// value is `""` in TypeScript too, so presence is the only distinction that
|
||||
// needs the two-value lookup.
|
||||
// Load validates the whole process environment. Presence, not emptiness,
|
||||
// distinguishes an explicit value from an unset value where that distinction
|
||||
// is part of the contract.
|
||||
func Load(env map[string]string) (Config, error) {
|
||||
for _, removed := range removedEnvironmentVariables {
|
||||
if _, present := env[removed.name]; present {
|
||||
@@ -132,8 +125,8 @@ func Load(env map[string]string) (Config, error) {
|
||||
listenHost = "0.0.0.0"
|
||||
}
|
||||
|
||||
// TS: environment.PUBLIC_BASE_URL ?? `http://localhost:${port}` — only an
|
||||
// absent variable takes the default.
|
||||
// Only an absent variable takes the default; an explicit empty value is
|
||||
// invalid below.
|
||||
rawPublicBaseURL, present := env["PUBLIC_BASE_URL"]
|
||||
if !present {
|
||||
rawPublicBaseURL = fmt.Sprintf("http://localhost:%d", port)
|
||||
@@ -200,9 +193,6 @@ func Load(env map[string]string) (Config, error) {
|
||||
return Config{}, fmt.Errorf("SITE_ACCESS_PASSWORD must contain %d to %d visible ASCII bytes",
|
||||
minSiteAccessPasswordBytes, maxSiteAccessPasswordBytes)
|
||||
}
|
||||
if environment == EnvironmentProduction && siteAccessPassword == "" {
|
||||
return Config{}, errors.New("SITE_ACCESS_PASSWORD is required in production")
|
||||
}
|
||||
if environment == EnvironmentProduction && len(stunURLs) == 0 {
|
||||
return Config{}, errors.New("STUN is required in production")
|
||||
}
|
||||
@@ -231,8 +221,6 @@ func Load(env map[string]string) (Config, error) {
|
||||
|
||||
// parseEnvironment ports parseEnvironment for PIIK_ENV.
|
||||
func parseEnvironment(env map[string]string) (Environment, error) {
|
||||
// TS: value ?? "development" — a present empty value is not nullish and so
|
||||
// reaches the check below.
|
||||
value, present := env["PIIK_ENV"]
|
||||
if !present {
|
||||
return EnvironmentDevelopment, nil
|
||||
@@ -342,8 +330,7 @@ func parseURLList(value string, name string) ([]string, error) {
|
||||
return list, nil
|
||||
}
|
||||
|
||||
// parseStunURLList ports parseStunUrlList. The TypeScript `maximum` parameter
|
||||
// was only ever the default.
|
||||
// parseStunURLList validates the bounded ordinary-STUN list.
|
||||
func parseStunURLList(value string, name string) ([]string, error) {
|
||||
values, err := parseURLList(value, name)
|
||||
if err != nil {
|
||||
@@ -380,8 +367,7 @@ func parseOrigins(value string, fallback string) (map[string]struct{}, error) {
|
||||
return set, nil
|
||||
}
|
||||
|
||||
// toOrigin ports toOrigin. The TypeScript let an unparsable entry escape as a
|
||||
// raw TypeError from `new URL`; it fails startup here with a message instead.
|
||||
// toOrigin validates one allowed HTTP(S) origin.
|
||||
func toOrigin(value string) (string, error) {
|
||||
parsed, err := url.Parse(value)
|
||||
if err != nil || parsed.Scheme == "" || parsed.Host == "" {
|
||||
@@ -405,8 +391,7 @@ func parseOriginURL(value, name, scheme, alternative string) (*url.URL, error) {
|
||||
if parsed.Scheme != scheme && parsed.Scheme != alternative {
|
||||
return nil, fmt.Errorf("%s must use %s or %s", name, scheme, alternative)
|
||||
}
|
||||
// TS: url.username || url.password || url.pathname !== "/" || url.search ||
|
||||
// url.hash. WHATWG reports "/" for an empty path, which net/url leaves "".
|
||||
// WHATWG reports "/" for an empty path, while net/url leaves it "".
|
||||
if hasUserinfo(parsed) || (parsed.Path != "" && parsed.Path != "/") ||
|
||||
parsed.RawQuery != "" || parsed.Fragment != "" {
|
||||
return nil, fmt.Errorf(
|
||||
|
||||
@@ -98,6 +98,21 @@ func TestLoadDevelopmentDefaults(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadProductionAllowsAnUnsetOrEmptySiteAccessPassword(t *testing.T) {
|
||||
for _, name := range []string{"unset", "empty"} {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
environment := env(productionBase, map[string]string{"SITE_ACCESS_PASSWORD": ""})
|
||||
if name == "unset" {
|
||||
delete(environment, "SITE_ACCESS_PASSWORD")
|
||||
}
|
||||
configuration := mustLoad(t, environment)
|
||||
if configuration.Env != EnvironmentProduction || configuration.SiteAccessPassword != "" {
|
||||
t.Fatal("expected a production site without an access password")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadRoomStorage(t *testing.T) {
|
||||
directory := t.TempDir()
|
||||
t.Chdir(directory)
|
||||
@@ -377,10 +392,6 @@ func TestLoadRejects(t *testing.T) {
|
||||
"PIIK_ENV": "production", "PUBLIC_BASE_URL": "https://share.test",
|
||||
"SITE_ACCESS_PASSWORD": "host-password-12",
|
||||
}, "STUN is required in production"},
|
||||
{"production without a site password", map[string]string{
|
||||
"PIIK_ENV": "production", "PUBLIC_BASE_URL": "https://share.test",
|
||||
"STUN_URLS": "stun:stun.test:3478",
|
||||
}, "SITE_ACCESS_PASSWORD is required in production"},
|
||||
|
||||
// Site access password bounds (UTF-8 bytes).
|
||||
{"site password too short", map[string]string{"SITE_ACCESS_PASSWORD": strings.Repeat("x", 7)},
|
||||
|
||||
@@ -10,12 +10,11 @@ import (
|
||||
"github.com/TNTcraftHIM/Piik/internal/server/protocol"
|
||||
)
|
||||
|
||||
// Ported from src/server/ice.ts.
|
||||
const natPredictionBasePort = 3478
|
||||
|
||||
// natPredictionAuxiliaryPorts are NAT_PREDICTION_AUXILIARY_PORTS: the two
|
||||
// listeners a NAT-prediction STUN deployment binds beside the base port, whose
|
||||
// answers reveal the peer's port-allocation stride.
|
||||
// natPredictionAuxiliaryPorts are the two listeners a NAT-prediction STUN
|
||||
// deployment binds beside the base port; their answers reveal the peer's
|
||||
// port-allocation stride.
|
||||
var natPredictionAuxiliaryPorts = [...]int{natPredictionBasePort + 1, natPredictionBasePort + 2}
|
||||
|
||||
func stunListenAddresses(host string, prediction bool) ([]string, error) {
|
||||
@@ -40,8 +39,6 @@ func stunListenAddresses(host string, prediction bool) ([]string, error) {
|
||||
// listeners derived from the first ordinary STUN authority on UDP 3478.
|
||||
func NATPredictionStunURLs(stunURLs []string) []string {
|
||||
for _, stunURL := range stunURLs {
|
||||
// TS: stunUrl.slice(stunUrl.indexOf(":") + 1) — a URL without a colon
|
||||
// keeps its whole text, which then fails to yield a host below.
|
||||
authorityText := stunURL[strings.IndexByte(stunURL, ':')+1:]
|
||||
authority, err := url.Parse("http://" + authorityText)
|
||||
if err != nil || authority.Hostname() == "" {
|
||||
@@ -77,8 +74,7 @@ func NATPredictionStunURLs(stunURLs []string) []string {
|
||||
|
||||
// IceConfig ports createIceConfig.
|
||||
func IceConfig(c Config) protocol.IceConfig {
|
||||
// TS: options.natPredictionStunUrls ?? natPredictionStunUrls(stunUrls). A
|
||||
// configured empty list (Local without prediction) is kept as-is; only an
|
||||
// A configured empty list (Local without prediction) is kept as-is; only an
|
||||
// absent one is derived, so nil and empty differ here.
|
||||
prediction := c.NATPredictionSTUNURLs
|
||||
if prediction == nil {
|
||||
|
||||
@@ -12,18 +12,13 @@ import (
|
||||
"github.com/TNTcraftHIM/Piik/internal/server/protocol"
|
||||
)
|
||||
|
||||
// localPasswordPattern is VISIBLE_ASCII_PATTERN from local-config.ts. Because
|
||||
// it admits one byte per character the TypeScript's extra Buffer.byteLength
|
||||
// bounds are already implied by {8,128}.
|
||||
// localPasswordPattern admits 8..128 one-byte visible ASCII characters.
|
||||
var localPasswordPattern = regexp.MustCompile(`^[\x21-\x7e]{8,128}$`)
|
||||
|
||||
// LocalOptions ports LocalServerConfigOptions. loadLocalServerConfig, the
|
||||
// environment form, went away with the Node child process, so Local never
|
||||
// reads the process environment.
|
||||
// LocalOptions describes App-selected Local composition. It never reads the
|
||||
// process environment.
|
||||
type LocalOptions struct {
|
||||
// Port of 0 selects the default 8787, as TypeScript's absent `port` did.
|
||||
// An explicit 0 is indistinguishable from absent here, where TypeScript
|
||||
// rejected it; 0 is not a usable port either way.
|
||||
// Port 0 selects the default 8787.
|
||||
Port int
|
||||
PublicAddress string
|
||||
PublicOrigin string
|
||||
@@ -65,8 +60,7 @@ func Local(options LocalOptions) (Config, error) {
|
||||
}
|
||||
}
|
||||
|
||||
// TS spreads `?? []`, so both lists are always arrays; keeping them non-nil
|
||||
// is what tells IceConfig that NAT prediction URLs were configured.
|
||||
// Copy non-nil lists so configured-empty prediction does not derive auxiliary URLs.
|
||||
stunURLs := append([]string{}, options.STUNURLs...)
|
||||
natPredictionSTUNURLs := append([]string{}, options.NATPredictionSTUNURLs...)
|
||||
if len(stunURLs)+len(natPredictionSTUNURLs) > protocol.MaxIceServerURLs ||
|
||||
@@ -78,9 +72,7 @@ func Local(options LocalOptions) (Config, error) {
|
||||
return Config{}, errors.New("Local STUN URLs are invalid")
|
||||
}
|
||||
|
||||
// TS builds these with a template string, so a non-default port is always
|
||||
// spelled out and port 80 would produce "http://host:80", which is not the
|
||||
// serialised origin. Kept as-is: Local never runs on 80.
|
||||
// Local is loopback/LAN HTTP; its origin always spells out the port.
|
||||
origin := func(host string) string { return fmt.Sprintf("http://%s:%d", host, port) }
|
||||
publicBaseURL := &url.URL{Scheme: "http", Host: fmt.Sprintf("%s:%d", publicAddress, port), Path: "/"}
|
||||
if options.PublicOrigin != "" {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// Package ordered provides an insertion-ordered map with the iteration
|
||||
// semantics of a JavaScript Map. The TypeScript server relied on those
|
||||
// semantics wherever it iterated a Map or Set: wire-visible participant order,
|
||||
// candidate ordering, round-robin state, and delete-during-iteration loops.
|
||||
// Package ordered provides an insertion-ordered map with live iteration.
|
||||
// Wire-visible participant order, candidate ordering, round-robin state and
|
||||
// deletion during iteration depend on those semantics.
|
||||
package ordered
|
||||
|
||||
import "iter"
|
||||
@@ -13,9 +12,9 @@ type entry[K comparable, V any] struct {
|
||||
}
|
||||
|
||||
// Map keeps keys in first-insertion order. Set on an existing key keeps its
|
||||
// position; MoveToBack reproduces the JavaScript delete-then-set idiom.
|
||||
// Iteration is live: an entry deleted before it is visited is skipped and an
|
||||
// entry appended during iteration is visited, exactly like a JavaScript Map.
|
||||
// position; MoveToBack removes and reinserts it at the end. Iteration is live:
|
||||
// an entry deleted before it is visited is skipped and an entry appended
|
||||
// during iteration is visited.
|
||||
// The zero value is ready to use.
|
||||
type Map[K comparable, V any] struct {
|
||||
entries []entry[K, V]
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
// Package protocol is the Go owner of the Piik wire contract. It mirrors
|
||||
// src/shared/protocol.ts (zod schemas) plus the four shared scalar helpers
|
||||
// (media-copy-accounting.ts, nat-candidate.ts, packet-loss.ts,
|
||||
// video-codec-evidence.ts). The package performs no I/O and imports nothing
|
||||
// from other internal packages.
|
||||
// Package protocol is the Go owner of the Piik wire contract. Its shared
|
||||
// fixtures and scalar helpers are kept semantics-compatible with the Browser
|
||||
// implementation. The package performs no I/O and imports nothing from other
|
||||
// internal packages.
|
||||
package protocol
|
||||
|
||||
// MaxSafeInteger is JavaScript's Number.MAX_SAFE_INTEGER.
|
||||
const MaxSafeInteger = 9007199254740991
|
||||
|
||||
// Limits and identifiers mirrored from src/shared/protocol.ts.
|
||||
// Limits and identifiers in the Piik wire contract.
|
||||
const (
|
||||
MaxViewersPerRoomLimit = 20
|
||||
MaxParticipantsPerRoomLimit = MaxViewersPerRoomLimit + 1
|
||||
@@ -27,7 +26,7 @@ const (
|
||||
|
||||
MaxDisplayNameCodePoints = 24
|
||||
DefaultViewerDisplayName = "观众"
|
||||
DefaultHostDisplayNamePrefix = "分享者"
|
||||
DefaultHostDisplayNamePrefix = "房主"
|
||||
|
||||
MinViewerPasswordLength = 1
|
||||
MaxViewerPasswordLength = 64
|
||||
|
||||
@@ -8,6 +8,33 @@ import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestRuntimeCapabilities(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
data string
|
||||
want RuntimeCapabilities
|
||||
}{
|
||||
{`{}`, RuntimeCapabilities{}},
|
||||
{`{"sfu":true,"extra":{"enabled":true}}`, RuntimeCapabilities{Sfu: true}},
|
||||
{`{"natPrediction":true,"SFU":true}`, RuntimeCapabilities{NatPrediction: true}},
|
||||
{`{"sfu":true,"natPrediction":true}`, RuntimeCapabilities{Sfu: true, NatPrediction: true}},
|
||||
{`{"sfu":"ignored duplicate","sfu":true}`, RuntimeCapabilities{Sfu: true}},
|
||||
} {
|
||||
got := RuntimeCapabilities{Sfu: true, NatPrediction: true}
|
||||
if err := json.Unmarshal([]byte(test.data), &got); err != nil || got != test.want {
|
||||
t.Errorf("decode %s = %+v, %v; want %+v", test.data, got, err, test.want)
|
||||
}
|
||||
}
|
||||
for _, data := range []string{
|
||||
`null`, `[]`, `true`, `{"sfu":null}`, `{"sfu":"true"}`,
|
||||
`{"natPrediction":null}`, `{"natPrediction":1}`,
|
||||
} {
|
||||
var got RuntimeCapabilities
|
||||
if err := json.Unmarshal([]byte(data), &got); err == nil {
|
||||
t.Errorf("expected %s to be rejected", data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// member is one JSON object key with its raw encoded value, so the tests can
|
||||
// reproduce the exact wire shapes from tests/protocol.test.ts (including key
|
||||
// presence, which a Go map cannot express).
|
||||
|
||||
@@ -11,10 +11,7 @@ type ServerMessage interface {
|
||||
isServerMessage()
|
||||
}
|
||||
|
||||
// AuthenticatedHostMessage is the host member of authenticatedMessageSchema.
|
||||
// The field order is the emission order of the object literal in
|
||||
// src/server/signaling.ts (`role` and `viewerPasswordEnabled` are appended
|
||||
// after the shared base spread).
|
||||
// AuthenticatedHostMessage returns the Host's current room and transport facts.
|
||||
type AuthenticatedHostMessage struct {
|
||||
Type string `json:"type"`
|
||||
Protocol string `json:"protocol"`
|
||||
|
||||
@@ -168,18 +168,33 @@ func (v *RoutePolicy) UnmarshalJSON(data []byte) error {
|
||||
|
||||
// RuntimeCapabilities mirrors runtimeCapabilitiesSchema.
|
||||
type RuntimeCapabilities struct {
|
||||
Sfu bool `json:"sfu"`
|
||||
NatPrediction bool `json:"natPrediction"`
|
||||
}
|
||||
|
||||
// UnmarshalJSON implements json.Unmarshaler.
|
||||
func (v *RuntimeCapabilities) UnmarshalJSON(data []byte) error {
|
||||
*v = RuntimeCapabilities{}
|
||||
type raw RuntimeCapabilities
|
||||
present, err := decodeObject(data, (*raw)(v))
|
||||
if err != nil {
|
||||
var present fields
|
||||
if err := json.Unmarshal(data, &present); err != nil {
|
||||
return err
|
||||
}
|
||||
return present.require("natPrediction")
|
||||
if present == nil {
|
||||
return errors.New("runtime capabilities must be an object")
|
||||
}
|
||||
if err := present.optional("sfu", "natPrediction"); err != nil {
|
||||
return err
|
||||
}
|
||||
for key, target := range map[string]*bool{
|
||||
"sfu": &v.Sfu, "natPrediction": &v.NatPrediction,
|
||||
} {
|
||||
if value, ok := present[key]; ok {
|
||||
if err := json.Unmarshal(value, target); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
+20
-80
@@ -1,87 +1,27 @@
|
||||
// Package room ports the room authority: src/server/room-store.ts (rooms,
|
||||
// participants, room codes, viewer credentials, the four-digit code pool) and
|
||||
// src/server/room-database.ts (the SQLite stable authority). Authority mutations
|
||||
// write SQLite before memory and return the write error unchanged. Participants
|
||||
// remain process-only.
|
||||
// Package room owns room codes, Host/Viewer credentials, participant membership
|
||||
// and persistent room authority. Durable mutations write SQLite before memory;
|
||||
// participants and live connections remain process-only.
|
||||
//
|
||||
// # Locking
|
||||
// # Locking And Password Work
|
||||
//
|
||||
// Store has no mutex. The signaling layer's single global lock guards it, which
|
||||
// is what Node's single thread did: nothing interleaved except at an await. The
|
||||
// caller must hold that lock for every method below except the two derive
|
||||
// methods, which run the password KDF and must NOT be called under the lock.
|
||||
// Store has no mutex. The caller holds the signaling server's global lock for
|
||||
// store operations, except password derivation, which must run unlocked.
|
||||
//
|
||||
// The TypeScript methods that await the KDF are split into a locked "begin",
|
||||
// an unlocked derive, and a locked "commit" so that the guards the TypeScript
|
||||
// re-ran after its await are re-run here after the relock, in the same order.
|
||||
// The commit takes the derive result as (material, derived error) because the
|
||||
// TypeScript inspected the gate outcome (busy/cancelled) only after re-reading
|
||||
// the room: forward both values unchanged and the commit reproduces the order.
|
||||
// Password changes use a locked begin, unlocked derive, then locked commit.
|
||||
// Pass both material and error from DeriveViewerPasswordMaterial unchanged;
|
||||
// the commit revalidates room ownership/capacity before mapping gate outcomes.
|
||||
// Viewer login instead returns derivation errors before committing credentials.
|
||||
//
|
||||
// POST /api/rooms (createRoom):
|
||||
// A derivation admission predicate runs outside the signaling lock and must
|
||||
// acquire that lock when consulting room/session state. ConnectViewerWithPassword
|
||||
// also calls its predicate during the locked commit, so that call needs the
|
||||
// already-locked form. Both must express the same current-session condition.
|
||||
//
|
||||
// mu.Lock()
|
||||
// if err := store.BeginCreateRoom(); err != nil { mu.Unlock(); return err }
|
||||
// var material []byte
|
||||
// var derived error
|
||||
// if password != "" {
|
||||
// mu.Unlock()
|
||||
// material, derived = store.DeriveViewerPasswordMaterial(password, nil)
|
||||
// mu.Lock()
|
||||
// }
|
||||
// created, err := store.CreateRoom(policy, material, derived, preferredRoomID)
|
||||
// mu.Unlock()
|
||||
// HostManagedRoom and ViewerPasswordChallenge return a room reference for
|
||||
// identity checks after unlocked work. A removed room is never reused as its
|
||||
// replacement; a stale reference cannot authorize mutation of the new room.
|
||||
// Do not inspect a retained room's mutable state without the lock.
|
||||
//
|
||||
// replaceRoom and set-viewer-password (the mayStart closure is the TypeScript
|
||||
// `rooms.get(roomId) === current && verifyDigest(hostToken, ...)`; it runs on
|
||||
// the derive goroutine, so it takes the lock itself):
|
||||
//
|
||||
// mu.Lock()
|
||||
// current, err := store.HostManagedRoom(roomID, hostToken) // may fail INVALID_TOKEN
|
||||
// if err != nil { mu.Unlock(); return err }
|
||||
// var material []byte
|
||||
// var derived error
|
||||
// if password != "" {
|
||||
// mu.Unlock()
|
||||
// material, derived = store.DeriveViewerPasswordMaterial(password, func() bool {
|
||||
// mu.Lock()
|
||||
// defer mu.Unlock()
|
||||
// return store.HostStillOwnsRoom(roomID, hostToken, current)
|
||||
// })
|
||||
// mu.Lock()
|
||||
// }
|
||||
// replaced, err := store.ReplaceRoom(roomID, hostToken, policy, material, derived, current)
|
||||
// mu.Unlock()
|
||||
//
|
||||
// Viewer password login (signaling authenticate). mayConnect is the TypeScript
|
||||
// closure `!closing && socketStates.get(socket) === state && !state.revoked &&
|
||||
// !state.authenticated && socket.readyState === OPEN`. It is evaluated twice,
|
||||
// once on the derive goroutine (gate admission) and once under the lock inside
|
||||
// the commit, so the caller needs both a locking and a non-locking form of the
|
||||
// same predicate:
|
||||
//
|
||||
// mu.Lock()
|
||||
// salt, expected, ref, err := store.ViewerPasswordChallenge(input.RoomID)
|
||||
// mu.Unlock()
|
||||
// if err != nil { return err } // not initialized
|
||||
// derivedKey, err := store.DeriveViewerPassword(input.Password, salt, func() bool {
|
||||
// mu.Lock()
|
||||
// defer mu.Unlock()
|
||||
// return mayConnectLocked()
|
||||
// })
|
||||
// mu.Lock()
|
||||
// if err != nil { mu.Unlock(); return err } // ROOM_BUSY or INVALID_TOKEN
|
||||
// participant, err := store.ConnectViewerWithPassword(
|
||||
// input, derivedKey, expected, ref, mayConnectLocked)
|
||||
// mu.Unlock()
|
||||
//
|
||||
// The room reference handed back by HostManagedRoom and ViewerPasswordChallenge
|
||||
// is only ever compared for pointer identity, reproducing the TypeScript
|
||||
// `currentRoom !== room` checks that detect a room replaced while the KDF ran.
|
||||
// Holding it across the unlock is safe: an abandoned room is dropped from the
|
||||
// map, never mutated into another room.
|
||||
//
|
||||
// Options.Random is called both under the caller's lock and from the derive
|
||||
// goroutine (the 16-byte salt), so it must be safe for concurrent use;
|
||||
// crypto/rand is.
|
||||
// Options.Random is used under the caller's lock and by derivation goroutines,
|
||||
// so it must be concurrency-safe; crypto/rand satisfies this requirement.
|
||||
package room
|
||||
@@ -19,8 +19,7 @@ const (
|
||||
viewerPasswordScryptR = 8
|
||||
viewerPasswordScryptP = 1
|
||||
|
||||
// AsyncGate bounds from src/server/room-store.ts (DECISIONS D11): scrypt at
|
||||
// N=16384 costs ~16 MiB and tens of milliseconds, so two concurrent
|
||||
// Scrypt at N=16384 costs ~16 MiB per derivation, so two concurrent
|
||||
// derivations bound the memory and CPU a room burst can claim, and sixteen
|
||||
// waiters bound the queue before the next caller is told ROOM_BUSY.
|
||||
gateActiveLimit = 2
|
||||
|
||||
@@ -229,20 +229,6 @@ type CommittedEdge struct {
|
||||
Resource *Resource
|
||||
}
|
||||
|
||||
// committedEdgeSeed is CommittedEdge without the session fields the
|
||||
// controller derives at hydrateEdge. TS: CommittedEdgeSeed, which like
|
||||
// hydrateEdge only ever had test callers.
|
||||
type committedEdgeSeed struct {
|
||||
Kind UpstreamKind
|
||||
ParentPeerID string
|
||||
PublicationGeneration string
|
||||
Transport Transport
|
||||
ConnectionID string
|
||||
Usable bool
|
||||
PhysicalActive bool
|
||||
Resource *Resource
|
||||
}
|
||||
|
||||
// HostPublication is the single live SFU ingress. TS: HostPublication.
|
||||
type HostPublication struct {
|
||||
Generation string
|
||||
@@ -288,7 +274,6 @@ type OperationSnapshot struct {
|
||||
// RouteSnapshot is the committed graph as the router reads it.
|
||||
// UpstreamByViewer is a fresh insertion-ordered copy (child peerId -> edge);
|
||||
// HostPublication is a shallow copy or nil; Operation is nil when idle.
|
||||
// TS: RouteSnapshot.
|
||||
type RouteSnapshot struct {
|
||||
Revision int64
|
||||
Paused bool
|
||||
@@ -323,7 +308,6 @@ type SettleResult struct {
|
||||
type SenderQualityEvidenceResult = SettleResult
|
||||
|
||||
// BeginResult is the outcome of the cursor-guarded candidate calls.
|
||||
// TS: BeginResult.
|
||||
type BeginResult struct {
|
||||
Accepted bool
|
||||
Operation *OperationSnapshot
|
||||
@@ -351,7 +335,6 @@ type EdgeGuard struct {
|
||||
}
|
||||
|
||||
// ParentEdgeGuard identifies a direct edge from the parent's side.
|
||||
// TS: the inline input of invalidateDirectEdgeFromParent.
|
||||
type ParentEdgeGuard struct {
|
||||
ParentPeerID string
|
||||
ParentSessionID string
|
||||
@@ -410,7 +393,6 @@ type BeginInput struct {
|
||||
}
|
||||
|
||||
// CandidateProof carries the Viewer's one-shot relative quality approval.
|
||||
// TS: the proof parameter of candidateReady ({ relativeQualityApproved? }).
|
||||
type CandidateProof struct {
|
||||
RelativeQualityApproved bool
|
||||
}
|
||||
@@ -425,7 +407,6 @@ type QualityUpstream struct {
|
||||
// RouteQualityEvidenceInput is one receiver-side quality window. Only
|
||||
// FramesDecodedDelta, FreezeCountDelta, FreezeDurationMsDelta,
|
||||
// PauseCountDelta and PauseDurationMsDelta of Metrics are read.
|
||||
// TS: RouteQualityEvidenceInput.
|
||||
type RouteQualityEvidenceInput struct {
|
||||
ChildPeerID string
|
||||
ChildSessionID string
|
||||
@@ -440,7 +421,6 @@ type RouteQualityEvidenceInput struct {
|
||||
|
||||
// SenderQualityEvidenceInput is one sender-side health sample from a parent
|
||||
// about one child. SenderIdentity and SampleTimestampMs are nullable.
|
||||
// TS: SenderQualityEvidenceInput.
|
||||
type SenderQualityEvidenceInput struct {
|
||||
ParentPeerID string
|
||||
ParentSessionID string
|
||||
@@ -454,7 +434,6 @@ type SenderQualityEvidenceInput struct {
|
||||
}
|
||||
|
||||
// SfuPublisherQualityEvidenceInput is one Host->SFU ingress health sample.
|
||||
// TS: SfuPublisherQualityEvidenceInput.
|
||||
type SfuPublisherQualityEvidenceInput struct {
|
||||
HostPeerID string
|
||||
HostSessionID string
|
||||
@@ -468,8 +447,11 @@ type SfuPublisherQualityEvidenceInput struct {
|
||||
// Options configures a Controller. DebugRoomID empty disables the sanitised
|
||||
// route debug events. TS: ControllerOptions.
|
||||
type Options struct {
|
||||
HostPeerID string
|
||||
DebugRoomID string
|
||||
HostPeerID string
|
||||
DebugRoomID string
|
||||
// DebugLog receives sanitized route events. nil disables them; the effect
|
||||
// layer owns environment, logger and retention policy.
|
||||
DebugLog func(string, ...any)
|
||||
EndpointMediaCopyCapacity int
|
||||
OperationTimeoutMs int64
|
||||
SfuEnabled bool
|
||||
@@ -487,6 +469,7 @@ type Controller struct {
|
||||
// options are copied once and never change.
|
||||
hostPeerID string
|
||||
debugRoomID string
|
||||
debugLog func(string, ...any)
|
||||
endpointMediaCopyCapacity int
|
||||
operationTimeoutMs int64
|
||||
sfuEnabled bool
|
||||
|
||||
@@ -275,6 +275,7 @@ func New(options Options) *Controller {
|
||||
c := &Controller{
|
||||
hostPeerID: options.HostPeerID,
|
||||
debugRoomID: options.DebugRoomID,
|
||||
debugLog: options.DebugLog,
|
||||
endpointMediaCopyCapacity: options.EndpointMediaCopyCapacity,
|
||||
operationTimeoutMs: options.OperationTimeoutMs,
|
||||
sfuEnabled: options.SfuEnabled,
|
||||
@@ -642,68 +643,6 @@ func (c *Controller) TouchExternalFacts() {
|
||||
c.touchFacts()
|
||||
}
|
||||
|
||||
// hydrateEdge installs a committed edge for a connected viewer while no
|
||||
// operation is active, without changing revision or fact version. Panics
|
||||
// on an active operation, a non-viewer child or a child without a session.
|
||||
func (c *Controller) hydrateEdge(childPeerID string, edge committedEdgeSeed) {
|
||||
if c.operation != nil {
|
||||
panic(errors.New("Cannot hydrate while an operation is active"))
|
||||
}
|
||||
c.assertViewer(childPeerID)
|
||||
child, _ := c.participants.Get(childPeerID)
|
||||
childSessionID := child.sessionID
|
||||
if childSessionID == "" {
|
||||
panic(errors.New("Route child has no current session"))
|
||||
}
|
||||
committed := &CommittedEdge{
|
||||
Kind: edge.Kind,
|
||||
ChildSessionID: childSessionID,
|
||||
ParentPeerID: edge.ParentPeerID,
|
||||
PublicationGeneration: edge.PublicationGeneration,
|
||||
Transport: edge.Transport,
|
||||
ConnectionID: edge.ConnectionID,
|
||||
Usable: edge.Usable,
|
||||
PhysicalActive: edge.PhysicalActive,
|
||||
Resource: edge.Resource,
|
||||
}
|
||||
if edge.Kind == UpstreamPeer {
|
||||
// TS 750: `this.participants.get(edge.parentPeerId)?.sessionId ?? ""`.
|
||||
if parent, ok := c.participants.Get(edge.ParentPeerID); ok {
|
||||
committed.ParentSessionID = parent.sessionID
|
||||
}
|
||||
}
|
||||
// Map.set keeps the position of an existing child (§4.2 #15).
|
||||
c.upstreamByViewer.Set(childPeerID, committed)
|
||||
c.requireSenderQualityBaseline(childPeerID, false)
|
||||
c.assertGraph()
|
||||
}
|
||||
|
||||
// hydrateHostPublication installs a usable, physically active publication
|
||||
// while no operation is active; connectionID empty selects the TS default
|
||||
// "publication:<generation>". Panics on an active operation or a Host
|
||||
// without a session.
|
||||
func (c *Controller) hydrateHostPublication(generation string, resource *Resource, connectionID string) {
|
||||
if c.operation != nil {
|
||||
panic(errors.New("Cannot hydrate while an operation is active"))
|
||||
}
|
||||
host, _ := c.participants.Get(c.hostPeerID)
|
||||
if host == nil || host.sessionID == "" {
|
||||
panic(errors.New("Route Host has no current session"))
|
||||
}
|
||||
if connectionID == "" {
|
||||
connectionID = "publication:" + generation
|
||||
}
|
||||
c.hostPublication = &HostPublication{
|
||||
Generation: generation,
|
||||
HostSessionID: host.sessionID,
|
||||
ConnectionID: connectionID,
|
||||
Usable: true,
|
||||
PhysicalActive: true,
|
||||
Resource: resource,
|
||||
}
|
||||
c.assertGraph()
|
||||
}
|
||||
|
||||
// InvalidateEdge marks the exactly identified edge unusable and records an
|
||||
// "edge-unavailable" demand; true when the guard matched (idempotent).
|
||||
func (c *Controller) InvalidateEdge(guard EdgeGuard, nowMs *int64) bool {
|
||||
|
||||
@@ -1,40 +1,26 @@
|
||||
package route
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/TNTcraftHIM/Piik/internal/server/protocol"
|
||||
)
|
||||
|
||||
func debugEnabled() bool {
|
||||
return routeDebugEnabled(os.Getenv("PIIK_DEBUG")) ||
|
||||
slog.Default().Enabled(context.Background(), slog.LevelDebug)
|
||||
}
|
||||
|
||||
func routeDebugEnabled(value string) bool {
|
||||
for _, section := range strings.Split(value, ",") {
|
||||
if strings.TrimSpace(section) == "route" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// debugCandidate is the `{route, transition}` item of "operation-started".
|
||||
type debugCandidate struct {
|
||||
Route string
|
||||
Transition TransitionKind
|
||||
}
|
||||
|
||||
func (c *Controller) debugEnabled() bool {
|
||||
return c.debugLog != nil && c.debugRoomID != ""
|
||||
}
|
||||
|
||||
// debug emits one sanitized event with a diagnostic room ID and current facts.
|
||||
// Never pass raw peer IDs, session IDs or connection IDs; use debugPeer and
|
||||
// debugTuple. The environment or diagnostic logger enables these Info events.
|
||||
// debugTuple. The effect layer injects the sink and owns its enabling policy.
|
||||
func (c *Controller) debug(event string, details ...any) {
|
||||
if !debugEnabled() || c.debugRoomID == "" {
|
||||
if !c.debugEnabled() {
|
||||
return
|
||||
}
|
||||
args := make([]any, 0, 8+len(details))
|
||||
@@ -44,7 +30,7 @@ func (c *Controller) debug(event string, details ...any) {
|
||||
"revision", c.revision,
|
||||
"factVersion", c.factVersion)
|
||||
args = append(args, details...)
|
||||
slog.Info("piik-route", args...)
|
||||
c.debugLog("piik-route", args...)
|
||||
}
|
||||
|
||||
// debugPeer ports 4895: "host", "viewer-<joinOrder>" or "viewer-unknown".
|
||||
|
||||
@@ -1,24 +1,26 @@
|
||||
package route
|
||||
|
||||
import (
|
||||
"io"
|
||||
"log/slog"
|
||||
"testing"
|
||||
)
|
||||
import "testing"
|
||||
|
||||
func TestDebugUsesCurrentEnvironmentAndLogger(t *testing.T) {
|
||||
previous := slog.Default()
|
||||
t.Cleanup(func() { slog.SetDefault(previous) })
|
||||
slog.SetDefault(slog.New(slog.NewTextHandler(io.Discard, nil)))
|
||||
for value, want := range map[string]bool{"": false, "client": false, "route": true, "client, route": true} {
|
||||
t.Setenv("PIIK_DEBUG", value)
|
||||
if got := debugEnabled(); got != want {
|
||||
t.Fatalf("debugEnabled with %q = %v, want %v", value, got, want)
|
||||
func TestControllerDebugRequiresInjectedSinkAndRoom(t *testing.T) {
|
||||
var kinds, events []string
|
||||
sink := func(kind string, args ...any) {
|
||||
kinds = append(kinds, kind)
|
||||
if len(args) >= 2 {
|
||||
if event, ok := args[1].(string); ok {
|
||||
events = append(events, event)
|
||||
}
|
||||
}
|
||||
}
|
||||
t.Setenv("PIIK_DEBUG", "")
|
||||
slog.SetDefault(slog.New(slog.NewTextHandler(io.Discard, &slog.HandlerOptions{Level: slog.LevelDebug})))
|
||||
if !debugEnabled() {
|
||||
t.Fatal("the active diagnostic logger did not enable route events")
|
||||
c := &Controller{debugRoomID: "1234", debugLog: sink}
|
||||
c.debug("operation-started", "child", "viewer-1")
|
||||
if len(kinds) != 1 || kinds[0] != "piik-route" ||
|
||||
len(events) != 1 || events[0] != "operation-started" {
|
||||
t.Fatalf("injected sink received kinds=%v events=%v", kinds, events)
|
||||
}
|
||||
(&Controller{debugRoomID: "1234"}).debug("no-sink")
|
||||
(&Controller{debugLog: sink}).debug("no-room")
|
||||
if len(kinds) != 1 {
|
||||
t.Fatalf("disabled controller emitted %v", kinds)
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,14 @@
|
||||
// Package route ports src/server/room-route-controller.ts: the pure,
|
||||
// synchronous decision logic that owns one room's media route graph, its
|
||||
// Package route owns synchronous decisions over one room's media route graph, its
|
||||
// single in-flight route operation, the quality/opportunity ledgers and the
|
||||
// privacy-safe diagnostic snapshot.
|
||||
//
|
||||
// The controller performs no I/O, starts no timers and never locks. Every
|
||||
// method is a state transition over the nowMs the caller passes in; the
|
||||
// The controller starts no timers, never locks and returns resource effects
|
||||
// to its caller. Optional diagnostics invoke the injected sink synchronously.
|
||||
// Every method is a state transition over the nowMs the caller passes in; the
|
||||
// caller (signal.Server) holds the global mutex around every call and
|
||||
// releases exactly the *Resource values a method returns, in order.
|
||||
//
|
||||
// Resources are compared by pointer identity, mirroring the JS Set semantics
|
||||
// of the TypeScript source. Programming-error throws in the source
|
||||
// (assertGraph, bad reservations, bad options) become panics with the same
|
||||
// messages.
|
||||
// Resources are compared by pointer identity. Invalid graphs, reservations or
|
||||
// constructor options are programming errors and panic; ordinary unavailable
|
||||
// routes remain decision outcomes.
|
||||
package route
|
||||
@@ -212,6 +212,69 @@ func sfuCreateOverlap(edge, publication, overlap string) CandidateReservation {
|
||||
return reservation
|
||||
}
|
||||
|
||||
type committedEdgeSeed struct {
|
||||
Kind UpstreamKind
|
||||
ParentPeerID string
|
||||
PublicationGeneration string
|
||||
Transport Transport
|
||||
ConnectionID string
|
||||
Usable bool
|
||||
PhysicalActive bool
|
||||
Resource *Resource
|
||||
}
|
||||
|
||||
func (c *Controller) hydrateEdge(childPeerID string, edge committedEdgeSeed) {
|
||||
if c.operation != nil {
|
||||
panic("Cannot hydrate while an operation is active")
|
||||
}
|
||||
c.assertViewer(childPeerID)
|
||||
child, _ := c.participants.Get(childPeerID)
|
||||
if child.sessionID == "" {
|
||||
panic("Route child has no current session")
|
||||
}
|
||||
committed := &CommittedEdge{
|
||||
Kind: edge.Kind,
|
||||
ChildSessionID: child.sessionID,
|
||||
ParentPeerID: edge.ParentPeerID,
|
||||
PublicationGeneration: edge.PublicationGeneration,
|
||||
Transport: edge.Transport,
|
||||
ConnectionID: edge.ConnectionID,
|
||||
Usable: edge.Usable,
|
||||
PhysicalActive: edge.PhysicalActive,
|
||||
Resource: edge.Resource,
|
||||
}
|
||||
if edge.Kind == UpstreamPeer {
|
||||
if parent, ok := c.participants.Get(edge.ParentPeerID); ok {
|
||||
committed.ParentSessionID = parent.sessionID
|
||||
}
|
||||
}
|
||||
c.upstreamByViewer.Set(childPeerID, committed)
|
||||
c.requireSenderQualityBaseline(childPeerID, false)
|
||||
c.assertGraph()
|
||||
}
|
||||
|
||||
func (c *Controller) hydrateHostPublication(generation string, resource *Resource, connectionID string) {
|
||||
if c.operation != nil {
|
||||
panic("Cannot hydrate while an operation is active")
|
||||
}
|
||||
host, _ := c.participants.Get(c.hostPeerID)
|
||||
if host == nil || host.sessionID == "" {
|
||||
panic("Route Host has no current session")
|
||||
}
|
||||
if connectionID == "" {
|
||||
connectionID = "publication:" + generation
|
||||
}
|
||||
c.hostPublication = &HostPublication{
|
||||
Generation: generation,
|
||||
HostSessionID: host.sessionID,
|
||||
ConnectionID: connectionID,
|
||||
Usable: true,
|
||||
PhysicalActive: true,
|
||||
Resource: resource,
|
||||
}
|
||||
c.assertGraph()
|
||||
}
|
||||
|
||||
func peerEdge(parentPeerID, connectionID string) committedEdgeSeed {
|
||||
return committedEdgeSeed{
|
||||
Kind: UpstreamPeer,
|
||||
|
||||
@@ -214,7 +214,7 @@ func (c *Controller) Reconcile(nowMs int64) ReconcileResult {
|
||||
deferredParentPeerIDs: []string{},
|
||||
qualitySource: qualitySource,
|
||||
}
|
||||
if debugEnabled() {
|
||||
if c.debugEnabled() {
|
||||
routes := make([]debugCandidate, 0, len(candidates))
|
||||
for _, candidate := range candidates {
|
||||
routes = append(routes, debugCandidate{Route: c.debugTuple(candidate.Tuple), Transition: candidate.EndpointTransition.Kind})
|
||||
@@ -318,7 +318,7 @@ func (c *Controller) BeginCurrentCandidate(input BeginInput) BeginResult {
|
||||
mediaReady: false,
|
||||
connectionAttempt: c.startCandidateOpportunity(op, *plan),
|
||||
}
|
||||
if debugEnabled() {
|
||||
if c.debugEnabled() {
|
||||
details := []any{
|
||||
"child", c.debugPeer(op.childPeerID),
|
||||
"candidate", c.debugTuple(tuple),
|
||||
@@ -689,7 +689,7 @@ func (c *Controller) CandidateFailed(guard CandidateGuard, nowMs int64) SettleRe
|
||||
// OperationExpired applies head-start and deadline expiry at nowMs;
|
||||
// Accepted reports whether something expired.
|
||||
func (c *Controller) OperationExpired(nowMs int64) SettleResult {
|
||||
if op := c.operation; op != nil && debugEnabled() {
|
||||
if op := c.operation; op != nil && c.debugEnabled() {
|
||||
current := op.current
|
||||
noProgress := current != nil &&
|
||||
current.tuple.Kind == UpstreamPeer &&
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
package signal
|
||||
|
||||
// Viewer presence, host status, disconnect handling and room termination of
|
||||
// src/server/signaling.ts (sendViewerPresence 1594, broadcastHostStatus
|
||||
// 1683, handleDisconnect 1397, stopSharing 1466, closeRoom,
|
||||
// revokeGrantViewers 1016, closeRevokedViewerSession 1697 and the
|
||||
// viewer-keyed map helpers). Every function here runs with mu held.
|
||||
// Viewer presence, Host status, disconnect handling and room termination run
|
||||
// with Server.mu held, so each effect observes the current room membership.
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
package signal
|
||||
|
||||
// Ported from src/server/hybrid-media-router.ts (baseline b20fd88).
|
||||
//
|
||||
// The router owns no lock. signal.Server.mu (routerOptions.mu) is held by the
|
||||
// caller of every method except close, hooks are called with it held, and the
|
||||
// goroutines the router starts (pump driver, media prepares, drains and timer
|
||||
// callbacks) take it themselves. Every I/O window is an
|
||||
// unlock / I/O / lock window followed by exactly the guard the TS code ran
|
||||
// after that await (map C:/tmp/piik-go/maps/effect-layer.md, section 2.2).
|
||||
// callbacks) take it themselves. After unlocked I/O, revalidate the owning
|
||||
// room, operation and resource before committing effects under the lock.
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -46,6 +43,15 @@ func routeDebugFlag(value string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// routeDebugSink is the injected route-diagnostic sink; nil disables route
|
||||
// events. The signaling effect layer owns environment and logger policy.
|
||||
func routeDebugSink() func(string, ...any) {
|
||||
if !routeDebugEnabled() {
|
||||
return nil
|
||||
}
|
||||
return slog.Info
|
||||
}
|
||||
|
||||
// sfuFallback is SfuFallbackOptions. A zero timeout means the TS default.
|
||||
type sfuFallback struct {
|
||||
media sfu.Runtime
|
||||
@@ -299,7 +305,6 @@ func (r *router) completeAuthentication(participant authenticatedRouteParticipan
|
||||
return
|
||||
}
|
||||
r.broadcastActive(participant.roomID, rm)
|
||||
// TS: void this.sendFreshSfuConfig(participant).catch(() => undefined)
|
||||
r.sendFreshSfuConfig(participant)
|
||||
r.requestPump(participant.roomID)
|
||||
}
|
||||
@@ -1002,6 +1007,7 @@ func (r *router) createController(roomID string, rm *roomRuntime, host authentic
|
||||
rm.controller = route.New(route.Options{
|
||||
HostPeerID: host.peerID,
|
||||
DebugRoomID: roomID,
|
||||
DebugLog: routeDebugSink(),
|
||||
EndpointMediaCopyCapacity: r.capacity,
|
||||
OperationTimeoutMs: operationTimeoutMs,
|
||||
SfuEnabled: r.sfu != nil && !(routePolicy != nil && routePolicy.PeerOnly),
|
||||
|
||||
@@ -279,7 +279,6 @@ func (r *router) selectCandidate(
|
||||
return pumpCandidate{}, true
|
||||
}
|
||||
if operation.Cursor < 0 || operation.Cursor >= len(operation.Candidates) {
|
||||
// TS: `if (!plan)` -- note: no clearDeadline here.
|
||||
r.resourceWaiters.Delete(roomID)
|
||||
return pumpCandidate{}, true
|
||||
}
|
||||
|
||||
@@ -185,7 +185,6 @@ func (r *router) scheduleSfuDrain(kind drainKind, fence sfu.SubscriptionFence) {
|
||||
r.sfuDrainTasks.Set(key, task)
|
||||
}
|
||||
if task.operation == nil {
|
||||
// TS: void this.runSfuDrain(key, task).catch(() => undefined)
|
||||
r.runSfuDrain(key, task)
|
||||
}
|
||||
}
|
||||
|
||||
Loaded 100 of 261 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user