feat: add marketing site (landing + docs)#5
Conversation
|
Warning Review limit reached
More reviews will be available in 47 minutes and 14 seconds. Learn how PR review limits work. Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file). ⌛ How to resolve this issue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based credits. 🚦 How do rate limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please see our Fair Usage Limits Policy for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (11)
📝 WalkthroughWalkthroughAdds a multi-page Vite React site under ChangesAirlock site buildout
Sequence Diagram(s)sequenceDiagram
participant SessionBuilder
participant Dropdown
participant MiniCopy
participant ClipboardAPI
SessionBuilder->>Dropdown: render browser and TTL selectors
Dropdown-->>SessionBuilder: onChange(value)
SessionBuilder->>MiniCopy: render copy controls
MiniCopy->>ClipboardAPI: writeText(text)
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 10
🧹 Nitpick comments (1)
site/src/Docs.tsx (1)
18-71: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winMove these docs content tables into
site/src/data.ts.This page already consumes shared copy from
site/src/data.ts, but these five arrays create a second source of truth for docs content. Keeping them centralized withBROWSERS,ENTRIES,INSTALL,REPO, andUSAGEwill reduce drift across the landing page and docs.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@site/src/Docs.tsx` around lines 18 - 71, The docs content tables in Docs.tsx are a duplicate source of truth and should be centralized in site/src/data.ts alongside BROWSERS, ENTRIES, INSTALL, REPO, and USAGE. Move ENDPOINTS, BODY_FIELDS, ENV_API, ENV_WORKER, and REPO_DOCS into the shared data module, export them there, and update Docs.tsx to consume the imported values so all docs copy comes from one place.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@site/README.md`:
- Around line 5-7: Update the README app description to reflect the current
implementation in the site entrypoint and BrowserIcon component. Remove or
revise the “single-page” and “no runtime dependencies beyond React” claims in
the README wording, since the docs entrypoint and site/src/BrowserIcon.tsx now
rely on runtime imports such as simple-icons. Keep the description aligned with
the actual app structure and dependencies so the README matches the codebase.
- Around line 27-45: Clarify the local preview behavior for the docs page in
site/README.md by stating that Vercel’s cleanUrls rewrite only applies in
production, while the Vite preview server serves the built file directly. Update
the docs-page instructions near the build/deploy section so users open the docs
locally at /docs.html instead of /docs, referencing the docs page and the Vercel
preview flow to keep the guidance accurate.
In `@site/src/Code.tsx`:
- Around line 12-17: The clipboard click handler in Code should be made safe for
unsupported or rejected writes: replace the chained .then() on
navigator.clipboard?.writeText() with an async onClick handler in the Code
component, await the writeText call only when the Clipboard API exists, and wrap
it in try-catch so missing clipboard support or permission failures do not throw
or create unhandled rejections. Keep the copied state update and reset logic
inside the success path, and use the existing Code symbol to locate the handler.
In `@site/src/docs-main.tsx`:
- Around line 1-10: This file is failing prettier checks, so update the
formatting in the docs entrypoint to match the repo style. Re-run prettier on
the docs-main.tsx module and verify the rendered createRoot/StrictMode/Docs
block and import layout are formatted exactly as expected before merging.
In `@site/src/Docs.tsx`:
- Around line 39-51: The Docs.tsx formatting is out of sync with Prettier,
causing the check to fail. Reformat the affected sections, including the ENV_API
array and the other referenced JSX blocks, by running Prettier so the long
string literals and JSX layout match the project style. Keep the content
unchanged and let the formatter rewrite the file consistently.
In `@site/src/Dropdown.tsx`:
- Around line 65-104: The Dropdown trigger/button is missing the ARIA wiring
needed to expose the listbox state and label association. Update the Dropdown
component to accept an id and/or labelId prop, then use it in the button and
listbox so the trigger sets aria-controls and aria-labelledby, and reflects the
current keyboard focus with a dynamic aria-activedescendant tied to the active
option. Make sure the list items in Dropdown map to stable option ids, and
update the SessionBuilder uses (“Browser”, “Lifetime”) to pass the new label/id
props so the wrapping label is programmatically associated with the button.
In `@site/src/main.tsx`:
- Around line 1-10: The formatting in the main entrypoint is failing Prettier
checks, so update the `createRoot` render block in `main.tsx` to match the
repository’s Prettier style by running the formatter and confirming the result
passes `prettier --check`. Focus on the imports and the
`createRoot(...).render(...)` call so the file’s formatting is fully normalized
before merge.
In `@site/src/SessionBuilder.tsx`:
- Around line 148-155: The Bearer token input in SessionBuilder should not be
rendered as plain text because it exposes AIRLOCK_API_TOKEN on-screen and to
browser helpers. Update the token field in SessionBuilder.tsx to use a
secret-style input appropriate for API credentials, while keeping the existing
state handling via setToken and the current placeholder/label behavior.
- Around line 60-70: The generated curl command in SessionBuilder’s useMemo
block is injecting raw baseUrl, targetUrl, and token directly into shell syntax,
which can break quoting or allow shell injection. Update the curl construction
to shell-escape or safely quote every interpolated value used in the request
string, including the URL, Authorization header value, and JSON payload contents
derived from targetUrl so the output remains valid for arbitrary input.
In `@site/vite.config.ts`:
- Around line 1-16: The Vite config in defineConfig is failing prettier check,
so normalize the formatting in the exported object and imports to match the
repo’s Prettier rules. Update the site/vite.config.ts content around
defineConfig, build, and rollupOptions so it is formatted exactly as prettier
--write would produce, then re-run the formatting validation to confirm the
warning is gone.
---
Nitpick comments:
In `@site/src/Docs.tsx`:
- Around line 18-71: The docs content tables in Docs.tsx are a duplicate source
of truth and should be centralized in site/src/data.ts alongside BROWSERS,
ENTRIES, INSTALL, REPO, and USAGE. Move ENDPOINTS, BODY_FIELDS, ENV_API,
ENV_WORKER, and REPO_DOCS into the shared data module, export them there, and
update Docs.tsx to consume the imported values so all docs copy comes from one
place.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 22479fa4-144c-4cbf-be85-25d717ab7ab3
⛔ Files ignored due to path filters (2)
site/package-lock.jsonis excluded by!**/package-lock.jsonsite/public/favicon.svgis excluded by!**/*.svg
📒 Files selected for processing (18)
site/.gitignoresite/README.mdsite/docs.htmlsite/index.htmlsite/package.jsonsite/src/App.tsxsite/src/BrowserIcon.tsxsite/src/Code.tsxsite/src/Docs.tsxsite/src/Dropdown.tsxsite/src/SessionBuilder.tsxsite/src/data.tssite/src/docs-main.tsxsite/src/index.csssite/src/main.tsxsite/tsconfig.jsonsite/vercel.jsonsite/vite.config.ts
| The marketing site for [Airlock](https://github.com/dotbrains/airlock) — a | ||
| single-page Vite + React + TypeScript app, terminal/dark theme, no runtime | ||
| dependencies beyond React. |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Update the app description to match the implementation.
This README still says the site is “single-page” and has no runtime dependencies beyond React, but this PR adds a docs entrypoint and site/src/BrowserIcon.tsx imports simple-icons at runtime. The onboarding docs will be wrong as soon as this lands.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@site/README.md` around lines 5 - 7, Update the README app description to
reflect the current implementation in the site entrypoint and BrowserIcon
component. Remove or revise the “single-page” and “no runtime dependencies
beyond React” claims in the README wording, since the docs entrypoint and
site/src/BrowserIcon.tsx now rely on runtime imports such as simple-icons. Keep
the description aligned with the actual app structure and dependencies so the
README matches the codebase.
| The build is multi-page: the landing page (`index.html`) and the docs page | ||
| (`docs.html`). | ||
|
|
||
| ## Deploy to Vercel | ||
|
|
||
| This app lives in the `site/` subdirectory, so point Vercel at that directory: | ||
|
|
||
| 1. In Vercel, **New Project** → import `dotbrains/airlock`. | ||
| 2. Set **Root Directory** to `site`. | ||
| 3. Framework preset auto-detects **Vite** (build `npm run build`, output `dist`). | ||
| 4. Deploy. | ||
|
|
||
| Or from the CLI: | ||
|
|
||
| ```sh | ||
| cd site | ||
| npx vercel # first run links/creates the project | ||
| npx vercel --prod # promote to production | ||
| ``` |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor
Ensure the local preview URL for the docs page is /docs.html.
The vercel.json configuration uses cleanUrls to serve docs.html at /docs in production, but npm run preview serves the static build artifacts without URL rewriting. Access the docs locally via http://localhost:4173/docs.html rather than /docs.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@site/README.md` around lines 27 - 45, Clarify the local preview behavior for
the docs page in site/README.md by stating that Vercel’s cleanUrls rewrite only
applies in production, while the Vite preview server serves the built file
directly. Update the docs-page instructions near the build/deploy section so
users open the docs locally at /docs.html instead of /docs, referencing the docs
page and the Vercel preview flow to keep the guidance accurate.
| onClick={() => { | ||
| void navigator.clipboard?.writeText(children).then(() => { | ||
| setCopied(true) | ||
| setTimeout(() => setCopied(false), 1400) | ||
| }) | ||
| }} |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major
Guard unsupported or rejected clipboard writes.
The handler calls .then() on the result of navigator.clipboard?.writeText(). If the Clipboard API is unavailable, this evaluates to undefined, causing undefined.then() to throw a TypeError. The code also fails to catch permission rejections, resulting in unhandled promise rejections.
Use an async handler with a try-catch block to safely await the operation:
Suggested fix
- onClick={() => {
- void navigator.clipboard?.writeText(children).then(() => {
- setCopied(true)
- setTimeout(() => setCopied(false), 1400)
- })
- }}
+ onClick={async () => {
+ try {
+ await navigator.clipboard?.writeText(children)
+ setCopied(true)
+ setTimeout(() => setCopied(false), 1400)
+ } catch {
+ // Silently fail or handle unsupported context
+ }
+ }}🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@site/src/Code.tsx` around lines 12 - 17, The clipboard click handler in Code
should be made safe for unsupported or rejected writes: replace the chained
.then() on navigator.clipboard?.writeText() with an async onClick handler in the
Code component, await the writeText call only when the Clipboard API exists, and
wrap it in try-catch so missing clipboard support or permission failures do not
throw or create unhandled rejections. Keep the copied state update and reset
logic inside the success path, and use the existing Code symbol to locate the
handler.
| <button | ||
| type="button" | ||
| className="adrop-btn" | ||
| aria-haspopup="listbox" | ||
| aria-expanded={open} | ||
| onClick={() => setOpen((o) => !o)} | ||
| onKeyDown={onKey} | ||
| > | ||
| {renderIcon?.(selected.value)} | ||
| <span className="adrop-name"> | ||
| {selected.name} | ||
| {selected.label ? <span className="adrop-label"> — {selected.label}</span> : null} | ||
| </span> | ||
| <span className="adrop-caret" aria-hidden="true"> | ||
| ▾ | ||
| </span> | ||
| </button> | ||
| {open && ( | ||
| <ul className="adrop-list" role="listbox" tabIndex={-1}> | ||
| {options.map((o, i) => ( | ||
| <li | ||
| key={o.value} | ||
| role="option" | ||
| aria-selected={o.value === value} | ||
| className={ | ||
| 'adrop-opt' + | ||
| (o.value === value ? ' sel' : '') + | ||
| (i === active ? ' active' : '') | ||
| } | ||
| onMouseEnter={() => setActive(i)} | ||
| onClick={() => choose(o.value)} | ||
| > | ||
| {renderIcon?.(o.value)} | ||
| <span className="adrop-name"> | ||
| {o.name} | ||
| {o.label ? <span className="adrop-label"> — {o.label}</span> : null} | ||
| </span> | ||
| </li> | ||
| ))} | ||
| </ul> |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major
🧩 Analysis chain
🏁 Script executed:
cat -n site/src/Dropdown.tsxRepository: dotbrains/airlock
Length of output: 4132
🏁 Script executed:
cat -n site/src/SessionBuilder.tsxRepository: dotbrains/airlock
Length of output: 9604
🏁 Script executed:
trueRepository: dotbrains/airlock
Length of output: 155
Missing accessibility attributes for custom listbox (button trigger).
The custom Dropdown uses a <button> to trigger a role="listbox", but lacks the necessary ARIA wiring to be accessible:
- The button is missing
aria-controlsto point to the listbox. - The button lacks
aria-activedescendant, so screen readers do not announce the currently "active" option during keyboard navigation. - Because the trigger is a
<button>(not an<input>), the wrapping<label>text inSessionBuilder.tsx("Browser", "Lifetime") is not programmatically associated with the control.
Update the component to accept an id or labelId prop to wire up aria-controls, aria-labelledby, and dynamic aria-activedescendant attributes.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@site/src/Dropdown.tsx` around lines 65 - 104, The Dropdown trigger/button is
missing the ARIA wiring needed to expose the listbox state and label
association. Update the Dropdown component to accept an id and/or labelId prop,
then use it in the button and listbox so the trigger sets aria-controls and
aria-labelledby, and reflects the current keyboard focus with a dynamic
aria-activedescendant tied to the active option. Make sure the list items in
Dropdown map to stable option ids, and update the SessionBuilder uses
(“Browser”, “Lifetime”) to pass the new label/id props so the wrapping label is
programmatically associated with the button.
| const curl = useMemo(() => { | ||
| const base = baseUrl.replace(/\/+$/, '') || 'http://localhost:8787' | ||
| const body = JSON.stringify({ | ||
| targetUrl: targetUrl || 'https://example.com', | ||
| browser, | ||
| ttlSeconds: ttl, | ||
| }) | ||
| const lines = [`curl -X POST ${base}/api/sessions \\`] | ||
| if (token.trim()) lines.push(` -H "Authorization: Bearer ${token.trim()}" \\`) | ||
| lines.push(' -H "Content-Type: application/json" \\') | ||
| lines.push(` -d '${body}'`) |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
Shell-escape the generated curl arguments.
baseUrl, targetUrl, and token are copied straight into shell syntax here. A valid URL like https://example.com/?q=O'Reilly already breaks the command, and pasted untrusted values can change what the shell executes.
Proposed fix
+const shQuote = (value: string) => `'${value.replaceAll("'", "'\\''")}'`
+
export default function SessionBuilder() {
@@
const curl = useMemo(() => {
const base = baseUrl.replace(/\/+$/, '') || 'http://localhost:8787'
const body = JSON.stringify({
targetUrl: targetUrl || 'https://example.com',
browser,
ttlSeconds: ttl,
})
- const lines = [`curl -X POST ${base}/api/sessions \\`]
- if (token.trim()) lines.push(` -H "Authorization: Bearer ${token.trim()}" \\`)
- lines.push(' -H "Content-Type: application/json" \\')
- lines.push(` -d '${body}'`)
+ const lines = [`curl -X POST ${shQuote(`${base}/api/sessions`)} \\`]
+ if (token.trim()) {
+ lines.push(` -H ${shQuote(`Authorization: Bearer ${token.trim()}`)} \\`)
+ }
+ lines.push(` -H ${shQuote('Content-Type: application/json')} \\`)
+ lines.push(` --data-raw ${shQuote(body)}`)
return lines.join('\n')
}, [baseUrl, targetUrl, browser, ttl, token])📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const curl = useMemo(() => { | |
| const base = baseUrl.replace(/\/+$/, '') || 'http://localhost:8787' | |
| const body = JSON.stringify({ | |
| targetUrl: targetUrl || 'https://example.com', | |
| browser, | |
| ttlSeconds: ttl, | |
| }) | |
| const lines = [`curl -X POST ${base}/api/sessions \\`] | |
| if (token.trim()) lines.push(` -H "Authorization: Bearer ${token.trim()}" \\`) | |
| lines.push(' -H "Content-Type: application/json" \\') | |
| lines.push(` -d '${body}'`) | |
| const shQuote = (value: string) => `'${value.replaceAll("'", "'\\''")}'` | |
| export default function SessionBuilder() { | |
| const curl = useMemo(() => { | |
| const base = baseUrl.replace(/\/+$/, '') || 'http://localhost:8787' | |
| const body = JSON.stringify({ | |
| targetUrl: targetUrl || 'https://example.com', | |
| browser, | |
| ttlSeconds: ttl, | |
| }) | |
| const lines = [`curl -X POST ${shQuote(`${base}/api/sessions`)} \\`] | |
| if (token.trim()) { | |
| lines.push(` -H ${shQuote(`Authorization: Bearer ${token.trim()}`)} \\`) | |
| } | |
| lines.push(` -H ${shQuote('Content-Type: application/json')} \\`) | |
| lines.push(` --data-raw ${shQuote(body)}`) | |
| return lines.join('\n') | |
| }, [baseUrl, targetUrl, browser, ttl, token]) |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@site/src/SessionBuilder.tsx` around lines 60 - 70, The generated curl command
in SessionBuilder’s useMemo block is injecting raw baseUrl, targetUrl, and token
directly into shell syntax, which can break quoting or allow shell injection.
Update the curl construction to shell-escape or safely quote every interpolated
value used in the request string, including the URL, Authorization header value,
and JSON payload contents derived from targetUrl so the output remains valid for
arbitrary input.
| <label className="field"> | ||
| <span>Bearer token (AIRLOCK_API_TOKEN) — blank for local dev</span> | ||
| <input | ||
| type="text" | ||
| value={token} | ||
| onChange={(e) => setToken(e.target.value)} | ||
| placeholder="(unset — unauthenticated)" | ||
| /> |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
Do not expose AIRLOCK_API_TOKEN as plain text.
This field invites users to paste a real API token, but type="text" leaves it visible on-screen and more likely to be retained by browser input helpers.
Proposed fix
<label className="field">
<span>Bearer token (AIRLOCK_API_TOKEN) — blank for local dev</span>
<input
- type="text"
+ type="password"
+ autoComplete="off"
+ autoCapitalize="none"
+ autoCorrect="off"
+ spellCheck={false}
value={token}
onChange={(e) => setToken(e.target.value)}
placeholder="(unset — unauthenticated)"
/>
</label>📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <label className="field"> | |
| <span>Bearer token (AIRLOCK_API_TOKEN) — blank for local dev</span> | |
| <input | |
| type="text" | |
| value={token} | |
| onChange={(e) => setToken(e.target.value)} | |
| placeholder="(unset — unauthenticated)" | |
| /> | |
| <label className="field"> | |
| <span>Bearer token (AIRLOCK_API_TOKEN) — blank for local dev</span> | |
| <input | |
| type="password" | |
| autoComplete="off" | |
| autoCapitalize="none" | |
| autoCorrect="off" | |
| spellCheck={false} | |
| value={token} | |
| onChange={(e) => setToken(e.target.value)} | |
| placeholder="(unset — unauthenticated)" | |
| /> |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@site/src/SessionBuilder.tsx` around lines 148 - 155, The Bearer token input
in SessionBuilder should not be rendered as plain text because it exposes
AIRLOCK_API_TOKEN on-screen and to browser helpers. Update the token field in
SessionBuilder.tsx to use a secret-style input appropriate for API credentials,
while keeping the existing state handling via setToken and the current
placeholder/label behavior.
Summary
A standalone marketing site for Airlock under
site/, modeled on fugue/site — a Vite + React + TypeScript multi-page app (landing + docs), terminal/dark theme, all copy centralized insrc/data.ts.It is standalone: it lives in
site/and is intentionally outside the Bun monorepo workspace (apps/*,packages/*), so it does not affectmake check, typecheck, or build. Usenpminsidesite/.Landing page (
App.tsx)Docs page (
Docs.tsx)Details
simple-icons(Chrome, Firefox, Brave, Vivaldi, Tor); Chromium reuses the Chrome mark tinted blue-grey; the Edge mark is bundled (CC0, pre-removal) since Microsoft's icons were pulled upstream.Dropdowncomponent backs every dropdown in the builder (browser dropdown shows logos; TTL is text-only, same styling/behavior).All facts (endpoints, env defaults, image tags, security posture) are sourced from
docs/.Test plan
cd site && npm install && npm run build—tsc+vite buildpass clean, both pages emit.npm run preview— both/and/docsserve.Need help on this PR? Tag
/codesmithwith what you need. Autofix is disabled.Summary by CodeRabbit
New Features
Documentation
Chores