diff --git a/.github/dependabot.yml b/.github/dependabot.yml index b28376f..3384b79 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -36,6 +36,15 @@ updates: frontend-minor-patch: update-types: [minor, patch] + - package-ecosystem: npm + directory: /web + schedule: { interval: weekly, day: monday } + open-pull-requests-limit: 5 + commit-message: { prefix: "chore", include: scope } + groups: + web-minor-patch: + update-types: [minor, patch] + - package-ecosystem: npm directory: /plugin schedule: { interval: weekly, day: monday } diff --git a/.github/workflows/lockfile-check.yaml b/.github/workflows/lockfile-check.yaml index e41a76f..4a7405c 100644 --- a/.github/workflows/lockfile-check.yaml +++ b/.github/workflows/lockfile-check.yaml @@ -29,7 +29,7 @@ jobs: # Every workspace with a bun.lock. The plugin was missing here, so a # package.json/bun.lock drift only surfaced later in plugin-test with a # confusing "lockfile is frozen" error instead of failing this check. - workspace: [frontend, desktop, plugin, apps/mobile, reef/admin-ui] + workspace: [frontend, desktop, plugin, apps/mobile, reef/admin-ui, web] steps: - uses: actions/checkout@v6 diff --git a/.github/workflows/web.yaml b/.github/workflows/web.yaml new file mode 100644 index 0000000..173e540 --- /dev/null +++ b/.github/workflows/web.yaml @@ -0,0 +1,161 @@ +name: Web + +# Marketing site (web/) -> Cloudflare Workers static assets. +# +# main -> clawbits-web-staging -> https://preview.freeclaws.ai +# prod -> clawbits-web -> https://preview.clawbits.ai +# +# `prod` advances by manual merge from `main`, matching how the app ships. +# Neither target is the apex: the apex cutover is Phase 6 of the landing site +# plan (private clawbits-internal repo, docs/plans/LANDING_SITE_PLAN.md) and is +# a deliberate, separate step. The +# custom domains are attached by hand in the Cloudflare dashboard, which is why +# wrangler.jsonc declares no routes and the CI token needs only +# "Workers Scripts: Edit". +# +# Pull requests build and verify but never deploy. + +on: + push: + branches: [main, prod] + # The site does not build from web/ alone. /changelog reads + # frontend/src/release-notes (content.config.ts) and /docs reads docs/ + # through the src/docs-allowlist.ts gate, so a change to either can alter + # published pages without touching web/. + # + # Duplicated below rather than shared via a YAML anchor: the Actions parser + # does not support anchors, and an alias here is a silent no-trigger. + paths: + - 'web/**' + - 'frontend/src/release-notes/**' + - 'docs/**' + - '.github/workflows/web.yaml' + pull_request: + paths: + - 'web/**' + - 'frontend/src/release-notes/**' + - 'docs/**' + - '.github/workflows/web.yaml' + workflow_dispatch: + +permissions: + contents: read + +# One deploy per branch at a time - two overlapping `wrangler deploy` runs +# against the same Worker race on which upload wins. PR builds are safe to +# cancel; a deploy in flight is not. +concurrency: + group: web-${{ github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +jobs: + build: + name: Build & deploy + runs-on: ubuntu-24.04 + timeout-minutes: 10 + defaults: + run: + working-directory: web + steps: + - name: Checkout Code + uses: actions/checkout@v6 + with: + persist-credentials: false + # Full checkout, NOT sparse: the build reads ../frontend and ../docs + # (see the paths filter above). + + - name: Setup Bun + uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + + - name: Setup Node + # Unlike frontend/, this does not use `bun --bun`. Astro's build runs + # sharp for image optimisation and the Fonts API downloader; both are + # exercised on node locally and there is no reason for CI to be the + # first place they meet bun's runtime. package.json engines requires + # >=22.12, so the runner's default node is not assumed either. + uses: actions/setup-node@v7 + with: + node-version: '22' + + - name: Cache bun dependencies + uses: actions/cache@v4 + with: + path: | + ~/.bun/install/cache + web/node_modules + key: ${{ runner.os }}-bun-web-${{ hashFiles('web/bun.lock') }} + restore-keys: | + ${{ runner.os }}-bun-web- + + - name: Install dependencies + run: bun install --frozen-lockfile + + - name: Resolve deploy target + id: target + # `prod` is the only production branch; everything else (main, PRs, + # manual dispatch from a topic branch) builds as staging. Defaulting the + # other way would let a mis-named branch publish production canonicals. + # + # PUBLIC_APP_URL is the staging APP's URL, not the staging SITE's: the + # marketing site on preview.freeclaws.ai must link to the freeclaws + # deployment of the app. It is the apex today and becomes + # app.freeclaws.ai at the Phase 6 cutover - flip this line then. + run: | + if [ "${{ github.ref_name }}" = "prod" ]; then + { + echo "env=production" + echo "site=https://preview.clawbits.ai" + echo "app=https://app.clawbits.ai" + } >> "$GITHUB_OUTPUT" + else + { + echo "env=staging" + echo "site=https://preview.freeclaws.ai" + echo "app=https://freeclaws.ai" + } >> "$GITHUB_OUTPUT" + fi + + - name: Build + env: + # Canonical URLs, the sitemap, absolute OG image URLs and the + # Sitemap: line in robots.txt are all built from this. Unset, it + # defaults to the production apex (astro.config.mjs) and staging + # would advertise itself as clawbits.ai. + SITE_URL: ${{ steps.target.outputs.site }} + PUBLIC_APP_URL: ${{ steps.target.outputs.app }} + run: bun run build + + - name: Verify + # astro check + legal parity + internal links. Runs AFTER the build on + # purpose: verify:links walks dist/ and hard-exits if it is missing. + run: bun run verify + + - name: Assert preview build is not indexable + # The one mistake in this pipeline that costs money to undo. Both + # targets are preview hosts, so robots.txt must refuse every crawler + # (src/pages/robots.txt.ts keys off SITE_URL's hostname). Delete this + # step at the apex cutover, when being indexed becomes the point. + run: | + if ! grep -qx 'Disallow: /' dist/robots.txt; then + echo "::error::robots.txt would allow indexing of ${{ steps.target.outputs.site }}" + cat dist/robots.txt + exit 1 + fi + + - name: Deploy to Cloudflare Workers + if: github.event_name != 'pull_request' + env: + CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} + CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + # bunx resolves the wrangler pinned in web/package.json, so the deploy + # is lockfile-reproducible rather than whatever `npx wrangler` fetches. + # --env is mandatory: without it wrangler deploys the unnamed top-level + # config as a third Worker. See wrangler.jsonc. + run: bunx wrangler deploy --env ${{ steps.target.outputs.env }} + + - name: Summary + if: github.event_name != 'pull_request' + run: | + echo "Deployed \`${{ steps.target.outputs.env }}\` -> ${{ steps.target.outputs.site }}" >> "$GITHUB_STEP_SUMMARY" diff --git a/desktop/package.json b/desktop/package.json index f1d9ed0..f4026ab 100644 --- a/desktop/package.json +++ b/desktop/package.json @@ -1,7 +1,7 @@ { "name": "clawbits-desktop", "private": true, - "version": "0.17.0", + "version": "0.18.0", "type": "module", "engines": { "node": ">=22.12.0" diff --git a/desktop/src-tauri/Cargo.lock b/desktop/src-tauri/Cargo.lock index 892d3ed..65d90ba 100644 --- a/desktop/src-tauri/Cargo.lock +++ b/desktop/src-tauri/Cargo.lock @@ -603,7 +603,7 @@ dependencies = [ [[package]] name = "clawbits" -version = "0.17.0" +version = "0.18.0" dependencies = [ "image", "log", diff --git a/desktop/src-tauri/Cargo.toml b/desktop/src-tauri/Cargo.toml index 70c7d11..792bf4a 100644 --- a/desktop/src-tauri/Cargo.toml +++ b/desktop/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "clawbits" -version = "0.17.0" +version = "0.18.0" description = "Clawbits desktop app" authors = ["Clawbits"] edition = "2021" diff --git a/desktop/src-tauri/tauri.conf.json b/desktop/src-tauri/tauri.conf.json index 512c66b..68de8f5 100644 --- a/desktop/src-tauri/tauri.conf.json +++ b/desktop/src-tauri/tauri.conf.json @@ -2,7 +2,7 @@ "$schema": "../node_modules/@tauri-apps/cli/config.schema.json", "productName": "Clawbits", "mainBinaryName": "clawbits", - "version": "0.17.0", + "version": "0.18.0", "identifier": "ai.clawbits.desktop", "build": { "frontendDist": "../../frontend/dist", diff --git a/frontend/bun.lock b/frontend/bun.lock index 625f6fc..39c48fb 100644 --- a/frontend/bun.lock +++ b/frontend/bun.lock @@ -11,6 +11,7 @@ "@fontsource-variable/jetbrains-mono": "^5.2.8", "@hugeicons/core-free-icons": "^4.2.2", "@hugeicons/react": "^1.1.9", + "@paper-design/shaders-react": "^0.0.78", "@shikijs/langs": "^4.3.0", "@shikijs/themes": "^4.3.0", "@tailwindcss/vite": "^4.3.2", @@ -248,6 +249,10 @@ "@oxc-project/types": ["@oxc-project/types@0.122.0", "", {}, "sha512-oLAl5kBpV4w69UtFZ9xqcmTi+GENWOcPF7FCrczTiBbmC0ibXxCwyvZGbO39rCVEuLGAZM84DH0pUIyyv/YJzA=="], + "@paper-design/shaders": ["@paper-design/shaders@0.0.78", "", {}, "sha512-xOxV7koSzzh47G34I7uE3MASzli28k6+V35dlVrjMu270SFs0t8zeb4F/XTUU0BOXoHqIjKiGu6DnqkxPlsTcw=="], + + "@paper-design/shaders-react": ["@paper-design/shaders-react@0.0.78", "", { "dependencies": { "@paper-design/shaders": "0.0.78" }, "peerDependencies": { "@types/react": "^18 || ^19", "react": "^18 || ^19" }, "optionalPeers": ["@types/react"] }, "sha512-qA3LsErxiUDHTJNGCsoPqCiqUpWG024ean9GYP+fqk0ewxAgf8QfsZnajq/T2DMUvxCfVFfRm1Kzt6IGBUHOBw=="], + "@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.0-rc.11", "", { "os": "android", "cpu": "arm64" }, "sha512-SJ+/g+xNnOh6NqYxD0V3uVN4W3VfnrGsC9/hoglicgTNfABFG9JjISvkkU0dNY84MNHLWyOgxP9v9Y9pX4S7+A=="], "@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.0.0-rc.11", "", { "os": "darwin", "cpu": "arm64" }, "sha512-7WQgR8SfOPwmDZGFkThUvsmd/nwAWv91oCO4I5LS7RKrssPZmOt7jONN0cW17ydGC1n/+puol1IpoieKqQidmg=="], diff --git a/frontend/index.html b/frontend/index.html index bb3d7fd..732d57d 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -87,18 +87,18 @@ - - + + - - + + - + diff --git a/frontend/package.json b/frontend/package.json index 42968fc..868847d 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,7 +1,7 @@ { "name": "frontend", "private": true, - "version": "0.17.0", + "version": "0.18.0", "type": "module", "scripts": { "dev": "vite", @@ -20,6 +20,7 @@ "@fontsource-variable/jetbrains-mono": "^5.2.8", "@hugeicons/core-free-icons": "^4.2.2", "@hugeicons/react": "^1.1.9", + "@paper-design/shaders-react": "^0.0.78", "@shikijs/langs": "^4.3.0", "@shikijs/themes": "^4.3.0", "@tailwindcss/vite": "^4.3.2", diff --git a/frontend/public/og/README.md b/frontend/public/og/README.md index 2d42673..12088e2 100644 --- a/frontend/public/og/README.md +++ b/frontend/public/og/README.md @@ -7,14 +7,21 @@ Files served at `https://clawbits.ai/og/*` and referenced from | File | Dimensions | Size | Used by | |---|---|---|---| -| `og-default.png` | 2400 × 1260 (1.91:1) | ~1.0 MB | Primary `og:image` and `twitter:image`. Facebook, LinkedIn, Telegram, Discord, Slack, iMessage, WhatsApp, X. | +| `og-clawbits-app.png` | 1200 × 630 (1.91:1) | ~0.9 MB | Primary `og:image` and `twitter:image`. Facebook, LinkedIn, Telegram, Discord, Slack, iMessage, WhatsApp, X. | +| `og-default.png` | 2400 × 1260 (1.91:1) | ~1.0 MB | **Superseded 2026-08-06**, kept as the old master. Nothing references it. | + +The marketing site has its own per-route cards under `web/public/og/`; this +folder holds only the APP's card. Both sets are 1200 × 630, which is what the +`og:image:width` / `height` meta in `index.html` and in `web/src/layouts/ +Base.astro` declare - a card at any other size makes that meta a lie and lets +scrapers crop against the wrong box. ## Design constraints - **Format:** PNG, sRGB, palette-quantized (≤ 256 colors) for size. - **Safe zone:** all critical content (logo + tagline) must live within the inner - 1800 × 900 region of the 2400 × 1260 master. Outer ~300 px on each side and - ~180 px top/bottom may be cropped by some platforms. + ~900 × 450 region of the 1200 × 630 card. Outer ~150 px on each side and + ~90 px top/bottom may be cropped by some platforms. - **Hero copy:** _Where humans and agents live together._ - **Sub copy:** _Channels, files, and Git for human-agent teams. One API._ - **Background:** must look acceptable on both light and dark chat themes diff --git a/frontend/public/og/og-clawbits-app.png b/frontend/public/og/og-clawbits-app.png new file mode 100644 index 0000000..9d26c12 Binary files /dev/null and b/frontend/public/og/og-clawbits-app.png differ diff --git a/frontend/src/components/Analytics.test.tsx b/frontend/src/components/Analytics.test.tsx index 10c17e5..8fe37d4 100644 --- a/frontend/src/components/Analytics.test.tsx +++ b/frontend/src/components/Analytics.test.tsx @@ -21,7 +21,22 @@ describe("Analytics", () => { expect(script).not.toBeNull(); expect(script?.src).toBe("https://cloud.umami.is/script.js"); expect(script?.dataset.websiteId).toBe("3b3f10a0-3d8a-4196-b692-1442deded2d9"); - expect(script?.dataset.domains).toBe("clawbits.ai"); + expect(script?.dataset.domains).toBe("clawbits.ai,app.clawbits.ai"); + }); + + // Where the app lives after the Phase 6 apex cutover. Tracked from the same + // website ID as the marketing site so landing -> signup is one funnel; this + // asserts the app half of that pair does not go dark when the host changes. + it("injects on the post-cutover app host", () => { + renderOn("app.clawbits.ai"); + expect(document.querySelector(SCRIPT_SELECTOR)).not.toBeNull(); + }); + + // The marketing site carries its own tag (web/src/layouts/Base.astro). If the + // SPA ever gets served from a preview host it must not double-count. + it("does not inject on a preview host", () => { + renderOn("preview.clawbits.ai"); + expect(document.querySelector(SCRIPT_SELECTOR)).toBeNull(); }); it("does not inject on the staging host", () => { diff --git a/frontend/src/components/Analytics.tsx b/frontend/src/components/Analytics.tsx index 687c26f..1b10d2d 100644 --- a/frontend/src/components/Analytics.tsx +++ b/frontend/src/components/Analytics.tsx @@ -14,9 +14,22 @@ import { useEffect } from "react"; * * `ANALYTICS_HOSTS` is the single source of truth: it drives both the load * decision here and Umami's own `data-domains` filter. To track a new host - * (e.g. `www.` or `app.clawbits.ai`), add it here once. + * (e.g. `www.`), add it here once. + * + * ONE Umami website spans the app AND the marketing site (web/src/config.ts + * ships the same `websiteId`), so that a landing pageview and the signup that + * follows it belong to the same funnel instead of two dashboards that each show + * the other as a dead end. Hostname is recorded per event, so the two are still + * separable after the fact. + * + * Both hosts are listed rather than swapping one for the other at the Phase 6 + * apex cutover (LANDING_SITE_PLAN §8, table row 8). The app is on `clawbits.ai` + * until the flip and on `app.clawbits.ai` after it, and the apex becomes the + * marketing site the same day - so the union is never true of two live app + * origins at once, and this file does not have to ship in lockstep with a DNS + * change to avoid a gap in the numbers. */ -const ANALYTICS_HOSTS = ["clawbits.ai"]; +const ANALYTICS_HOSTS = ["clawbits.ai", "app.clawbits.ai"]; const UMAMI_WEBSITE_ID = "3b3f10a0-3d8a-4196-b692-1442deded2d9"; export function Analytics() { diff --git a/frontend/src/components/ChannelMentionLink.tsx b/frontend/src/components/ChannelMentionLink.tsx index 79ad9c5..302fc04 100644 --- a/frontend/src/components/ChannelMentionLink.tsx +++ b/frontend/src/components/ChannelMentionLink.tsx @@ -80,9 +80,9 @@ export function ChannelMentionLink({ aria-label={ isMember ? `Go to #${displayName}` - : `Open #${displayName} — you are not a member yet` + : `Open #${displayName} - you are not a member yet` } - title={isMember ? `#${displayName}` : `#${displayName} — join to view`} + title={isMember ? `#${displayName}` : `#${displayName} - join to view`} > {handle} diff --git a/frontend/src/components/MessageComposer.tsx b/frontend/src/components/MessageComposer.tsx index 9cc5fcb..7b81cd6 100644 --- a/frontend/src/components/MessageComposer.tsx +++ b/frontend/src/components/MessageComposer.tsx @@ -519,7 +519,7 @@ function AgentTargetChip({ aria-label={ idle ? "Target an agent" - : `Targeting @${targetAgent?.agent_id ?? ""} — click to change or clear` + : `Targeting @${targetAgent?.agent_id ?? ""} - click to change or clear` } className={`${baseClass} ${stateClass} ${pulseClass}`} > diff --git a/frontend/src/components/MessageMarkdown.tsx b/frontend/src/components/MessageMarkdown.tsx index 1bc1606..12f4947 100644 --- a/frontend/src/components/MessageMarkdown.tsx +++ b/frontend/src/components/MessageMarkdown.tsx @@ -182,10 +182,10 @@ function tokenizeMentions(text: string, mentions: MessageMentions): ReactNode[] let title: string | undefined; if (isPrimaryAgent) { className = resolvedClass; - title = `@${token} — this channel's agent`; + title = `@${token} - this channel's agent`; } else if (isAgent) { className = resolvedClass; - title = `@${token} — agent`; + title = `@${token} - agent`; } else if (isHuman) { className = resolvedClass; title = `@${token}`; diff --git a/frontend/src/components/MobileBottomNav.tsx b/frontend/src/components/MobileBottomNav.tsx index 28bcc3a..5a52324 100644 --- a/frontend/src/components/MobileBottomNav.tsx +++ b/frontend/src/components/MobileBottomNav.tsx @@ -83,7 +83,7 @@ export function MobileBottomNav() { })} {/* Search is an action (opens the command palette overlay), not a - route — same look as a tab, but never the active one. */} + route - same look as a tab, but never the active one. */} @@ -1499,7 +1499,7 @@ function ConfirmActivateDialog({ Set active image - New agents — and every in-place upgrade — will boot{" "} + New agents - and every in-place upgrade - will boot{" "} {img.tag}. This affects all newly-created VMs fleet-wide. Existing agents keep running until you upgrade them. @@ -1564,7 +1564,7 @@ function BuildDialog({ {isOpenclaw ? "Pin a specific engine or plugin version, or leave them at the latest. New agents boot the image once it's built and set active." - : "IronClaw builds from the pinned submodule and the clawbits channel in this tree — the engine and channel versions come from source, so there's nothing to pin."} + : "IronClaw builds from the pinned submodule and the clawbits channel in this tree - the engine and channel versions come from source, so there's nothing to pin."} @@ -1596,7 +1596,7 @@ function BuildDialog({ diff --git a/frontend/src/pages/TermsPage.tsx b/frontend/src/pages/TermsPage.tsx index d8dd5af..d2f7c46 100644 --- a/frontend/src/pages/TermsPage.tsx +++ b/frontend/src/pages/TermsPage.tsx @@ -16,7 +16,7 @@ const SECTIONS: Section[] = [ body: ( <>

- Clawbits is operated by Byzantine Generals Research, Lda, + Clawbits is operated by SKALE Labs, a company registered in Portugal ("Clawbits", "we", "us", "our"). You can reach us at:

@@ -119,8 +119,8 @@ const SECTIONS: Section[] = [ <>

Clawbots are software agents that act under your control. We treat - actions taken by your Clawbots — sending messages, posting content, - sending email, calling the API, storing files, and so on — as actions + actions taken by your Clawbots - sending messages, posting content, + sending email, calling the API, storing files, and so on - as actions taken by you. You are fully responsible for what your Clawbots do, just as you are for your own actions.

@@ -163,7 +163,7 @@ const SECTIONS: Section[] = [
  • send spam, unsolicited bulk messages, phishing, scams, or - misleading content — including via the per-agent + misleading content - including via the per-agent @clawbits.ai email addresses;
  • @@ -205,7 +205,7 @@ const SECTIONS: Section[] = [ body: ( <>

    - The Service lets you and your Clawbots submit content — messages, + The Service lets you and your Clawbots submit content - messages, posts, comments, profiles, files, code, web UIs, email, and anything else you upload or transmit ("Your Content").

    @@ -234,7 +234,7 @@ const SECTIONS: Section[] = [ Some features (public posts, public channels, published web UIs, public agent profiles) make Your Content visible to anyone on the internet. Treat anything you publish through these features as public - and permanent — copies may be cached, indexed, or redistributed by + and permanent - copies may be cached, indexed, or redistributed by others outside our control.

    Your responsibility

    @@ -334,7 +334,7 @@ const SECTIONS: Section[] = [ delete or anonymize your data in line with our retention practices, except where we need to keep it for legal, security, or backup reasons. Sections of these Terms that by their nature should survive - termination — for example sections 7, 8, 13, 14, 15, and 16 — will do so. + termination - for example sections 7, 8, 13, 14, 15, and 16 - will do so.

    ), @@ -386,7 +386,7 @@ const SECTIONS: Section[] = [

    Nothing in these Terms limits or excludes any liability that cannot - legally be limited or excluded — for example liability for fraud, + legally be limited or excluded - for example liability for fraud, gross negligence, willful misconduct, death or personal injury caused by our negligence, or your mandatory rights as a consumer under the law of your country of residence. @@ -448,8 +448,8 @@ const SECTIONS: Section[] = [

    We may update these Terms from time to time. When we do, we will update the "Last updated" date at the top of this page. If the changes - are significant, we will give you reasonable advance notice — for - example by email or an in-product message — before they take effect. + are significant, we will give you reasonable advance notice - for + example by email or an in-product message - before they take effect. By continuing to use the Service after the new Terms become effective, you accept the updated Terms.

    @@ -510,7 +510,7 @@ export default function TermsPage() {

    Welcome to Clawbits. These Terms are a contract between you and - Byzantine Generals Research, Lda — the company behind Clawbits. + SKALE Labs - the company behind Clawbits. They cover what you can expect from us, what we expect from you, and what happens if something goes wrong. We've tried to keep them short and human-readable. If anything is unclear, write to us at{" "} @@ -562,7 +562,7 @@ export default function TermsPage() {