diff --git a/README.md b/README.md index cef5181..78fbae4 100644 --- a/README.md +++ b/README.md @@ -126,12 +126,18 @@ browser loads its JavaScript from that origin, is in the ## Status -Pre-1.0, and honest about it. The local terminal, the login service, the -fleet-wide sessions list and the end-to-end pairing all work. The Cloudflare -relay is built and deployable but has not been through its manual end-to-end -gate against a real account ([docs/RELAY.md](docs/RELAY.md)), so treat it as -ready to try rather than ready to rely on. Known rough edges live in -[docs/FOLLOW-UPS.md](docs/FOLLOW-UPS.md). +Released and in daily use. v0.5.1 is the current release, `brew install +karnstack/tap/flue` gets it, and the whole of it works: the local terminal, the +login service, the fleet-wide sessions list, pairing, and the Cloudflare relay. +The relay has been through its manual end-to-end gate +([docs/RELAY.md](docs/RELAY.md)) against a real account, with a phone on a +different network paired to it and a second machine joined to the same relay. +There is a recording of that run at +[flue.sh/docs/setup](https://flue.sh/docs/setup). + +It is 0.x, which means what it usually means: commands, flags and the config +file can still change between releases, and an upgrade may ask something of +you. Known rough edges live in [docs/FOLLOW-UPS.md](docs/FOLLOW-UPS.md). flue is open source and always free. diff --git a/site/package.json b/site/package.json index fbfdd84..1b58c09 100644 --- a/site/package.json +++ b/site/package.json @@ -13,6 +13,7 @@ "deploy": "wrangler deploy" }, "dependencies": { + "@karnstack/kino": "^0.12.0", "@tanstack/react-router": "1.170.17", "@tanstack/react-start": "1.168.27", "class-variance-authority": "^0.7.1", diff --git a/site/pnpm-lock.yaml b/site/pnpm-lock.yaml index d512239..59c6a56 100644 --- a/site/pnpm-lock.yaml +++ b/site/pnpm-lock.yaml @@ -8,6 +8,9 @@ importers: .: dependencies: + '@karnstack/kino': + specifier: ^0.12.0 + version: 0.12.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7) '@tanstack/react-router': specifier: 1.170.17 version: 1.170.17(react-dom@19.2.7(react@19.2.7))(react@19.2.7) @@ -539,6 +542,22 @@ packages: '@jridgewell/trace-mapping@0.3.9': resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} + '@karnstack/kino@0.12.0': + resolution: {integrity: sha512-kwrpXfSNPY+lSVms4uI62TQkGRPdPgLs0AhTNfU9FPqtZI7jm2A96B60W+LAYqhWfuxPtt6ZNf3UijIvWyivWg==} + engines: {node: '>=24'} + peerDependencies: + react: '>=19' + react-dom: '>=19' + + '@mux/mux-data-google-ima@0.3.17': + resolution: {integrity: sha512-4wpH6dYybyZhqLn9qGn/+67Z8MZnQRAdqTFEEZw2bx61M9q01uPYYHxd8qwOnYtUGEeafsdTwVHVxKHGD3oc1A==} + + '@mux/mux-video@0.31.2': + resolution: {integrity: sha512-waahxlrat1PpRzcY2pwMQJ5Gyhos+D6iRyKnYzF0wBf4SESfBtunSBj3EuKuljypm8bamh9gWnlA0LSzm5Pz1w==} + + '@mux/playback-core@0.35.2': + resolution: {integrity: sha512-AsMV0LMwJm9T1ig6BBYSddeMlkzz+2RGcd6dkOgYk+L5Hx477ovbTxJZJ/vTW+rZjNWHcG3BvgNbSLgiubyZhw==} + '@napi-rs/wasm-runtime@1.2.2': resolution: {integrity: sha512-JfB4kuJQjaoHuCTseIINHtHWeJnvgEcxjwA5t/Y00ZgaOO1Crz3fjT/p8kT28zA/Caz7oiUMn3d6H2yOVCVwuw==} engines: {node: ^20.19.0 || ^22.13.0 || >=23.5.0} @@ -1599,6 +1618,9 @@ packages: '@tybys/wasm-util@0.10.3': resolution: {integrity: sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==} + '@types/google_interactive_media_ads_types@3.697.1': + resolution: {integrity: sha512-gRjJoPKQRjjTuySHqZe8Pnu6a5B0bKbTUD2ioDLPSWlEuBaAc2p1mzUzy42SCeWkyYffo2Mnv+blUGuvVvx3dQ==} + '@types/react-dom@19.2.3': resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} peerDependencies: @@ -1650,6 +1672,9 @@ packages: caniuse-lite@1.0.30001809: resolution: {integrity: sha512-xxWVywk6a6Arlk+hymeycyn/VgqEfLDxupvhH/xiY5SJ/18kmi9o6MiO320DCUzypORHLtvh0I4i04tUhCNHNQ==} + castable-video@1.1.16: + resolution: {integrity: sha512-wBhe2dZu2afhewL3EaGgVYTyDsa9HvNhY98clMZkNzDrLelOValSrTaoMos9YX7PPBCrgpd1j6YmNyyI2Vbq3w==} + chokidar@5.0.0: resolution: {integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==} engines: {node: '>= 20.19.0'} @@ -1674,6 +1699,9 @@ packages: csstype@3.2.3: resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + custom-media-element@1.4.6: + resolution: {integrity: sha512-/HRYqJOa1ob5ik4q7FIJVYxTJCFs/FL3+cQPAJjUf2uiqrDEzbTgB315gQ2rG8oK3w094W9m5tcB8S5Qah+caA==} + debug@4.4.3: resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} engines: {node: '>=6.0'} @@ -1754,6 +1782,9 @@ packages: crossws: optional: true + hls.js@1.6.17: + resolution: {integrity: sha512-NUplVGVuc1hSPwdB/9/cbRkUmLrYi75/hqiXKdA+l300pJNxDu96R7jRb2imDzWJqIUF4I5ThmAdp9GvOCXsuQ==} + isbot@5.2.1: resolution: {integrity: sha512-dJ+LpKyClQZ7NG+j3OensC/mAZkGpukE9YUrgPYvAZj2doVL0edfDgywTUh5CXa0o+nW9a1V9e5+CJTX8+SxRw==} engines: {node: '>=18'} @@ -1942,6 +1973,9 @@ packages: magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + media-tracks@0.3.5: + resolution: {integrity: sha512-l54rkKXlLBt3ob3zOLWHcnjvwUmX5bNEZ70igyapOZZC9imzqBmq1oz8p2roiV04KhjblFIi2hetLPF1oYVLRA==} + miniflare@4.20260701.0: resolution: {integrity: sha512-L6eAAi6IKtyb/7J6L+YsH2vb1yBrJWKRXI293JYDiMl70+6nncdAgigex58w6WBd+CwvdMsqOyNyGs95Op5gWQ==} engines: {node: '>=22.0.0'} @@ -1950,6 +1984,9 @@ packages: ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + mux-embed@5.18.1: + resolution: {integrity: sha512-ePsHjiEKY+FgrSBiMmaF+LOtTQSSBWv/1zqpREQFN96JE93xlsArT/MEi30yKOE06MgjOlL70YI750molu3y7g==} + nanoid@3.3.18: resolution: {integrity: sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} @@ -2655,6 +2692,30 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 + '@karnstack/kino@0.12.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@mux/mux-video': 0.31.2 + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + + '@mux/mux-data-google-ima@0.3.17': + dependencies: + mux-embed: 5.18.1 + + '@mux/mux-video@0.31.2': + dependencies: + '@mux/mux-data-google-ima': 0.3.17 + '@mux/playback-core': 0.35.2 + '@types/google_interactive_media_ads_types': 3.697.1 + castable-video: 1.1.16 + custom-media-element: 1.4.6 + media-tracks: 0.3.5 + + '@mux/playback-core@0.35.2': + dependencies: + hls.js: 1.6.17 + mux-embed: 5.18.1 + '@napi-rs/wasm-runtime@1.2.2(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)': dependencies: '@emnapi/core': 1.11.1 @@ -3784,6 +3845,8 @@ snapshots: tslib: 2.8.1 optional: true + '@types/google_interactive_media_ads_types@3.697.1': {} + '@types/react-dom@19.2.3(@types/react@19.2.17)': dependencies: '@types/react': 19.2.17 @@ -3828,6 +3891,10 @@ snapshots: caniuse-lite@1.0.30001809: {} + castable-video@1.1.16: + dependencies: + custom-media-element: 1.4.6 + chokidar@5.0.0: dependencies: readdirp: 5.1.1 @@ -3846,6 +3913,8 @@ snapshots: csstype@3.2.3: {} + custom-media-element@1.4.6: {} + debug@4.4.3: dependencies: ms: 2.1.3 @@ -3918,6 +3987,8 @@ snapshots: rou3: 0.8.1 srvx: 0.11.22 + hls.js@1.6.17: {} + isbot@5.2.1: {} jiti@2.7.0: {} @@ -4044,6 +4115,8 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 + media-tracks@0.3.5: {} + miniflare@4.20260701.0: dependencies: '@cspotcode/source-map-support': 0.8.1 @@ -4058,6 +4131,8 @@ snapshots: ms@2.1.3: {} + mux-embed@5.18.1: {} + nanoid@3.3.18: {} node-releases@2.0.53: {} diff --git a/site/src/components/walkthrough.tsx b/site/src/components/walkthrough.tsx new file mode 100644 index 0000000..a9ea46c --- /dev/null +++ b/site/src/components/walkthrough.tsx @@ -0,0 +1,66 @@ +import { MuxPlayer } from '@karnstack/kino/mux' +import '@karnstack/kino/styles.css' +import { Clapperboard } from 'lucide-react' + +import { + WALKTHROUGH_BLUR, + WALKTHROUGH_PLAYBACK_ID, + WALKTHROUGH_POSTER, + WALKTHROUGH_RUNTIME, +} from '@/lib/site' +import { cn } from '@/lib/utils' + +/** + * The setup walkthrough. + * + * Nothing here starts playback, and that is worth saying because kino's + * `autoPlay` prop is right there and using it breaks in a way that takes a + * while to see. kino builds its inside a mount effect and sets + * `autoplay` on the element there, while its teardown calls remove(), which + * detaches an element without pausing it. React runs a mount effect twice in + * development, so the first element is left detached and playing and + * unreachable: a second audio track a beat behind the visible one, which no + * control on the page can stop, because the provider that owned it is gone. + * + * The accent is handed over as `var(--primary)` rather than as a colour. kino + * assigns the prop straight to --kino-accent on its own root, so it resolves + * inside whichever theme the page is wearing and the header's toggle moves the + * scrubber with everything else. A literal would have pinned it to one theme. + * + * `tokens` is absent on purpose: playback is public, so there is nothing to + * sign and no key to hold. + */ +export function Walkthrough({ + align = 'left', + className, +}: { + /** Follows the section it lands in, the same way InstallBlock does. */ + align?: 'left' | 'center' + className?: string +}) { + return ( +
+
+ +
+
+
+
+ ) +} diff --git a/site/src/lib/site.ts b/site/src/lib/site.ts index 34f0c1f..b416972 100644 --- a/site/src/lib/site.ts +++ b/site/src/lib/site.ts @@ -4,3 +4,36 @@ export const X_URL = 'https://x.com/gyankarn' export const INSTALL_CMD = 'curl -fsSL https://flue.sh/install.sh | sh' export const BREW_CMD = 'brew install karnstack/tap/flue' + +/** + * The setup walkthrough, on Mux with public playback. + * + * There is no signing key here and nothing to mint at runtime. A playback id + * is the whole of what it takes to watch this, and it is not a secret. + */ +export const WALKTHROUGH_PLAYBACK_ID = 'vnrDD86c33LvJ7Ftv6uIWlEDTGH6dxqnE3cH6e02pZ94' + +/** How long it runs. Written in the caption, and read out in the button label. */ +export const WALKTHROUGH_RUNTIME = '8:51' + +/** + * One frame, named once and used twice: the closed card paints it, and the + * player is handed the same URL as its poster when it opens. Naming it twice + * is what makes the swap seamless, because the second request is a cache hit + * rather than a fresh fetch across an empty rectangle. + * + * 5:30 is the second worth choosing. It is the Ubuntu machine taking the + * install line with the sessions page already up behind it, which is the most + * legible frame in the run and one of the few that says what the whole thing + * is about. Several of the later ones show a relay secret, so they are out. + */ +export const WALKTHROUGH_POSTER = `https://image.mux.com/${WALKTHROUGH_PLAYBACK_ID}/thumbnail.webp?time=330&width=1200` + +/** + * That same frame at 24px across, written out here at 263 bytes so the card + * has something to paint before anything is fetched. kino lays it under the + * poster the same way once the player opens, which is why both halves are + * given it. + */ +export const WALKTHROUGH_BLUR = + 'data:image/webp;base64,UklGRqwAAABXRUJQVlA4IKAAAAAwBACdASoYAA4APlEgjUQjoiEYBAA4BQSzAFiNqALX4WRJCDRqDujU6YwA/v8rl6RU7q8kvuFZDpsXENrifU0Ahz/kXdneUSBeBVkGphdpCdwmD5sUyoP+dwh+2GmbntH5//OW/GnmdJ0Flr88cz3OPlY3GzJprjYUdTTcvQwS8I3YClHd8aXjjLMDn/p/IzRA7SwbBX3zW5/NZT35QAAA' diff --git a/site/src/routes/docs.setup.tsx b/site/src/routes/docs.setup.tsx index ecb2f57..4061db9 100644 --- a/site/src/routes/docs.setup.tsx +++ b/site/src/routes/docs.setup.tsx @@ -1,6 +1,7 @@ import { createFileRoute } from '@tanstack/react-router' import { Code, DocPage, Lead, Link, Note, P, Section, Shell, Step, Steps } from '@/components/doc-page' +import { Walkthrough } from '@/components/walkthrough' import { docTitle, findDoc } from '@/lib/docs' const DOC = findDoc('setup')! @@ -15,6 +16,19 @@ export const Route = createFileRoute('/docs/setup')({ function Setup() { return ( + {/* First, before a single command. Somebody who opened this page has + already decided to install flue, and the thing they want to know + before they start is how long it takes and what it looks like when + it works. The steps below answer neither, and eight minutes of + somebody doing it answers both. */} +
+ + Every step below, run on two machines: a Mac from nothing, then an Ubuntu box joining the + same relay. + + +
+
One relay. Every machine joined to it. Every device paired once. Everything below is a diff --git a/site/src/routes/index.tsx b/site/src/routes/index.tsx index b586e38..c1fda52 100644 --- a/site/src/routes/index.tsx +++ b/site/src/routes/index.tsx @@ -8,6 +8,7 @@ import { SwitcherWindow } from '@/components/mock/switcher' import { MockTerminal, ok, output, prompt } from '@/components/mock/terminal' import { SiteFooter } from '@/components/site-footer' import { SiteHeader } from '@/components/site-header' +import { Walkthrough } from '@/components/walkthrough' import { GithubMark } from '@/components/wordmark' import { BREW_CMD, INSTALL_CMD, REPO_URL } from '@/lib/site' @@ -335,8 +336,7 @@ function Remote() {

Everything crossing it is end-to-end encrypted with Noise IK. Your browser pins the daemon’s key when it pairs, so the Worker forwards ciphertext it holds no key - for. The relay is new: it works, but it has not been through its release gate, so - treat it as ready to try rather than ready to rely on. + for. It is a 0.x release, so commands and config can still change between versions.

@@ -432,6 +442,11 @@ function Install() { .

+ {/* Full width, the way the switcher above sits full width in a + section that is otherwise columns. The caption is centred with + the rest of this section rather than left aligned, since it is + the only line here that would have read off axis. */} +