DevDashboard Mobile + Cloud + Agent extraction - #193
Conversation
|
Important Review skippedToo many files! This PR contains 492 files, which is 342 over the limit of 150. To get a review, narrow the scope: Upgrade to Pro+ to raise the limit. Usage-priced reviews support at most 300 files. ⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Run ID: ⛔ Files ignored due to path filters (25)
📒 Files selected for processing (492)
You can disable this status message by setting the ✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Code Review
This pull request scaffolds the DevDashboard Mobile Expo app and the DevDashboard Cloud web stack, implementing core features such as Pulse metrics, live terminals with swappable WebView drivers, a QA live-stream, and Obsidian note sharing, alongside an Appium E2E testing harness. The code review identifies several critical issues and improvement opportunities: a bug where Stripe Checkout metadata does not propagate to the subscription object, potential race conditions in the terminal socket connection and SSE event stream, a data boundary bypass in settings upserts, and a type error when injecting modified single characters. Additionally, feedback suggests persisting the mobile connection state, properly rendering Markdown in QA cards, fixing a rounding bug in daemon duration formatting, and defensively handling non-JSON responses from the Cloudflare API.
| const session = await stripe.checkout.sessions.create({ | ||
| mode: "subscription", | ||
| line_items: [{ price, quantity: 1 }], | ||
| customer: opts.existingCustomerId ?? undefined, | ||
| customer_email: opts.existingCustomerId ? undefined : opts.email, | ||
| client_reference_id: opts.accountId, | ||
| metadata: { accountId: opts.accountId, tier: opts.tier }, | ||
| success_url: `${opts.appBaseUrl}/dashboard/billing?status=success`, | ||
| cancel_url: `${opts.appBaseUrl}/dashboard/billing?status=cancelled`, | ||
| }); |
There was a problem hiding this comment.
The metadata set directly on the Stripe Checkout Session does not automatically propagate to the created Subscription object. As a result, the webhook handler for customer.subscription.updated and customer.subscription.deleted will find subscription.metadata?.accountId to be undefined, failing to update the database. To fix this, pass the metadata inside subscription_data.metadata as well.
const session = await stripe.checkout.sessions.create({
mode: "subscription",
line_items: [{ price, quantity: 1 }],
customer: opts.existingCustomerId ?? undefined,
customer_email: opts.existingCustomerId ? undefined : opts.email,
client_reference_id: opts.accountId,
subscription_data: {
metadata: { accountId: opts.accountId, tier: opts.tier },
},
metadata: { accountId: opts.accountId, tier: opts.tier },
success_url: opts.appBaseUrl + "/dashboard/billing?status=success",
cancel_url: opts.appBaseUrl + "/dashboard/billing?status=cancelled",
});| export function injectKey(key: TerminalKey, mods: { shift?: boolean; ctrl?: boolean; alt?: boolean } = {}): string { | ||
| const spec = KEY_TABLE[key]; | ||
|
|
||
| return `(function(){ | ||
| var ta=document.querySelector(".xterm-helper-textarea"); | ||
| if(!ta){return;} | ||
| ta.focus(); | ||
| ta.dispatchEvent(new KeyboardEvent("keydown",{ | ||
| key:${str(spec.key)},code:${str(spec.code)},keyCode:${spec.keyCode},which:${spec.keyCode}, | ||
| shiftKey:${!!mods.shift},ctrlKey:${!!mods.ctrl},altKey:${!!mods.alt}, | ||
| bubbles:true,cancelable:true | ||
| })); | ||
| })();true;`; |
There was a problem hiding this comment.
In MobileKeyBar.tsx, single characters with the Ctrl modifier (e.g., Ctrl+C) are sent via renderer.sendKey(char as TerminalKey, { ctrl: true }). However, in inject.ts, KEY_TABLE only contains mappings for named TerminalKeys. Passing a single character like "c" results in spec being undefined, causing a runtime TypeError: Cannot read properties of undefined (reading 'key') when evaluating spec.key. We should update injectKey to handle single characters defensively.
export function injectKey(key: TerminalKey | string, mods: { shift?: boolean; ctrl?: boolean; alt?: boolean } = {}): string {
const spec = KEY_TABLE[key as TerminalKey];
if (!spec) {
const keyCode = key.toUpperCase().charCodeAt(0);
return "(function(){\n" +
" var ta=document.querySelector(\".xterm-helper-textarea\");\n" +
" if(!ta){return;}\n" +
" ta.focus();\n" +
" ta.dispatchEvent(new KeyboardEvent(\"keydown\",{\n" +
" key:" + str(key) + ",code:" + str("Key" + key.toUpperCase()) + ",keyCode:" + keyCode + ",which:" + keyCode + ",\n" +
" shiftKey:" + !!mods.shift + ",ctrlKey:" + !!mods.ctrl + ",altKey:" + !!mods.alt + ",\n" +
" bubbles:true,cancelable:true\n" +
" }));\n" +
"})();true;";
}
return "(function(){\n" +
" var ta=document.querySelector(\".xterm-helper-textarea\");\n" +
" if(!ta){return;}\n" +
" ta.focus();\n" +
" ta.dispatchEvent(new KeyboardEvent(\"keydown\",{\n" +
" key:" + str(spec.key) + ",code:" + str(spec.code) + ",keyCode:" + spec.keyCode + ",which:" + spec.keyCode + ",\n" +
" shiftKey:" + !!mods.shift + ",ctrlKey:" + !!mods.ctrl + ",altKey:" + !!mods.alt + ",\n" +
" bubbles:true,cancelable:true\n" +
" }));\n" +
"})();true;";
}| if (existing) { | ||
| await db | ||
| .update(accountSettings) | ||
| .set({ ...patch, updatedAt: new Date().toISOString() }) | ||
| .where(eq(accountSettings.accountId, accountId)); | ||
| return; |
There was a problem hiding this comment.
To strictly enforce the data boundary (D11) and prevent any forbidden key material or unpermitted fields from being written, the update path in upsertSettings must also run through assertNoKeyMaterial defensively, just like the insert path and updateSubscription do.
if (existing) {
assertNoKeyMaterial("account_settings", {
accountId,
...patch,
updatedAt: new Date().toISOString(),
});
await db
.update(accountSettings)
.set({ ...patch, updatedAt: new Date().toISOString() })
.where(eq(accountSettings.accountId, accountId));
return;
}| eventSourceFactory: (url: string): EventSourceLike => { | ||
| let onmessage: ((ev: { data: string }) => void) | null = null; | ||
| let onerror: ((ev: unknown) => void) | null = null; | ||
| const auth = opts.authHeader(); | ||
| const handle: SseHandle = streamSse({ | ||
| url, | ||
| headers: auth ? { Authorization: auth } : undefined, | ||
| onEvent: (event) => onmessage?.({ data: event.data }), | ||
| onError: (err) => onerror?.(err), | ||
| }); | ||
|
|
||
| return { | ||
| close: () => handle.close(), | ||
| get onmessage() { | ||
| return onmessage; | ||
| }, | ||
| set onmessage(handler) { | ||
| onmessage = handler; | ||
| }, | ||
| get onerror() { | ||
| return onerror; | ||
| }, | ||
| set onerror(handler) { | ||
| onerror = handler; | ||
| }, | ||
| }; | ||
| }, |
There was a problem hiding this comment.
When eventSourceFactory is called, streamSse is initiated synchronously. If the connection is established or events are received before the caller can assign onmessage or onerror to the returned EventSourceLike object, those early events will be silently dropped. Implementing a simple queueing mechanism ensures no events are lost during this initial setup phase.
eventSourceFactory: (url: string): EventSourceLike => {
let onmessage: ((ev: { data: string }) => void) | null = null;
let onerror: ((ev: unknown) => void) | null = null;
const queue: string[] = [];
const auth = opts.authHeader();
const handle: SseHandle = streamSse({
url,
headers: auth ? { Authorization: auth } : undefined,
onEvent: (event) => {
if (onmessage) {
onmessage({ data: event.data });
} else {
queue.push(event.data);
}
},
onError: (err) => {
if (onerror) {
onerror(err);
}
},
});
return {
close: () => handle.close(),
get onmessage() {
return onmessage;
},
set onmessage(handler) {
onmessage = handler;
if (handler) {
while (queue.length > 0) {
const data = queue.shift();
if (data !== undefined) {
handler({ data });
}
}
}
},
get onerror() {
return onerror;
},
set onerror(handler) {
onerror = handler;
},
};
},| ws.onStatus((s) => { | ||
| if (s === "open") { | ||
| setStatus("connected"); | ||
| } else if (s === "reconnecting") { | ||
| setStatus("connecting", "reconnecting"); | ||
| } else if (s === "closed") { | ||
| setStatus("disconnected"); | ||
| } | ||
| }); | ||
|
|
||
| ws.onMessage((data) => { | ||
| const chunk = typeof data === "string" ? data : new Uint8Array(data); | ||
| if (chunk instanceof Uint8Array) { | ||
| cbRef.current.onData?.(chunk); | ||
| } | ||
|
|
||
| run(injectBytes(bufToBase64(data))); | ||
| }); |
There was a problem hiding this comment.
When openSocket is called multiple times (e.g., during rapid session switches or driver toggles), the previous socket is closed asynchronously. If the closed socket emits status or message events after the new socket has been opened, it can overwrite the active terminal's status or inject stale data. Guarding the callbacks by checking if (wsRef.current !== ws) return; prevents this race condition.
ws.onStatus((s) => {
if (wsRef.current !== ws) return;
if (s === "open") {
setStatus("connected");
} else if (s === "reconnecting") {
setStatus("connecting", "reconnecting");
} else if (s === "closed") {
setStatus("disconnected");
}
});
ws.onMessage((data) => {
if (wsRef.current !== ws) return;
const chunk = typeof data === "string" ? data : new Uint8Array(data);
if (chunk instanceof Uint8Array) {
cbRef.current.onData?.(chunk);
}
run(injectBytes(bufToBase64(data)));
});
| sendInput(text) { | ||
| wsRef.current?.send(text); | ||
| }, | ||
| sendKey(key: TerminalKey, mods?: TerminalKeyMods) { |
| setActive(next); | ||
| setStatus("connecting"); | ||
| // Remount-via-key: a fresh initial mount sidesteps the #3863 update-path bug. | ||
| setMountKey((k) => k + 1); |
|
|
||
| export const useConnection = create<ConnectionState>((set) => ({ | ||
| tier: "lan", | ||
| baseUrl: null, | ||
| authHeader: null, | ||
| status: "disconnected", | ||
| setEndpoint: (tier, baseUrl, authHeader) => set({ tier, baseUrl, authHeader }), | ||
| setStatus: (status) => set({ status }), | ||
| reset: () => set({ baseUrl: null, authHeader: null, status: "disconnected" }), |
There was a problem hiding this comment.
The useConnection store is currently a plain, non-persisted Zustand store. Since baseUrl and authHeader are not persisted to storage (e.g., via Zustand's persist middleware or manual hydration on startup), the connection state will be lost every time the app restarts, forcing the user to re-connect or re-pair on every launch. Consider persisting these fields or re-hydrating them on startup.
| Answer | ||
| </Text> | ||
| <Text testID={`qa-answer-${id}`} className="text-[13px]" style={{ color: c.textPrimary }}> |
There was a problem hiding this comment.
Rendering entry.answerMd directly inside a standard React Native <Text> component will display raw Markdown syntax (such as **bold**, _italic_, or backticks) as plain text to the user. To provide a clean reading experience, consider stripping the Markdown syntax for the preview or using a lightweight Markdown renderer.
| const detail = body.errors?.map((e) => e.message).join("; ") || `HTTP ${res.status}`; | ||
| throw new Error(`Cloudflare custom-hostname provisioning failed: ${detail}`); | ||
| } | ||
|
|
||
| return { | ||
| configured: true, |
There was a problem hiding this comment.
If the Cloudflare API returns a non-JSON error response (e.g., an HTML error page during a 502/504 gateway error), calling res.json() directly will throw a SyntaxError, masking the actual HTTP status code. It is safer to check the Content-Type header or handle JSON parsing defensively, while ensuring any parsing errors are logged rather than silently swallowed.
let body: CloudflareCustomHostnameResponse | null = null;
if (res.headers.get("content-type")?.includes("application/json")) {
try {
body = await res.json() as CloudflareCustomHostnameResponse;
} catch (err) {
console.error("Failed to parse Cloudflare response as JSON", err);
}
}
if (!res.ok || !body || !body.success) {
const detail = (body?.errors?.map((e) => e.message).join("; ") || "HTTP ") + res.status;
throw new Error("Cloudflare custom-hostname provisioning failed: " + detail);
}References
- When catching errors, avoid swallowing all errors silently. Instead, catch specific error types or re-throw unexpected errors to prevent silent failures. Logging unexpected errors is also a good practice.
There was a problem hiding this comment.
Actionable comments posted: 51
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
DevDashboard/mobile/src/features/qa/subscription.test.ts (1)
110-117:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winClean up the timeout in the subscribe smoke test to avoid latent timer-driven flakiness.
The current promise can resolve
truewhile the 2s timeout is still pending, which can keep extra timers alive and make this test slower/less deterministic than needed.Suggested fix
it("qa.subscribe emits then closes cleanly", async () => { - const received = await new Promise<boolean>((resolve) => { - const sub = mockDashboardClient.qa.subscribe(() => resolve(true)); - setTimeout(() => { - sub.close(); - resolve(false); - }, 2_000); + const received = await new Promise<boolean>((resolve) => { + const sub = mockDashboardClient.qa.subscribe(() => { + clearTimeout(timer); + sub.close(); + resolve(true); + }); + const timer = setTimeout(() => { + sub.close(); + resolve(false); + }, 2_000); }); expect(received).toBe(true); });🤖 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 `@DevDashboard/mobile/src/features/qa/subscription.test.ts` around lines 110 - 117, The smoke test for "subscribe" leaves a 2s setTimeout pending; modify the test so the timeout ID is stored (e.g., const timer = setTimeout(...)) and call clearTimeout(timer) whenever the test promise resolves or rejects (before returning/ending the test) so no latent timers remain; ensure both the success path and error/cleanup path clear the timeout to avoid flakiness in the subscribe smoke test.
🤖 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 `@DevDashboard/cloud/landing/field-notes/index.html`:
- Line 664: The email <input type="email" ...> lacks an accessible label; add a
properly associated label element or aria-label/aria-labelledby so screen
readers can identify it. Specifically, give the input a unique id (e.g.,
id="email") and add a <label for="email">Email address</label> (or update the
surrounding form to reference that id via aria-labelledby), ensuring the visual
styling preserves the existing classes on the input and that the label text
conveys that the field is required if applicable.
- Around line 213-214: The two interactive buttons ("Approve" and "Open shell")
are missing explicit type attributes causing them to default to type="submit"
inside forms; update the two button elements rendering the "Approve" and "Open
shell" controls in the landing field-notes markup to include type="button"
(i.e., add type="button" to the button elements for the Approve and Open shell
buttons) so they no longer trigger form submission.
In `@DevDashboard/cloud/landing/obsidian-terminal/index.html`:
- Around line 168-171: The hamburger button with id="menuBtn" (containing spans
with ids "ham1" and "ham2") is missing an explicit type and can accidentally
submit a parent form; add type="button" to the <button id="menuBtn"> element so
it won't trigger form submission while preserving the existing attributes and
behavior.
- Around line 721-724: The email <input> element in the form (the element with
type="email" and placeholder="you@yourmachine.dev") is missing an accessible
label; add one by either wrapping the input with a <label> that contains
descriptive text (e.g., "Email") or by adding an aria-label attribute (e.g.,
aria-label="Email address") to the input, and ensure the input keeps required
and placeholder attributes unchanged so screen readers can announce it
correctly.
In `@DevDashboard/cloud/web/.gitignore`:
- Around line 22-24: The .gitignore entry "*.png" is too broad and will ignore
PNGs project-wide; narrow it to only Playwright scratch artifacts by replacing
the global pattern with a directory-scoped pattern such as
".playwright-mcp/**/*.png" (or ".playwright-mcp/*.png") so only PNGs inside the
.playwright-mcp directory are ignored and legitimate assets (favicons, public
images, OG images) remain tracked; update the entry referencing the
.playwright-mcp/ and "*.png" patterns accordingly.
In `@DevDashboard/cloud/web/db/migrations/0000_true_energizer.sql`:
- Around line 63-72: The subscriptions table allows multiple rows per
account_id; add a unique constraint on the account_id column in the
subscriptions table (e.g., ALTER TABLE or CREATE TABLE with UNIQUE
(`account_id`)) so only one current subscription row exists per account; update
any related constraints/triggers or downstream code that assumes multiple rows
and, if history is required, move historical records to a separate table rather
than keeping duplicates in the subscriptions table (reference: table
`subscriptions`, column `account_id`, primary key `id`).
In `@DevDashboard/cloud/web/db/migrations/meta/0000_snapshot.json`:
- Around line 159-171: The foreign key account_settings_account_id_user_id_fk
indicates account_settings.account_id references user.id but other tables use
account_id to mean user.id (e.g., devices.account_id, subscriptions.account_id);
either rename the column to user_id in the account_settings schema and update
the FK definition (rename account_settings.account_id → account_settings.user_id
and adjust account_settings_account_id_user_id_fk to reference columnsFrom
["user_id"]), or if the current name is intentional, add a clear schema
comment/documentation entry explaining that account_id in account_settings
semantically refers to the user identifier so consumers and future migrations
remain consistent; update any code, indices, and tests that reference
account_settings.account_id accordingly.
In `@DevDashboard/cloud/web/package.json`:
- Line 30: The package.json currently uses an unpinned dependency "nitro":
"latest" — replace this with a specific stable version (e.g. "nitro": "2.x.x")
or switch to "nitropack" with a pinned version (e.g. "nitropack": "2.x.x") to
avoid accidentally pulling Nitro v3 beta; update the other package.json that
also uses "nitro" so both are consistent, then run your package manager to
update pnpm-lock.yaml so the resolved version is recorded in the lockfile.
In `@DevDashboard/cloud/web/src/components/auth/AuthCard.tsx`:
- Around line 28-45: AuthCard drops the signup plan from /signup?plan=... so new
accounts always default to free; preserve and propagate the plan by including it
in the signup flow and/or navigation. Update AuthCard (component and its
onSubmit handling where signUp.email(...) is called) to accept the passed-in
plan prop and forward it: either add the plan to the signUp request payload
(e.g., as metadata/tier so backend can persist it and
cloudStore.ensureSubscription can pick it up) or include the plan when
navigating after signup (e.g., navigate({ to: "/dashboard?plan=..." }) or to
/dashboard/setup?plan=...) so getOverview/ensureSubscription can create the
correct tier; ensure the same plan prop name is used across Signup route,
AuthCard, signUp.email call, and any subsequent
cloudStore.ensureSubscription/getOverview handling.
In `@DevDashboard/cloud/web/src/integrations/tanstack-query/root-provider.tsx`:
- Around line 4-15: getContext() currently constructs a new QueryClient on every
call which creates multiple isolated caches; instead create and export a single
shared QueryClient instance (or memoize the call) so consumers reuse the same
client. Replace the pattern in getContext()/QueryClient with a module-level
singleton named queryClient (or ensure getContext() is invoked once and its
return stored) so all consumers import/use the same QueryClient instance rather
than recreating one each call.
In `@DevDashboard/cloud/web/src/lib/db/schema.ts`:
- Line 7: The schema documentation in schema.ts incorrectly lists an `accounts`
domain table even though that table is not defined anywhere in the schema.
Update the top-level domain table list to match the actual schema symbols
(`subscriptions`, `devices`, `managedSubdomains`, and `accountSettings`) and
remove the stale `accounts` reference. Make sure the comment stays consistent
with the table definitions exposed in this file.
In `@DevDashboard/DECISIONS.md`:
- Around line 67-150: Several level-3 headings (e.g., "### 2026-05-29 14:xx —
Kickoff", "### 2026-05-29 — Architecture forks answered", "### 2026-05-29 —
Stack steer", and other `###` headings in this block) lack a blank line after
the heading which triggers MD022; insert a single blank line immediately after
each `###` heading in DECISIONS.md so every heading is followed by an empty line
before the subsequent paragraph or list, then re-run markdownlint to confirm the
MD022 warnings are resolved.
In `@DevDashboard/mobile/AGENTS.md`:
- Around line 1-3: AGENTS.md currently only contains an Expo version note;
replace and expand it into a proper agent reference by adding a concise
Overview, Agent Implementations (list classes/modules and their
responsibilities, e.g., AgentName or class names), Capabilities (what each agent
can/can't do), Usage Patterns and Examples (how to instantiate/configure/invoke
each agent including expected inputs/outputs), Configuration/Environment
requirements, and Troubleshooting/Errors; ensure each agent entry references the
actual implementation symbols (class/function names) and includes minimal code
snippets or command examples illustrating common workflows and configuration
keys.
In `@DevDashboard/mobile/app.config.ts`:
- Around line 3-8: Update the header comment to remove the incorrect claim that
"react-native-zeroconf" is added as a config plugin; instead state that
"react-native-zeroconf" must NOT be included in the plugins array and is handled
natively, while "expo-camera" remains a config plugin. Edit the top comment
block (the dynamic config description) to match the actual behavior of the
plugins array and avoid restating obvious code — reference the plugins array and
the plugin names "react-native-zeroconf" and "expo-camera" in the revised
comment so future maintainers are not misled.
In `@DevDashboard/mobile/e2e/pages/MoreNav.page.ts`:
- Around line 22-28: The deep-link fallback bundleId in the open(route:
MoreRoute) method currently defaults to "dev.genesistools.devdashboard"; update
this fallback to match the iOS bundleIdentifier "dev.foltyn.dev-dashboard" (and
make the same change where DD_BUNDLE_ID is used in ConnectPage.page.ts) or
alternatively ensure CI/e2e always sets DD_BUNDLE_ID to the configured iOS
bundle id; modify the bundleId expression in MoreNav.open and ConnectPage usage
so the default matches "dev.foltyn.dev-dashboard".
In `@DevDashboard/mobile/e2e/README.md`:
- Around line 13-27: Update the fenced code block in
DevDashboard/mobile/e2e/README.md to include a language identifier (e.g.,
"text") on the opening fence so the directory tree block satisfies markdownlint
MD040; locate the triple-backtick block that begins the directory tree and
change it from ``` to ```text (leaving the contents and closing fence
unchanged).
- Around line 140-143: Add a blank line before the fenced code block that begins
with ``` in the README so the code block is surrounded by blank lines per MD031;
locate the triple-backtick fence in DevDashboard/mobile/e2e/README.md around the
list item mentioning "e2e/specs/<feature>.spec.ts" and insert a single empty
line immediately above the opening ``` fence.
In `@DevDashboard/mobile/README.md`:
- Around line 26-37: The README currently references the top-level "app" and
"app-example" directories but the project uses "src/app" in this PR; update the
onboarding paths and reset description to reference "src/app" and
"src/app-example" instead of "app" and "app-example" and ensure the text around
"npm run reset-project" explains it moves starter code to src/app-example and
creates a blank src/app. While editing README.md, keep the file-based routing
link but verify the "npm run reset-project" explanation matches the actual reset
script in package.json (adjust wording if the script targets different paths).
In `@DevDashboard/mobile/src/app/_layout.tsx`:
- Line 14: The useEffect in _layout.tsx currently calls wireAppStateFocus() but
does not clean up its subscription; change the effect to capture the unsubscribe
returned by wireAppStateFocus() and return it from the effect (e.g., const
unsubscribe = wireAppStateFocus(); return unsubscribe;) so the AppState listener
established by wireAppStateFocus() is removed on unmount; update the effect
surrounding symbols: useEffect and wireAppStateFocus to implement this cleanup.
In `@DevDashboard/mobile/src/app/`(tabs)/terminals.tsx:
- Around line 146-150: The MobileKeyBar can receive a null/stale renderer
because rendererRef.current is read during render before DriverComponent's ref
is assigned; change the wiring to promote the renderer to React state and set it
via a ref/onReady callback from DriverComponent so MobileKeyBar always receives
a stable value. Concretely: add state (e.g., renderer and setRenderer) in the
component, pass a callback prop or ref callback into DriverComponent (the same
place currently using rendererRef) that calls setRenderer(ref) once
mounted/committed, and pass the state value into MobileKeyBar instead of reading
rendererRef.current; keep rendererRef if still needed internally but ensure
MobileKeyBar uses the state-updated renderer to avoid the initial null window.
In `@DevDashboard/mobile/src/app/connect.tsx`:
- Around line 57-60: Validate the parsed port before calling setLan: when
computing port from url.port (using Number.parseInt), check for NaN or
out-of-range values (<=0 or >65535) and fall back to the default 3042; then pass
that validated port into setLan and use it to build baseUrl (e.g.,
`http://${url.hostname}:${validatedPort}`) so an invalid parse never gets
written into setLan or baseUrl; update the code around the setLan call and the
local port variable accordingly (references: url.port, setLan, baseUrl).
- Around line 91-101: Wrap the call to applyPairingUri in a try/catch inside the
async block so thrown exceptions are handled: call applyPairingUri(data,
password) inside try, on success keep the existing dispatchReach({ type:
"probe-ok" }); on error (catch) call setError with the caught error message (or
a fallback like "Pairing failed.") and run the same tier-aware dispatchReach({
type: "probe-fail", tier, paired: tier !== "managed" }) logic as in the current
else branch; ensure you reference applyPairingUri, dispatchReach, setError, and
the "probe-ok"/"probe-fail" actions so the UI/state updates even when
applyPairingUri throws.
In `@DevDashboard/mobile/src/components/connect/QrScanner.tsx`:
- Around line 37-44: The handler in onBarcodeScanned is setting scannedOnce
unconditionally which blocks all future scans; change it so you call the
onScanned/onQrScanned prop and only setScannedOnce(true) when that call
indicates success (have the parent return a boolean or Promise<boolean>), i.e.
await the result of onScanned/onQrScanned and if it returns true then call
setScannedOnce(true), otherwise leave scannedOnce false to allow retries; also
handle both sync and async returns and guard against exceptions by catching
errors and treating them as a failed scan (do not set scannedOnce on failure).
In `@DevDashboard/mobile/src/features/claude-usage/units.ts`:
- Around line 38-53: Remove the redundant JSDoc-style comment lines that only
restate the function signatures for bucketLabel, utilizationPct, and clock:
delete the three comment blocks immediately above the functions bucketLabel,
utilizationPct, and clock so the code is self-explanatory; if any comment
contains non-obvious behavior (e.g., return formats like "em dash" or percent
rounding) replace it with a single concise phrase that adds meaning rather than
repeating the signature.
In `@DevDashboard/mobile/src/features/containers/units.test.ts`:
- Around line 10-13: The test title is inaccurate: it claims only lowercase
"running" is accepted while the assertions expect case-insensitive normalization
(e.g., "Running" -> "running"); update the test's description string to reflect
that runState normalizes case and treats any case-variant of "running" as
"running" and everything else as "stopped" (refer to the test and the runState({
state: ... }) calls to locate and change the it(...) description).
In `@DevDashboard/mobile/src/features/daemon/queries.ts`:
- Around line 59-60: The query treats an empty string as "no log file" in the
early guard (if (!logFile) return []), but the react-query option `enabled` is
using `logFile != null`, which still enables the query for empty strings; change
the query's `enabled` condition to `Boolean(logFile)` so it matches the guard
and prevents running the `queryFn` when `logFile` is empty. Update the `enabled`
option where the query is created (referencing the `logFile` variable and the
query's `enabled` property) to use Boolean(logFile).
- Around line 35-37: The helper asArray currently converts any non-array into []
which masks server/contract failures; update asArray<T>(value: unknown): T[] so
it treats undefined or null as an acceptable empty response (return []), but for
any other non-array type it throws a descriptive error (or returns a
Result/Error) so callers like the code using asArray at the daemon run/log query
sites surface contract violations instead of silently succeeding; keep the
function name asArray and update all callsites to handle the thrown error or
Result accordingly.
In `@DevDashboard/mobile/src/features/daemon/units.ts`:
- Around line 36-38: The current logic computes mins and rem from seconds but
rounds the remainder which can produce rem === 60 (e.g. "1m60s"); instead, round
the total seconds first and then split into minutes and seconds so the
minute/second relationship stays consistent: compute a roundedSeconds =
Math.round(seconds) (or Math.round(ms/1000) if input is ms), then set mins =
Math.floor(roundedSeconds / 60) and rem = roundedSeconds % 60, and format using
String(rem).padStart(2,"0"); update the function that defines mins and rem to
use this approach.
In `@DevDashboard/mobile/src/features/obsidian/components/NewFolderModal.tsx`:
- Around line 18-28: The submit function concatenates the user-entered trimmed
into relativeDir, allowing path separators or traversal like "a/b" or "../x";
update submit (and any helper it calls) to validate trimmed before building
relativeDir: reject if trimmed contains '/' or path separators, equals "." or
"..", or contains any ".." segment, and on violation do not call onCreate
(instead setName("") or set an error state/return early); reference the submit
function, the trimmed/local variables, parentDir, onCreate and setName when
making the guard so only a single path segment is accepted.
In `@DevDashboard/mobile/src/features/obsidian/components/NoteRenderer.tsx`:
- Line 18: Replace the overly-broad jsDelivr host allowlist in NoteRenderer.tsx:
tighten CDN_HOST_RE and the request gating logic to only permit the exact pinned
asset URLs (HLJS_CSS_URL, KATEX_CSS_URL, MERMAID_JS_URL) used by note-html.ts
rather than any cdn.jsdelivr.net path; update the check around the request gate
(the logic that currently matches CDN_HOST_RE) to compare the requested URL
string against those three constants (or a Set of those constants) so only those
exact URLs are allowed.
In `@DevDashboard/mobile/src/features/obsidian/note-html.ts`:
- Around line 69-75: Move the duplicated CDN constants into a single shared
module and import them from both places to avoid drift; specifically extract
HLJS_CSS_URL, HLJS_CSS_SRI, KATEX_CSS_URL, KATEX_CSS_SRI and MERMAID_JS_URL into
a new exported constants file and replace the hard-coded values in note-html.ts
(and src/dev-dashboard/lib/obsidian/share-template.ts) with imports of those
symbols so both files reference the same source of truth.
In `@DevDashboard/mobile/src/features/obsidian/queries.ts`:
- Around line 33-39: noteQuery currently uses a type assertion in queryFn (path
as string); remove the assertion by capturing path in a local const and doing an
explicit null-check before using it: inside noteQuery do const p = path; if (p
=== null) throw new Error("noteQuery: path is null"); then use queryFn: () =>
client.obsidian.note(p); keep enabled: path !== null and reference
obsidianKeys.note and queryFn to locate the change.
In `@DevDashboard/mobile/src/features/qa/components/QaFeed.tsx`:
- Around line 19-27: The current resolveUnread uses a shared empty-string
fallback (row.id ?? "") which makes id-less rows collide; change it to guard
against missing ids instead of defaulting to "", e.g. remove the "?? ''" and add
an early check in resolveUnread that if row.id is null/undefined then return a
safe default (e.g. false) so locallyUnread/locallyRead checks only run for real
IDs; update references to id, locallyUnread, and locallyRead in resolveUnread
accordingly.
In `@DevDashboard/mobile/src/features/qa/hooks.ts`:
- Around line 78-105: The effect's open() unconditionally sets status to
"connecting" causing a flicker on AppState resume even when an active
subscription already exists; modify open() to check handleRef.current (or the
subscription's active flag returned by openQaSubscription) and only call
setStatus("connecting") and create a new handle when there is no active
subscription, leaving status untouched on resume; use the existing symbols
open(), close(), handleRef, setStatus, openQaSubscription, and onResumeRef to
implement this guard so AppState "active" only opens a new SSE when needed.
In `@DevDashboard/mobile/src/features/qa/subscription.ts`:
- Around line 44-63: The subscription currently calls callbacks.onStatus("live")
for every row; modify the subscription logic (in client.qa.subscribe / the
handler using seen, closed, onRow) to track a local boolean like isLive
initialized false and only call callbacks.onStatus?.("live") when isLive is
false, then set isLive = true so subsequent rows won't re-emit "live"; ensure
this flag is scoped to the subscriber handler so it resets only when the
subscription is torn down.
In `@DevDashboard/mobile/src/features/qa/units.ts`:
- Around line 65-76: Update the docstring for the tagTone function to reflect
the actual mapping used by tagTone (action → "accent", directive → "danger", all
other tags including "question" → "muted"); edit the comment above the tagTone
function (which references QaRow["tag"] and returns QaTagTone) so it no longer
says "question→accent" but correctly documents that non-action/non-directive
tags (e.g., "question") map to "muted".
In `@DevDashboard/mobile/src/features/terminals/components/SessionsList.tsx`:
- Around line 136-141: The onPress currently calls void openTmux(s) which
discards the returned promise so any errors from spawn.mutateAsync inside
openTmux are unhandled; update the ActionButton onPress to await and handle
errors (or call a wrapper that does) by using an async handler with try/catch or
by checking and using the mutation's error/status to surface feedback, and
ensure openTmux and the mutation (spawn.mutateAsync / spawn) are referenced so
you catch and display spawn errors to the user instead of letting them be
unhandled.
In
`@DevDashboard/mobile/src/features/terminals/components/WebViewTtydRenderer.tsx`:
- Around line 161-162: The current onLoadEnd handler in WebViewTtydRenderer
unconditionally calls setStatus("connected") which overwrites an error state set
by onError; change the onLoadEnd usage to onLoad (success-only) or add a guard
inside the onLoadEnd callback to only call setStatus("connected") if the current
status is not "error" (e.g., read status before calling setStatus), ensuring
setStatus is only set to "connected" on a successful load; adjust the handlers
where onLoad, onError and setStatus are referenced to keep error status intact.
In `@DevDashboard/mobile/src/features/terminals/scripts/build-xterm-host.ts`:
- Around line 20-22: The current script uses
dirname(fileURLToPath(import.meta.url)) to compute paths (symbols: here,
mobileRoot, out) which is verbose for Bun; replace that expression with
import.meta.dir and keep the rest of the path logic using resolve/join (update
usages of here, mobileRoot, out accordingly) so paths are derived from
import.meta.dir instead of dirname(fileURLToPath(import.meta.url)); ensure
imports of fileURLToPath/dirname are removed if no longer used.
In `@DevDashboard/mobile/src/global.css`:
- Around line 8-14: The CSS custom properties --font-display, --font-mono,
--font-rounded, and --font-serif contain multi-word font family names that need
to be quoted to satisfy stylelint; update each variable so any family name with
spaces (e.g., Spline Sans, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol,
Noto Color Emoji, Courier New, Times New Roman, MS PGothic, SF Pro Rounded if
not already) is wrapped in quotes (choose single or double quotes consistently)
so the font stacks are treated as single tokens and the
value-keyword-case/stylelint errors are resolved.
- Around line 1-5: The `@import` "./theme/tokens.css" is placed after Tailwind
directives which violates CSS ordering rules; move the `@import` line to precede
any other at-rules so it appears above the `@tailwind` base/components/utilities
directives in global.css, ensuring the tokens.css is imported first and the
`@tailwind` statements remain unchanged (look for the literal string `@import`
"./theme/tokens.css" and the three `@tailwind` lines).
In `@DevDashboard/mobile/src/hooks/use-theme.ts`:
- Around line 10-13: The hook currently sets theme via scheme === "unspecified"
? "light" : scheme but doesn't handle useColorScheme() returning null, causing
Colors[theme] to be undefined; update useTheme (where scheme is assigned from
useColorScheme(), the theme variable is computed, and Colors[...] is returned)
to treat both null and "unspecified" as "light" (e.g., default scheme to "light"
before indexing) so Colors[theme] always yields a valid theme object and
downstream components like themed-text/themed-view don't crash.
In `@DevDashboard/mobile/src/lib/sse-frame.ts`:
- Around line 11-16: The SSE parser currently splits on "\n" and pushes lines
matching "data:" without removing a trailing "\r", which breaks CRLF-terminated
messages; update the logic in the loop that processes frame.split("\n") (the
block using line.startsWith("data:") and dataParts.push(...)) to strip any
trailing carriage return(s) from the extracted payload (after slicing off
"data:" and removing the optional single leading space) before pushing into
dataParts so CRLF line endings are normalized.
In `@DevDashboard/mobile/src/lib/sse.ts`:
- Around line 52-54: In makeExpoEventSource, the catch block currently forwards
all stream errors to es.onerror even when they are caused by an intentional
teardown via close()/controller.abort(); update the catch to check
controller.signal.aborted (or err.name === 'AbortError') and skip calling
es.onerror when the abort is intentional, otherwise call es.onerror?.(err);
reference the controller, close(), and es.onerror symbols to locate and change
the catch handler.
In `@DevDashboard/mobile/src/lib/storage/db.ts`:
- Around line 15-27: getDb currently assigns a failing initialization to the
module-level dbPromise and never clears it, so subsequent calls always reject;
modify getDb to wrap the async initialization (the async IIFE assigned to
dbPromise) in try/catch and on any error set dbPromise back to undefined before
rethrowing the error so future calls can retry; refer to the getDb function and
the dbPromise symbol (and leave the MIGRATIONS/SQLite calls unchanged) when
adding the try/catch and reset logic.
In `@DevDashboard/mobile/src/lib/storage/kv.ts`:
- Around line 11-15: The getPref function currently parses stored JSON directly
which can throw for corrupted values; wrap the JSON.parse call in a try/catch
inside getPref (after awaiting Storage.getItem(key)) and return null if parsing
fails, otherwise return the parsed value cast to Prefs[K]; keep the
Storage.getItem call and the function signature intact and ensure any caught
error does not rethrow so callers get null instead of a rejected promise.
In `@DevDashboard/mobile/src/lib/storage/secure.ts`:
- Around line 33-40: loadBasicAuthHeader currently uses global btoa which is
Latin-1 only; replace it with a UTF-8-safe path by encoding
`${creds.username}:${creds.password}` to UTF-8 bytes (e.g., via TextEncoder or
Buffer) and then base64-encoding those bytes (use a byte-safe base64 helper or
Buffer.from(...).toString('base64') / a platform polyfill) before returning
`Basic <base64>`; update the implementation in loadBasicAuthHeader (and add any
small helper or import) so non-Latin-1 characters encode correctly while still
returning the same string shape.
In `@DevDashboard/mobile/src/shims/event-polyfill.ts`:
- Line 54: Remove the unused EventCtor extraction and its suppressing usage:
delete the declaration "const EventCtor = g.Event ..." and the no-op "void
EventCtor;" used to silence warnings in the EventTargetPolyfill implementation;
ensure no other code relies on EventCtor and keep EventTargetPolyfill behavior
unchanged.
In `@DevDashboard/mobile/src/transport/e2e/box-cipher.ts`:
- Around line 35-49: loadOrCreateDeviceKeys has a race: concurrent callers can
both see empty SecureStore, generate different keypairs and write conflicting
values; fix by memoizing the in-flight promise so only the first call performs
read/create/write and all concurrent callers await the same Promise. Implement a
module-scoped variable (e.g., let inFlightLoadOrCreate: Promise<KeyPair> | null)
and in loadOrCreateDeviceKeys return that promise if non-null; clear it on
success or failure; keep using SECRET_KEY_ITEM, PUBLIC_KEY_ITEM,
SecureStore.getItemAsync/setItemAsync and cipher.keyPair() so callers always
receive the same persisted keypair.
In `@DevDashboard/mobile/src/transport/lan-discovery.ts`:
- Around line 36-71: The rescan currently only clears agents and cannot restart
discovery because the Zeroconf instance is scoped inside useEffect; change to
store the Zeroconf instance in a useRef (e.g., zeroconfRef) created outside
useEffect, move creation/uses to ref.current, wrap startScan and rescan in
useCallback, and implement rescan to call zeroconfRef.current.stop(),
setAgents([]), then startScan() so discovery restarts; also ensure scanning
state is set true in startScan and set to false on both "error" and the Zeroconf
"stop" event, and keep cleanup to stop() and removeDeviceListeners() on the ref.
In `@DevDashboard/README.md`:
- Around line 18-29: The fenced artifact-map block in README.md lacks a language
info string which triggers MD040; update the opening triple-backtick for the
DevDashboard tree block to include a language identifier (e.g., add "text" so it
becomes ```text) so the block is properly annotated; locate the fenced block in
README.md (the directory listing snippet shown under "DevDashboard/") and modify
only the opening fence to include the language token.
---
Outside diff comments:
In `@DevDashboard/mobile/src/features/qa/subscription.test.ts`:
- Around line 110-117: The smoke test for "subscribe" leaves a 2s setTimeout
pending; modify the test so the timeout ID is stored (e.g., const timer =
setTimeout(...)) and call clearTimeout(timer) whenever the test promise resolves
or rejects (before returning/ending the test) so no latent timers remain; ensure
both the success path and error/cleanup path clear the timeout to avoid
flakiness in the subscribe smoke test.
🪄 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: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: 2d91475d-bf90-4006-8a49-2ee3188cd831
⛔ Files ignored due to path filters (24)
DevDashboard/cloud/web/bun.lockis excluded by!**/*.lockDevDashboard/mobile/assets/expo.icon/Assets/expo-symbol 2.svgis excluded by!**/*.svgDevDashboard/mobile/assets/expo.icon/Assets/grid.pngis excluded by!**/*.pngDevDashboard/mobile/assets/images/android-icon-background.pngis excluded by!**/*.pngDevDashboard/mobile/assets/images/android-icon-foreground.pngis excluded by!**/*.pngDevDashboard/mobile/assets/images/android-icon-monochrome.pngis excluded by!**/*.pngDevDashboard/mobile/assets/images/expo-badge-white.pngis excluded by!**/*.pngDevDashboard/mobile/assets/images/expo-badge.pngis excluded by!**/*.pngDevDashboard/mobile/assets/images/expo-logo.pngis excluded by!**/*.pngDevDashboard/mobile/assets/images/favicon.pngis excluded by!**/*.pngDevDashboard/mobile/assets/images/icon.pngis excluded by!**/*.pngDevDashboard/mobile/assets/images/logo-glow.pngis excluded by!**/*.pngDevDashboard/mobile/assets/images/react-logo.pngis excluded by!**/*.pngDevDashboard/mobile/assets/images/react-logo@2x.pngis excluded by!**/*.pngDevDashboard/mobile/assets/images/react-logo@3x.pngis excluded by!**/*.pngDevDashboard/mobile/assets/images/splash-icon.pngis excluded by!**/*.pngDevDashboard/mobile/assets/images/tabIcons/explore.pngis excluded by!**/*.pngDevDashboard/mobile/assets/images/tabIcons/explore@2x.pngis excluded by!**/*.pngDevDashboard/mobile/assets/images/tabIcons/explore@3x.pngis excluded by!**/*.pngDevDashboard/mobile/assets/images/tabIcons/home.pngis excluded by!**/*.pngDevDashboard/mobile/assets/images/tabIcons/home@2x.pngis excluded by!**/*.pngDevDashboard/mobile/assets/images/tabIcons/home@3x.pngis excluded by!**/*.pngDevDashboard/mobile/assets/images/tutorial-web.pngis excluded by!**/*.pngDevDashboard/mobile/bun.lockis excluded by!**/*.lock
📒 Files selected for processing (276)
DevDashboard/DECISIONS.mdDevDashboard/PRODUCT-ROADMAP.mdDevDashboard/README.mdDevDashboard/cloud/landing/daylight/index.htmlDevDashboard/cloud/landing/field-notes/index.htmlDevDashboard/cloud/landing/obsidian-terminal/index.htmlDevDashboard/cloud/shared/account-model.tsDevDashboard/cloud/shared/data-boundary.test.tsDevDashboard/cloud/shared/data-boundary.tsDevDashboard/cloud/shared/tier-policy.test.tsDevDashboard/cloud/shared/tier-policy.tsDevDashboard/cloud/web/.env.exampleDevDashboard/cloud/web/.gitignoreDevDashboard/cloud/web/biome.jsonDevDashboard/cloud/web/db/migrations/0000_true_energizer.sqlDevDashboard/cloud/web/db/migrations/meta/0000_snapshot.jsonDevDashboard/cloud/web/db/migrations/meta/_journal.jsonDevDashboard/cloud/web/drizzle.config.tsDevDashboard/cloud/web/package.jsonDevDashboard/cloud/web/src/components/RouteError.tsxDevDashboard/cloud/web/src/components/RouteNotFound.tsxDevDashboard/cloud/web/src/components/auth/AuthCard.tsxDevDashboard/cloud/web/src/components/dashboard/Card.tsxDevDashboard/cloud/web/src/components/landing/Features.tsxDevDashboard/cloud/web/src/components/landing/Footer.tsxDevDashboard/cloud/web/src/components/landing/Hero.tsxDevDashboard/cloud/web/src/components/landing/HowItWorks.tsxDevDashboard/cloud/web/src/components/landing/Nav.tsxDevDashboard/cloud/web/src/components/landing/Pricing.tsxDevDashboard/cloud/web/src/components/landing/TrustStory.tsxDevDashboard/cloud/web/src/components/landing/icons.tsxDevDashboard/cloud/web/src/content/copy.tsDevDashboard/cloud/web/src/integrations/tanstack-query/root-provider.tsxDevDashboard/cloud/web/src/lib/auth/auth-client.tsDevDashboard/cloud/web/src/lib/auth/auth-service.tsDevDashboard/cloud/web/src/lib/auth/auth.functions.tsDevDashboard/cloud/web/src/lib/auth/auth.server.tsDevDashboard/cloud/web/src/lib/auth/auth.test.tsDevDashboard/cloud/web/src/lib/billing/billing.functions.tsDevDashboard/cloud/web/src/lib/billing/env-gating.test.tsDevDashboard/cloud/web/src/lib/billing/stripe.tsDevDashboard/cloud/web/src/lib/dashboard/dashboard.functions.tsDevDashboard/cloud/web/src/lib/db/cloud-store.tsDevDashboard/cloud/web/src/lib/db/index.tsDevDashboard/cloud/web/src/lib/db/migrate.tsDevDashboard/cloud/web/src/lib/db/schema.tsDevDashboard/cloud/web/src/lib/provision/cloudflare.tsDevDashboard/cloud/web/src/lib/server/env.tsDevDashboard/cloud/web/src/router.tsxDevDashboard/cloud/web/src/routes/__root.tsxDevDashboard/cloud/web/src/routes/api.auth.$.tsDevDashboard/cloud/web/src/routes/api.stripe.webhook.tsDevDashboard/cloud/web/src/routes/dashboard.billing.tsxDevDashboard/cloud/web/src/routes/dashboard.devices.tsxDevDashboard/cloud/web/src/routes/dashboard.index.tsxDevDashboard/cloud/web/src/routes/dashboard.settings.tsxDevDashboard/cloud/web/src/routes/dashboard.setup.tsxDevDashboard/cloud/web/src/routes/dashboard.tsxDevDashboard/cloud/web/src/routes/index.tsxDevDashboard/cloud/web/src/routes/signin.tsxDevDashboard/cloud/web/src/routes/signup.tsxDevDashboard/cloud/web/src/start.tsDevDashboard/cloud/web/src/styles/app.cssDevDashboard/cloud/web/tsconfig.jsonDevDashboard/cloud/web/vite.config.tsDevDashboard/mobile/.claude/settings.jsonDevDashboard/mobile/.gitignoreDevDashboard/mobile/.vscode/extensions.jsonDevDashboard/mobile/.vscode/settings.jsonDevDashboard/mobile/AGENTS.mdDevDashboard/mobile/CLAUDE.mdDevDashboard/mobile/README.mdDevDashboard/mobile/app.config.tsDevDashboard/mobile/app.jsonDevDashboard/mobile/assets/expo.icon/icon.jsonDevDashboard/mobile/babel.config.jsDevDashboard/mobile/e2e/README.mdDevDashboard/mobile/e2e/pages/ClaudeUsagePage.page.tsDevDashboard/mobile/e2e/pages/ConnectPage.page.tsDevDashboard/mobile/e2e/pages/ContainersPage.page.tsDevDashboard/mobile/e2e/pages/DaemonPage.page.tsDevDashboard/mobile/e2e/pages/MoreNav.page.tsDevDashboard/mobile/e2e/pages/ObsidianPage.page.tsDevDashboard/mobile/e2e/pages/PulsePage.page.tsDevDashboard/mobile/e2e/pages/QaPage.page.tsDevDashboard/mobile/e2e/pages/TerminalsPage.page.tsDevDashboard/mobile/e2e/pages/WeatherPage.page.tsDevDashboard/mobile/e2e/pages/app.page.tsDevDashboard/mobile/e2e/pages/base.page.tsDevDashboard/mobile/e2e/specs/claude-usage.spec.tsDevDashboard/mobile/e2e/specs/connect.spec.tsDevDashboard/mobile/e2e/specs/containers.spec.tsDevDashboard/mobile/e2e/specs/daemon.spec.tsDevDashboard/mobile/e2e/specs/features-rest.smoke.spec.tsDevDashboard/mobile/e2e/specs/obsidian.spec.tsDevDashboard/mobile/e2e/specs/pulse.spec.tsDevDashboard/mobile/e2e/specs/qa.spec.tsDevDashboard/mobile/e2e/specs/smoke.spec.tsDevDashboard/mobile/e2e/specs/terminals.spec.tsDevDashboard/mobile/e2e/specs/weather.spec.tsDevDashboard/mobile/e2e/tsconfig.jsonDevDashboard/mobile/e2e/wdio.conf.tsDevDashboard/mobile/eslint.config.jsDevDashboard/mobile/metro.config.jsDevDashboard/mobile/nativewind-env.d.tsDevDashboard/mobile/package.jsonDevDashboard/mobile/patches/react-native-webview@13.16.0.patchDevDashboard/mobile/scripts/reset-project.jsDevDashboard/mobile/src/api/client-provider.tsxDevDashboard/mobile/src/api/mock-client.tsDevDashboard/mobile/src/api/query-keys.tsDevDashboard/mobile/src/app/(more)/_layout.tsxDevDashboard/mobile/src/app/(more)/claude-usage.tsxDevDashboard/mobile/src/app/(more)/containers.tsxDevDashboard/mobile/src/app/(more)/daemon.tsxDevDashboard/mobile/src/app/(more)/weather.tsxDevDashboard/mobile/src/app/(tabs)/_layout.tsxDevDashboard/mobile/src/app/(tabs)/index.tsxDevDashboard/mobile/src/app/(tabs)/more.tsxDevDashboard/mobile/src/app/(tabs)/obsidian.tsxDevDashboard/mobile/src/app/(tabs)/qa.tsxDevDashboard/mobile/src/app/(tabs)/terminals.tsxDevDashboard/mobile/src/app/_layout.tsxDevDashboard/mobile/src/app/connect.tsxDevDashboard/mobile/src/app/pair.tsxDevDashboard/mobile/src/components/animated-icon.module.cssDevDashboard/mobile/src/components/animated-icon.tsxDevDashboard/mobile/src/components/animated-icon.web.tsxDevDashboard/mobile/src/components/connect/QrScanner.tsxDevDashboard/mobile/src/components/connect/ReachabilityBadge.tsxDevDashboard/mobile/src/components/connect/TierPicker.tsxDevDashboard/mobile/src/components/external-link.tsxDevDashboard/mobile/src/components/hint-row.tsxDevDashboard/mobile/src/components/themed-text.tsxDevDashboard/mobile/src/components/themed-view.tsxDevDashboard/mobile/src/components/ui/collapsible.tsxDevDashboard/mobile/src/components/web-badge.tsxDevDashboard/mobile/src/constants/theme.tsDevDashboard/mobile/src/features/claude-usage/components/AccountHistoryCharts.tsxDevDashboard/mobile/src/features/claude-usage/components/AccountUsageCard.tsxDevDashboard/mobile/src/features/claude-usage/components/RangeSelector.tsxDevDashboard/mobile/src/features/claude-usage/hooks.tsDevDashboard/mobile/src/features/claude-usage/queries.test.tsDevDashboard/mobile/src/features/claude-usage/queries.tsDevDashboard/mobile/src/features/claude-usage/units.test.tsDevDashboard/mobile/src/features/claude-usage/units.tsDevDashboard/mobile/src/features/containers/components/ContainerRow.tsxDevDashboard/mobile/src/features/containers/hooks.tsDevDashboard/mobile/src/features/containers/queries.test.tsDevDashboard/mobile/src/features/containers/queries.tsDevDashboard/mobile/src/features/containers/units.test.tsDevDashboard/mobile/src/features/containers/units.tsDevDashboard/mobile/src/features/daemon/components/DaemonStatusHeader.tsxDevDashboard/mobile/src/features/daemon/components/RunLogSheet.tsxDevDashboard/mobile/src/features/daemon/components/RunRow.tsxDevDashboard/mobile/src/features/daemon/hooks.tsDevDashboard/mobile/src/features/daemon/queries.test.tsDevDashboard/mobile/src/features/daemon/queries.tsDevDashboard/mobile/src/features/daemon/units.test.tsDevDashboard/mobile/src/features/daemon/units.tsDevDashboard/mobile/src/features/obsidian/components/NewFolderModal.tsxDevDashboard/mobile/src/features/obsidian/components/NoteReader.tsxDevDashboard/mobile/src/features/obsidian/components/NoteRenderer.tsxDevDashboard/mobile/src/features/obsidian/components/VaultTree.tsxDevDashboard/mobile/src/features/obsidian/components/VaultTreeNode.tsxDevDashboard/mobile/src/features/obsidian/expanded-dirs.test.tsDevDashboard/mobile/src/features/obsidian/expanded-dirs.tsDevDashboard/mobile/src/features/obsidian/hooks.tsDevDashboard/mobile/src/features/obsidian/note-html.test.tsDevDashboard/mobile/src/features/obsidian/note-html.tsDevDashboard/mobile/src/features/obsidian/queries.test.tsDevDashboard/mobile/src/features/obsidian/queries.tsDevDashboard/mobile/src/features/obsidian/vault-filter.test.tsDevDashboard/mobile/src/features/obsidian/vault-filter.tsDevDashboard/mobile/src/features/pulse/components/KpiCard.tsxDevDashboard/mobile/src/features/pulse/components/NetworkInfo.tsxDevDashboard/mobile/src/features/pulse/components/ProcessTable.tsxDevDashboard/mobile/src/features/pulse/components/RangeSelector.tsxDevDashboard/mobile/src/features/pulse/components/SparklineRow.tsxDevDashboard/mobile/src/features/pulse/components/WeatherCard.tsxDevDashboard/mobile/src/features/pulse/hooks.tsDevDashboard/mobile/src/features/pulse/queries.test.tsDevDashboard/mobile/src/features/pulse/queries.tsDevDashboard/mobile/src/features/pulse/units.test.tsDevDashboard/mobile/src/features/pulse/units.tsDevDashboard/mobile/src/features/qa/components/QaCard.tsxDevDashboard/mobile/src/features/qa/components/QaFeed.tsxDevDashboard/mobile/src/features/qa/components/QaFilterBar.tsxDevDashboard/mobile/src/features/qa/components/QaLiveDot.tsxDevDashboard/mobile/src/features/qa/hooks.tsDevDashboard/mobile/src/features/qa/live-feed.test.tsDevDashboard/mobile/src/features/qa/live-feed.tsDevDashboard/mobile/src/features/qa/queries.test.tsDevDashboard/mobile/src/features/qa/queries.tsDevDashboard/mobile/src/features/qa/subscription.test.tsDevDashboard/mobile/src/features/qa/subscription.tsDevDashboard/mobile/src/features/qa/units.test.tsDevDashboard/mobile/src/features/qa/units.tsDevDashboard/mobile/src/features/terminals/TerminalRenderer.tsDevDashboard/mobile/src/features/terminals/bridge.test.tsDevDashboard/mobile/src/features/terminals/bridge.tsDevDashboard/mobile/src/features/terminals/components/DriverSwitcher.tsxDevDashboard/mobile/src/features/terminals/components/MobileKeyBar.tsxDevDashboard/mobile/src/features/terminals/components/SessionsList.tsxDevDashboard/mobile/src/features/terminals/components/WebViewHtmlRenderer.tsxDevDashboard/mobile/src/features/terminals/components/WebViewTtydRenderer.tsxDevDashboard/mobile/src/features/terminals/components/drivers.tsDevDashboard/mobile/src/features/terminals/driver-store.tsDevDashboard/mobile/src/features/terminals/hooks.tsDevDashboard/mobile/src/features/terminals/inject.tsDevDashboard/mobile/src/features/terminals/keymap.test.tsDevDashboard/mobile/src/features/terminals/keymap.tsDevDashboard/mobile/src/features/terminals/queries.test.tsDevDashboard/mobile/src/features/terminals/queries.tsDevDashboard/mobile/src/features/terminals/registry.test.tsDevDashboard/mobile/src/features/terminals/registry.tsDevDashboard/mobile/src/features/terminals/scripts/build-xterm-host.tsDevDashboard/mobile/src/features/terminals/xterm-host.generated.tsDevDashboard/mobile/src/features/weather/components/WeatherCard.tsxDevDashboard/mobile/src/features/weather/hooks.tsDevDashboard/mobile/src/features/weather/queries.test.tsDevDashboard/mobile/src/features/weather/queries.tsDevDashboard/mobile/src/features/weather/units.test.tsDevDashboard/mobile/src/features/weather/units.tsDevDashboard/mobile/src/global.cssDevDashboard/mobile/src/hooks/use-color-scheme.tsDevDashboard/mobile/src/hooks/use-color-scheme.web.tsDevDashboard/mobile/src/hooks/use-theme.tsDevDashboard/mobile/src/lib/__tests__/contract-import.test.tsDevDashboard/mobile/src/lib/__tests__/sse.test.tsDevDashboard/mobile/src/lib/apply-pairing.tsDevDashboard/mobile/src/lib/cn.tsDevDashboard/mobile/src/lib/contract-client.tsDevDashboard/mobile/src/lib/qr.test.tsDevDashboard/mobile/src/lib/qr.tsDevDashboard/mobile/src/lib/query.tsDevDashboard/mobile/src/lib/sse-frame.tsDevDashboard/mobile/src/lib/sse.tsDevDashboard/mobile/src/lib/storage/__tests__/kv.test.tsDevDashboard/mobile/src/lib/storage/db.tsDevDashboard/mobile/src/lib/storage/kv.tsDevDashboard/mobile/src/lib/storage/secure.tsDevDashboard/mobile/src/shims/event-polyfill.tsDevDashboard/mobile/src/shims/safe-json.tsDevDashboard/mobile/src/state/connection-store.tsDevDashboard/mobile/src/state/connection.tsDevDashboard/mobile/src/theme/colors.tsDevDashboard/mobile/src/theme/tokens.cssDevDashboard/mobile/src/transport/Transport.tsDevDashboard/mobile/src/transport/e2e-transport.tsDevDashboard/mobile/src/transport/e2e/box-cipher.test.tsDevDashboard/mobile/src/transport/e2e/box-cipher.tsDevDashboard/mobile/src/transport/e2e/envelope.test.tsDevDashboard/mobile/src/transport/e2e/envelope.tsDevDashboard/mobile/src/transport/lan-discovery.tsDevDashboard/mobile/src/transport/plain-transport.test.tsDevDashboard/mobile/src/transport/plain-transport.tsDevDashboard/mobile/src/transport/qa-stream.test.tsDevDashboard/mobile/src/transport/qa-stream.tsDevDashboard/mobile/src/transport/reachability.test.tsDevDashboard/mobile/src/transport/reachability.tsDevDashboard/mobile/src/transport/sse-parser.test.tsDevDashboard/mobile/src/transport/sse-parser.tsDevDashboard/mobile/src/transport/terminal-ws.test.tsDevDashboard/mobile/src/transport/terminal-ws.tsDevDashboard/mobile/src/transport/tiers/cloudflared.tsDevDashboard/mobile/src/transport/tiers/lan.tsDevDashboard/mobile/src/transport/tiers/managed.test.tsDevDashboard/mobile/src/transport/tiers/managed.tsDevDashboard/mobile/src/transport/tiers/tailscale.tsDevDashboard/mobile/src/types/assets.d.tsDevDashboard/mobile/src/types/css-modules.d.tsDevDashboard/mobile/src/types/react-native-zeroconf.d.tsDevDashboard/mobile/src/ui/Banner.tsxDevDashboard/mobile/src/ui/Card.tsxDevDashboard/mobile/src/ui/Empty.tsx
| <button class="flex-1 rounded-lg bg-oxblood py-1.5 text-[11px] font-medium text-cream">Approve</button> | ||
| <button class="flex-1 rounded-lg border border-cream/15 py-1.5 text-[11px] text-cream/70">Open shell</button> |
There was a problem hiding this comment.
Add type="button" to interactive buttons.
The buttons at lines 213-214 lack explicit type attributes. Add type="button" to prevent form submission behavior.
♿ Proposed fix
- <button class="flex-1 rounded-lg bg-oxblood py-1.5 text-[11px] font-medium text-cream">Approve</button>
- <button class="flex-1 rounded-lg border border-cream/15 py-1.5 text-[11px] text-cream/70">Open shell</button>
+ <button type="button" class="flex-1 rounded-lg bg-oxblood py-1.5 text-[11px] font-medium text-cream">Approve</button>
+ <button type="button" class="flex-1 rounded-lg border border-cream/15 py-1.5 text-[11px] text-cream/70">Open shell</button>📝 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.
| <button class="flex-1 rounded-lg bg-oxblood py-1.5 text-[11px] font-medium text-cream">Approve</button> | |
| <button class="flex-1 rounded-lg border border-cream/15 py-1.5 text-[11px] text-cream/70">Open shell</button> | |
| <button type="button" class="flex-1 rounded-lg bg-oxblood py-1.5 text-[11px] font-medium text-cream">Approve</button> | |
| <button type="button" class="flex-1 rounded-lg border border-cream/15 py-1.5 text-[11px] text-cream/70">Open shell</button> |
🧰 Tools
🪛 HTMLHint (1.9.2)
[warning] 213-213: The type attribute must be present on elements.
(button-type-require)
[warning] 214-214: The type attribute must be present on
elements.(button-type-require)
🤖 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 `@DevDashboard/cloud/landing/field-notes/index.html` around lines 213 - 214,
The two interactive buttons ("Approve" and "Open shell") are missing explicit
type attributes causing them to default to type="submit" inside forms; update
the two button elements rendering the "Approve" and "Open shell" controls in the
landing field-notes markup to include type="button" (i.e., add type="button" to
the button elements for the Approve and Open shell buttons) so they no longer
trigger form submission.
| <form class="flex flex-col gap-3" onsubmit="return false;"> | ||
| <div class="rounded-full border border-cream/15 bg-cream/[0.06] p-1.5"> | ||
| <div class="flex items-center gap-2"> | ||
| <input type="email" required placeholder="you@yourmachine.dev" class="w-full bg-transparent px-4 py-2.5 text-[14px] text-cream placeholder:text-cream/35 focus:outline-none" /> |
There was a problem hiding this comment.
Add accessible label for email input.
The email input lacks an associated <label> element for screen reader accessibility.
♿ Proposed fix
- <div class="flex items-center gap-2">
- <input type="email" required placeholder="you@yourmachine.dev" class="w-full bg-transparent px-4 py-2.5 text-[14px] text-cream placeholder:text-cream/35 focus:outline-none" />
+ <label class="flex items-center gap-2">
+ <span class="sr-only">Email address</span>
+ <input type="email" required placeholder="you@yourmachine.dev" class="w-full bg-transparent px-4 py-2.5 text-[14px] text-cream placeholder:text-cream/35 focus:outline-none" />🧰 Tools
🪛 HTMLHint (1.9.2)
[warning] 664-664: No matching [ label ] tag found.
(input-requires-label)
🤖 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 `@DevDashboard/cloud/landing/field-notes/index.html` at line 664, The email
<input type="email" ...> lacks an accessible label; add a properly associated
label element or aria-label/aria-labelledby so screen readers can identify it.
Specifically, give the input a unique id (e.g., id="email") and add a <label
for="email">Email address</label> (or update the surrounding form to reference
that id via aria-labelledby), ensuring the visual styling preserves the existing
classes on the input and that the label text conveys that the field is required
if applicable.
| <button id="menuBtn" aria-label="Open menu" class="relative flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-white/[0.06] ring-1 ring-white/10 md:hidden"> | ||
| <span id="ham1" class="absolute h-px w-4 bg-zinc-200 transition-transform duration-500 ease-silk -translate-y-1"></span> | ||
| <span id="ham2" class="absolute h-px w-4 bg-zinc-200 transition-transform duration-500 ease-silk translate-y-1"></span> | ||
| </button> |
There was a problem hiding this comment.
Add type="button" to prevent unintended form submission.
The hamburger menu button should have an explicit type="button" attribute to prevent it from submitting a form if it's accidentally nested within one.
♿ Proposed fix
- <button id="menuBtn" aria-label="Open menu" class="relative flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-white/[0.06] ring-1 ring-white/10 md:hidden">
+ <button type="button" id="menuBtn" aria-label="Open menu" class="relative flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-white/[0.06] ring-1 ring-white/10 md:hidden">📝 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.
| <button id="menuBtn" aria-label="Open menu" class="relative flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-white/[0.06] ring-1 ring-white/10 md:hidden"> | |
| <span id="ham1" class="absolute h-px w-4 bg-zinc-200 transition-transform duration-500 ease-silk -translate-y-1"></span> | |
| <span id="ham2" class="absolute h-px w-4 bg-zinc-200 transition-transform duration-500 ease-silk translate-y-1"></span> | |
| </button> | |
| <button type="button" id="menuBtn" aria-label="Open menu" class="relative flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-white/[0.06] ring-1 ring-white/10 md:hidden"> | |
| <span id="ham1" class="absolute h-px w-4 bg-zinc-200 transition-transform duration-500 ease-silk -translate-y-1"></span> | |
| <span id="ham2" class="absolute h-px w-4 bg-zinc-200 transition-transform duration-500 ease-silk translate-y-1"></span> | |
| </button> |
🧰 Tools
🪛 HTMLHint (1.9.2)
[warning] 168-168: The type attribute must be present on elements.
(button-type-require)
🤖 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 `@DevDashboard/cloud/landing/obsidian-terminal/index.html` around lines 168 -
171, The hamburger button with id="menuBtn" (containing spans with ids "ham1"
and "ham2") is missing an explicit type and can accidentally submit a parent
form; add type="button" to the <button id="menuBtn"> element so it won't trigger
form submission while preserving the existing attributes and behavior.
| <form class="relative mx-auto mt-10 flex max-w-md flex-col items-center gap-3 sm:flex-row" onsubmit="return false;"> | ||
| <div class="w-full rounded-full border border-white/10 bg-white/[0.04] p-1.5 sm:flex-1"> | ||
| <input type="email" required placeholder="you@yourmachine.dev" class="w-full rounded-full bg-transparent px-4 py-2.5 text-sm text-zinc-100 placeholder:text-zinc-600 focus:outline-none" /> | ||
| </div> |
There was a problem hiding this comment.
Add accessible label for email input.
The email input lacks an associated <label> element. Add an aria-label or wrap it with a <label> for screen reader accessibility.
♿ Proposed fix
- <div class="w-full rounded-full border border-white/10 bg-white/[0.04] p-1.5 sm:flex-1">
- <input type="email" required placeholder="you@yourmachine.dev" class="w-full rounded-full bg-transparent px-4 py-2.5 text-sm text-zinc-100 placeholder:text-zinc-600 focus:outline-none" />
+ <label class="w-full rounded-full border border-white/10 bg-white/[0.04] p-1.5 sm:flex-1">
+ <span class="sr-only">Email address</span>
+ <input type="email" required placeholder="you@yourmachine.dev" class="w-full rounded-full bg-transparent px-4 py-2.5 text-sm text-zinc-100 placeholder:text-zinc-600 focus:outline-none" />
+ </label>🧰 Tools
🪛 HTMLHint (1.9.2)
[warning] 723-723: No matching [ label ] tag found.
(input-requires-label)
🤖 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 `@DevDashboard/cloud/landing/obsidian-terminal/index.html` around lines 721 -
724, The email <input> element in the form (the element with type="email" and
placeholder="you@yourmachine.dev") is missing an accessible label; add one by
either wrapping the input with a <label> that contains descriptive text (e.g.,
"Email") or by adding an aria-label attribute (e.g., aria-label="Email address")
to the input, and ensure the input keeps required and placeholder attributes
unchanged so screen readers can announce it correctly.
| # Playwright-mcp scratch (verification artifacts, never committed) | ||
| .playwright-mcp/ | ||
| *.png |
There was a problem hiding this comment.
*.png is broader than the stated intent and may silently drop committed assets.
The comment scopes this to Playwright scratch, but *.png ignores every PNG anywhere under cloud/web — including legitimate assets you may later add (favicons, OG images, public/ icons). Consider scoping the ignore to the artifacts directory.
🔧 Proposed scoping
# Playwright-mcp scratch (verification artifacts, never committed)
.playwright-mcp/
-*.png
+.playwright-mcp/**/*.png📝 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.
| # Playwright-mcp scratch (verification artifacts, never committed) | |
| .playwright-mcp/ | |
| *.png | |
| # Playwright-mcp scratch (verification artifacts, never committed) | |
| .playwright-mcp/ | |
| .playwright-mcp/**/*.png |
🤖 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 `@DevDashboard/cloud/web/.gitignore` around lines 22 - 24, The .gitignore entry
"*.png" is too broad and will ignore PNGs project-wide; narrow it to only
Playwright scratch artifacts by replacing the global pattern with a
directory-scoped pattern such as ".playwright-mcp/**/*.png" (or
".playwright-mcp/*.png") so only PNGs inside the .playwright-mcp directory are
ignored and legitimate assets (favicons, public images, OG images) remain
tracked; update the entry referencing the .playwright-mcp/ and "*.png" patterns
accordingly.
| export async function getPref<K extends keyof Prefs>(key: K): Promise<Prefs[K] | null> { | ||
| const v = await Storage.getItem(key); | ||
|
|
||
| return v === null ? null : (JSON.parse(v) as Prefs[K]); | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Inspect the mobile safe-json shim API surface
fd -t f 'safe-json.ts' DevDashboard/mobile | xargs -I{} sh -c 'echo "== {} =="; cat "{}"'Repository: genesiscz/GenesisTools
Length of output: 2122
Guard JSON.parse in getPref to avoid crashing on corrupt stored prefs.
DevDashboard/mobile/src/lib/storage/kv.ts currently calls JSON.parse directly; malformed/legacy values will throw and propagate to callers as an unhandled rejection. The mobile SafeJSON.parse shim also ignores { strict: true } for error handling (it still uses native JSON.parse), so keep the explicit try/catch and return null on failure.
♻️ Proposed fix
export async function getPref<K extends keyof Prefs>(key: K): Promise<Prefs[K] | null> {
const v = await Storage.getItem(key);
-
- return v === null ? null : (JSON.parse(v) as Prefs[K]);
+ if (v === null) {
+ return null;
+ }
+ try {
+ return JSON.parse(v) as Prefs[K];
+ } catch {
+ return null;
+ }
}📝 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.
| export async function getPref<K extends keyof Prefs>(key: K): Promise<Prefs[K] | null> { | |
| const v = await Storage.getItem(key); | |
| return v === null ? null : (JSON.parse(v) as Prefs[K]); | |
| } | |
| export async function getPref<K extends keyof Prefs>(key: K): Promise<Prefs[K] | null> { | |
| const v = await Storage.getItem(key); | |
| if (v === null) { | |
| return null; | |
| } | |
| try { | |
| return JSON.parse(v) as Prefs[K]; | |
| } catch { | |
| return null; | |
| } | |
| } |
🤖 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 `@DevDashboard/mobile/src/lib/storage/kv.ts` around lines 11 - 15, The getPref
function currently parses stored JSON directly which can throw for corrupted
values; wrap the JSON.parse call in a try/catch inside getPref (after awaiting
Storage.getItem(key)) and return null if parsing fails, otherwise return the
parsed value cast to Prefs[K]; keep the Storage.getItem call and the function
signature intact and ensure any caught error does not rethrow so callers get
null instead of a rejected promise.
| export async function loadBasicAuthHeader(): Promise<string | null> { | ||
| const creds = await loadBasicCreds(); | ||
|
|
||
| if (!creds) { | ||
| return null; | ||
| } | ||
|
|
||
| return `Basic ${btoa(`${creds.username}:${creds.password}`)}`; |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
In Expo SDK 55 / React Native 0.83 with Hermes, is the global btoaguaranteed to exist, and doesbtoa accept arbitrary Unicode input or only Latin-1?
💡 Result:
Yes, in Expo SDK 55 (which uses React Native 0.83) with the Hermes JavaScript engine, the global btoa function is guaranteed to exist [1][2]. Hermes added native support for the global btoa and atob functions in recent versions to improve compatibility with the broader JavaScript ecosystem [3][2]. Regarding its behavior, btoa does not accept arbitrary Unicode input. Like its browser counterpart, it only supports characters within the Latin-1 range (U+0000 to U+00FF) [1][4][5]. If you pass a string containing characters outside this range (such as emojis or non-Latin script), Hermes will throw an error: "Found invalid character when converting to base64" [1][5]. To encode Unicode strings in React Native, you should first encode the string to a UTF-8 byte sequence—typically using TextEncoder or by manually escaping the string—before passing it to btoa [5][6]. Alternatively, you can use specialized libraries like base-64 or react-native-quick-base64 to handle Unicode-safe base64 conversion [2][7][8].
Citations:
- 1: facebook/hermes@d2177c3
- 2: https://github.com/craftzdog/react-native-quick-base64
- 3: Add support for
atob()andbtoa()functions facebook/hermes#1178 - 4: https://stackoverflow.com/questions/74182943/btoa-behavior-with-multibyte-characters
- 5: https://dev.to/_d7eb1c1703182e3ce1782/how-to-encode-and-decode-base64-in-javascript-4bcj
- 6: https://dev.to/aitranxuan/how-to-encode-raw-binary-files-and-canvas-images-to-base64-strings-for-direct-inline-css-mockups-948
- 7: https://omniapps.blog/react-native-base64
- 8: https://stackoverflow.com/questions/42829838/react-native-atob-btoa-not-working-without-remote-js-debugging
Use a UTF-8-safe base64 encoding for the Basic auth header (don’t use global btoa).
loadBasicAuthHeader() uses btoa to encode ${creds.username}:${creds.password}. In Hermes/React Native, btoa only supports Latin-1; non-Latin-1 characters can throw or encode incorrectly, causing authentication failures. Encode the credentials to UTF-8 bytes (e.g., via TextEncoder) and base64-encode those bytes with a UTF-8/byte-safe helper instead of btoa.
🤖 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 `@DevDashboard/mobile/src/lib/storage/secure.ts` around lines 33 - 40,
loadBasicAuthHeader currently uses global btoa which is Latin-1 only; replace it
with a UTF-8-safe path by encoding `${creds.username}:${creds.password}` to
UTF-8 bytes (e.g., via TextEncoder or Buffer) and then base64-encoding those
bytes (use a byte-safe base64 helper or Buffer.from(...).toString('base64') / a
platform polyfill) before returning `Basic <base64>`; update the implementation
in loadBasicAuthHeader (and add any small helper or import) so non-Latin-1
characters encode correctly while still returning the same string shape.
| export async function loadOrCreateDeviceKeys(cipher: BoxCipher = naclBoxCipher): Promise<KeyPair> { | ||
| const storedSecret = await SecureStore.getItemAsync(SECRET_KEY_ITEM); | ||
| const storedPublic = await SecureStore.getItemAsync(PUBLIC_KEY_ITEM); | ||
|
|
||
| if (storedSecret && storedPublic) { | ||
| return { publicKey: fromBase64(storedPublic), secretKey: fromBase64(storedSecret) }; | ||
| } | ||
|
|
||
| const kp = cipher.keyPair(); | ||
| await SecureStore.setItemAsync(SECRET_KEY_ITEM, toBase64(kp.secretKey), { | ||
| keychainAccessible: SecureStore.WHEN_UNLOCKED_THIS_DEVICE_ONLY, | ||
| }); | ||
| await SecureStore.setItemAsync(PUBLIC_KEY_ITEM, toBase64(kp.publicKey)); | ||
|
|
||
| return kp; |
There was a problem hiding this comment.
Serialize first-time device key creation.
Two concurrent calls can both observe empty storage, generate different keypairs, and race the writes. The loser still returns its in-memory keypair, so that caller can proceed with a device identity that no longer matches what was persisted. Memoize the in-flight load/create path so every caller gets the same keys.
🐛 Proposed fix
+let deviceKeysPromise: Promise<KeyPair> | null = null;
+
export async function loadOrCreateDeviceKeys(cipher: BoxCipher = naclBoxCipher): Promise<KeyPair> {
- const storedSecret = await SecureStore.getItemAsync(SECRET_KEY_ITEM);
- const storedPublic = await SecureStore.getItemAsync(PUBLIC_KEY_ITEM);
-
- if (storedSecret && storedPublic) {
- return { publicKey: fromBase64(storedPublic), secretKey: fromBase64(storedSecret) };
- }
-
- const kp = cipher.keyPair();
- await SecureStore.setItemAsync(SECRET_KEY_ITEM, toBase64(kp.secretKey), {
- keychainAccessible: SecureStore.WHEN_UNLOCKED_THIS_DEVICE_ONLY,
- });
- await SecureStore.setItemAsync(PUBLIC_KEY_ITEM, toBase64(kp.publicKey));
-
- return kp;
+ deviceKeysPromise ??= (async () => {
+ const storedSecret = await SecureStore.getItemAsync(SECRET_KEY_ITEM);
+ const storedPublic = await SecureStore.getItemAsync(PUBLIC_KEY_ITEM);
+
+ if (storedSecret && storedPublic) {
+ return { publicKey: fromBase64(storedPublic), secretKey: fromBase64(storedSecret) };
+ }
+
+ const kp = cipher.keyPair();
+ await SecureStore.setItemAsync(SECRET_KEY_ITEM, toBase64(kp.secretKey), {
+ keychainAccessible: SecureStore.WHEN_UNLOCKED_THIS_DEVICE_ONLY,
+ });
+ await SecureStore.setItemAsync(PUBLIC_KEY_ITEM, toBase64(kp.publicKey));
+ return kp;
+ })();
+
+ return deviceKeysPromise;
}🤖 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 `@DevDashboard/mobile/src/transport/e2e/box-cipher.ts` around lines 35 - 49,
loadOrCreateDeviceKeys has a race: concurrent callers can both see empty
SecureStore, generate different keypairs and write conflicting values; fix by
memoizing the in-flight promise so only the first call performs
read/create/write and all concurrent callers await the same Promise. Implement a
module-scoped variable (e.g., let inFlightLoadOrCreate: Promise<KeyPair> | null)
and in loadOrCreateDeviceKeys return that promise if non-null; clear it on
success or failure; keep using SECRET_KEY_ITEM, PUBLIC_KEY_ITEM,
SecureStore.getItemAsync/setItemAsync and cipher.keyPair() so callers always
receive the same persisted keypair.
| useEffect(() => { | ||
| const zeroconf = new Zeroconf(); | ||
|
|
||
| const startScan = (): void => { | ||
| setScanning(true); | ||
| zeroconf.scan(SERVICE_TYPE, "tcp", "local."); | ||
| }; | ||
|
|
||
| zeroconf.on("resolved", (service: Service) => { | ||
| const agent = toAgent(service); | ||
|
|
||
| if (agent) { | ||
| setAgents((prev) => (prev.some((a) => a.baseUrl === agent.baseUrl) ? prev : [...prev, agent])); | ||
| } | ||
| }); | ||
|
|
||
| zeroconf.on("error", () => setScanning(false)); | ||
| startScan(); | ||
|
|
||
| const sub = AppState.addEventListener("change", (next: AppStateStatus) => { | ||
| if (next === "active") { | ||
| setAgents([]); | ||
| zeroconf.stop(); | ||
| startScan(); | ||
| } | ||
| }); | ||
|
|
||
| return () => { | ||
| sub.remove(); | ||
| zeroconf.stop(); | ||
| zeroconf.removeDeviceListeners(); | ||
| }; | ||
| }, []); | ||
|
|
||
| return { agents, scanning, rescan: () => setAgents([]) }; | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
fd 'package.json' DevDashboard/mobile -d 2 --exec rg -n 'react-native-zeroconf' {}
rg -nP --type=ts -C2 '\b(scan|removeDeviceListeners|stop)\s*\(' --glob '**/react-native-zeroconf/**'Repository: genesiscz/GenesisTools
Length of output: 276
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# 1) Inspect the reviewed file around the referenced lines
FILE="DevDashboard/mobile/src/transport/lan-discovery.ts"
echo "---- $FILE (context) ----"
nl -ba "$FILE" | sed -n '1,220p'
# 2) Find other uses of react-native-zeroconf in the repo (ts only)
echo "---- usages of react-native-zeroconf ----"
rg -n --type=ts 'react-native-zeroconf' -S .
# 3) Find all references to the hook's rescan return value (if any)
echo "---- references to useZeroconfDiscovery / rescan ----"
rg -n --type=ts 'useZeroconfDiscovery\(|rescan' DevDashboard/mobile/src -SRepository: genesiscz/GenesisTools
Length of output: 198
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="DevDashboard/mobile/src/transport/lan-discovery.ts"
echo "---- $FILE (context) ----"
sed -n '1,220p' "$FILE" | cat -n
echo "---- usages of react-native-zeroconf ----"
rg -n --type=ts 'react-native-zeroconf' -S DevDashboard/mobile/src || true
echo "---- references to useZeroconfDiscovery / rescan ----"
rg -n --type=ts 'useZeroconfDiscovery|rescan' -S DevDashboard/mobile/src || true
echo "---- Zeroconf constructor usage ----"
rg -n --type=ts '\bnew\s+Zeroconf\b' -S DevDashboard/mobile/src || trueRepository: genesiscz/GenesisTools
Length of output: 3568
🏁 Script executed:
#!/bin/bash
set -euo pipefail
TFILE="DevDashboard/mobile/src/types/react-native-zeroconf.d.ts"
echo "---- $TFILE ----"
sed -n '1,200p' "$TFILE" | cat -nRepository: genesiscz/GenesisTools
Length of output: 1910
Fix rescan to restart Zeroconf discovery (and address scanning getting stuck).
- In
DevDashboard/mobile/src/transport/lan-discovery.ts,rescanonly clearsagents(rescan: () => setAgents([])); it never callszeroconf.scan(...)again because thezeroconfinstance is created inside theuseEffectclosure and not accessible torescan. - A manual refresh can therefore leave the list empty until the network re-advertises services.
scanningis set totruewhenstartScan()runs and is only reset tofalseon"error"; there’s no handler for a terminal discovery event, so the UI can remain “scanning” indefinitely.react-native-zeroconf@0.14.0supportsscan(...)andstop()(seeDevDashboard/mobile/src/types/react-native-zeroconf.d.ts), so store the instance in auseRefand implementrescanas stop+clear+scan again (optionally also setscanningtofalseon"stop").
🔧 Proposed fix
-import { useEffect, useState } from "react";
+import { useEffect, useRef, useState } from "react";
import { AppState, type AppStateStatus } from "react-native";
import Zeroconf, { type Service } from "react-native-zeroconf";
@@
export function useZeroconfDiscovery(): ZeroconfDiscovery {
const [agents, setAgents] = useState<DiscoveredAgent[]>([]);
const [scanning, setScanning] = useState(false);
+ const zeroconfRef = useRef<Zeroconf | null>(null);
+
+ const restartScan = useCallback(() => {
+ const zc = zeroconfRef.current;
+ if (!zc) return;
+ setAgents([]);
+ zc.stop();
+ setScanning(true);
+ zc.scan(SERVICE_TYPE, "tcp", "local.");
+ }, []);
useEffect(() => {
const zeroconf = new Zeroconf();
+ zeroconfRef.current = zeroconf;
@@
return () => {
sub.remove();
zeroconf.stop();
zeroconf.removeDeviceListeners();
+ zeroconfRef.current = null;
};
}, []);
- return { agents, scanning, rescan: () => setAgents([]) };
+ return { agents, scanning, rescan: restartScan };
}(Also add useCallback to the React import.)
🤖 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 `@DevDashboard/mobile/src/transport/lan-discovery.ts` around lines 36 - 71, The
rescan currently only clears agents and cannot restart discovery because the
Zeroconf instance is scoped inside useEffect; change to store the Zeroconf
instance in a useRef (e.g., zeroconfRef) created outside useEffect, move
creation/uses to ref.current, wrap startScan and rescan in useCallback, and
implement rescan to call zeroconfRef.current.stop(), setAgents([]), then
startScan() so discovery restarts; also ensure scanning state is set true in
startScan and set to false on both "error" and the Zeroconf "stop" event, and
keep cleanup to stop() and removeDeviceListeners() on the ref.
| ``` | ||
| DevDashboard/ | ||
| README.md ← you are here (product hub) | ||
| DECISIONS.md ← canonical decision log (always read first) | ||
| PRODUCT-ROADMAP.md ← expanded audiences + feature roadmap + monetization + positioning | ||
| research/ ← 11 verified research reports (00 baseline → 10 terminal synthesis) | ||
| cloud/landing/ ← 3 landing-page directions to choose from (obsidian-terminal / field-notes / daylight) | ||
| mobile/ ← the Expo SDK 55 app (scaffolded by plan 04) | ||
| DevDashboard/plans/ ← the 13 implementation plans (ADR + 00–11), committed for durability | ||
| .claude/plans/2026-05-29-DevDashboardMobile-*.md ← same plans, local working copies (gitignored) | ||
| src/dev-dashboard/ ← the DevDashboard Agent (extracted backend) + contract + existing web UI | ||
| ``` |
There was a problem hiding this comment.
Add a language to the fenced code block.
The artifact-map fenced block is missing a language info string, which triggers markdown lint (MD040).
Suggested fix
-```
+```text
DevDashboard/
README.md ← you are here (product hub)
DECISIONS.md ← canonical decision log (always read first)
PRODUCT-ROADMAP.md ← expanded audiences + feature roadmap + monetization + positioning
research/ ← 11 verified research reports (00 baseline → 10 terminal synthesis)
cloud/landing/ ← 3 landing-page directions to choose from (obsidian-terminal / field-notes / daylight)
mobile/ ← the Expo SDK 55 app (scaffolded by plan 04)
DevDashboard/plans/ ← the 13 implementation plans (ADR + 00–11), committed for durability
.claude/plans/2026-05-29-DevDashboardMobile-*.md ← same plans, local working copies (gitignored)
src/dev-dashboard/ ← the DevDashboard Agent (extracted backend) + contract + existing web UI</details>
<!-- suggestion_start -->
<details>
<summary>📝 Committable suggestion</summary>
> ‼️ **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.
```suggestion
🧰 Tools
🪛 markdownlint-cli2 (0.22.1)
[warning] 18-18: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
🤖 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 `@DevDashboard/README.md` around lines 18 - 29, The fenced artifact-map block
in README.md lacks a language info string which triggers MD040; update the
opening triple-backtick for the DevDashboard tree block to include a language
identifier (e.g., add "text" so it becomes ```text) so the block is properly
annotated; locate the fenced block in README.md (the directory listing snippet
shown under "DevDashboard/") and modify only the opening fence to include the
language token.
79a3db0 to
b31a369
Compare
There was a problem hiding this comment.
Actionable comments posted: 28
♻️ Duplicate comments (12)
DevDashboard/cloud/web/src/components/auth/AuthCard.tsx (1)
28-45:⚠️ Potential issue | 🟠 Major | 🏗️ Heavy liftPlan selection from signup is not persisted or propagated.
The
planprop from/signup?plan=pro|teamis only used for display text (line 69-70) but never persisted. WhensignUp.emailis called (line 30), the plan is not included in the request. After signup, navigation to/dashboard(line 45) does not preserve the plan. Consequently,getOverview→ensureSubscriptiondefaults all new accounts totier: "free".To fix: either include
planin thesignUp.emailpayload (so Better Auth can persist it as metadata/tier), or append?plan=...when navigating to/dashboardso the dashboard setup flow can read and apply the tier.🤖 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 `@DevDashboard/cloud/web/src/components/auth/AuthCard.tsx` around lines 28 - 45, The signup flow in AuthCard (signUp.email) does not persist the selected plan and navigation to dashboard loses it; update the signUp.email call in AuthCard.tsx to include the plan value (e.g., pass plan as part of the payload/metadata) so the backend can store tier, and also ensure navigate({ to: "/dashboard" }) preserves the plan by appending ?plan=... (or include plan in the post-signup response handling) so downstream getOverview/ensureSubscription can read and apply the selected tier; adjust the signUp.email payload and the navigate invocation accordingly (references: signUp.email, navigate in AuthCard).DevDashboard/cloud/web/.gitignore (1)
22-24:⚠️ Potential issue | 🟡 Minor | ⚡ Quick win
*.pngis broader than the stated intent and may silently drop committed assets.The comment scopes this to Playwright scratch, but
*.pngignores every PNG anywhere undercloud/web— including legitimate assets you may later add (favicons, OG images,public/icons). Consider scoping the ignore to the artifacts directory.🔧 Proposed scoping
# Playwright-mcp scratch (verification artifacts, never committed) .playwright-mcp/ -*.png +.playwright-mcp/**/*.png🤖 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 `@DevDashboard/cloud/web/.gitignore` around lines 22 - 24, The .gitignore entry '*.png' is too broad and will ignore all PNGs under the repo; narrow it to only the Playwright scratch artifacts by replacing the global '*.png' pattern with a path-scoped pattern that targets the .playwright-mcp directory (e.g., use '.playwright-mcp/*.png' or '.playwright-mcp/**/.png' or similar), keeping the existing '.playwright-mcp/' entry intact so only Playwright verification artifacts are ignored and legitimate assets (favicons, public images) are not accidentally excluded.DevDashboard/mobile/app.config.ts (1)
3-8:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winFix contradictory header comment about plugin behavior.
The header comment claims both
react-native-zeroconfandexpo-cameraare config plugins, but Lines 42-45 correctly state thatreact-native-zeroconfmust NOT be inplugins. Update the header to match the actual implementation.📝 Suggested fix
-// Dynamic config (plan 02). Expo loads the static `app.json` first and passes it in as -// `config`; we MERGE the transport/trust native requirements onto it so no app.json key is -// lost. Adds: iOS Bonjour (`_devdashboard._tcp`) + local-network + camera usage strings; -// Android INTERNET / network-state / Wi-Fi-multicast / camera permissions; the -// `react-native-zeroconf` + `expo-camera` config plugins; and the `devdashboard` deep-link -// scheme used by the pairing QR. +// Dynamic config (plan 02). Expo loads the static `app.json` first and passes it in as +// `config`; we MERGE the transport/trust native requirements onto it so no app.json key is +// lost. Adds: iOS Bonjour (`_devdashboard._tcp`) + local-network + camera usage strings; +// Android INTERNET / network-state / Wi-Fi-multicast / camera permissions; the +// `expo-camera` config plugin; and the `devdashboard` deep-link scheme used by the pairing QR. +// Note: `react-native-zeroconf` is a native module with NO config plugin.As per coding guidelines: "Do not add comments that restate what the code already says; avoid obvious comments..."
🤖 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 `@DevDashboard/mobile/app.config.ts` around lines 3 - 8, The header comment incorrectly states both react-native-zeroconf and expo-camera are config plugins; update it to match the implementation by removing the claim that react-native-zeroconf is a config plugin and instead note that react-native-zeroconf must NOT be listed in the plugins array while expo-camera is applied via a config plugin. Reference the existing symbols: the header text describing iOS Bonjour/_devdashboard._tcp, local-network/camera usage strings, and the plugins array (which contains expo-camera but not react-native-zeroconf) and adjust the wording accordingly.DevDashboard/mobile/README.md (1)
26-36:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winUpdate onboarding paths to match actual directory structure.
The README references top-level
appandapp-exampledirectories, but this project usessrc/appbased on the file tree.📝 Proposed fix
-You can start developing by editing the files inside the **app** directory. This project uses [file-based routing](https://docs.expo.dev/router/introduction). +You can start developing by editing the files inside the **src/app** directory. This project uses [file-based routing](https://docs.expo.dev/router/introduction). ... -This command will move the starter code to the **app-example** directory and create a blank **app** directory where you can start developing. +This command will move the starter code to the **src/app-example** directory and create a blank **src/app** directory where you can start developing.🤖 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 `@DevDashboard/mobile/README.md` around lines 26 - 36, Update the README text so the onboarding paths reflect the actual project structure: replace references to the top-level "app" and "app-example" directories with "src/app" and "src/app-example" and clarify that the npm script "npm run reset-project" will move starter code to src/app-example and create a blank src/app; edit the paragraph containing the command and the two directory names to use these updated paths and wording.DevDashboard/mobile/src/app/(tabs)/terminals.tsx (1)
205-209:⚠️ Potential issue | 🟡 Minor | 🏗️ Heavy liftMobileKeyBar may receive stale/null renderer on initial terminal open.
Per the previous review,
rendererRef.currentis read during render (line 209) beforeDriverComponent's ref assignment completes. Whenopentransitions fromnullto a session, the first render pass sendsnulltoMobileKeyBar, which won't re-render until another state change. Promoting the renderer to state (set viaDriverComponent's ref callback) would ensureMobileKeyBaralways receives a valid, up-to-date renderer reference.🤖 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 `@DevDashboard/mobile/src/app/`(tabs)/terminals.tsx around lines 205 - 209, The MobileKeyBar is getting rendererRef.current during initial render and can be null; instead promote the renderer to React state and set it from DriverComponent's ref callback so MobileKeyBar always gets a stable value. Introduce a state variable (e.g., renderer, setRenderer) and change the DriverComponent ref to a callback that assigns rendererRef.current and also calls setRenderer(rendererRef.current); then pass the state variable (renderer) into MobileKeyBar rather than rendererRef.current so it updates when the ref is first assigned. Ensure any existing uses of rendererRef.current continue to work by keeping the ref in sync in the callback.DevDashboard/cloud/landing/obsidian-terminal/index.html (2)
168-168:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winAdd
type="button"to prevent unintended form submission.The hamburger menu button should have an explicit
type="button"attribute.🤖 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 `@DevDashboard/cloud/landing/obsidian-terminal/index.html` at line 168, The menu button with id "menuBtn" is missing an explicit type and can submit surrounding forms unintentionally; update the <button id="menuBtn" ...> element (the hamburger menu) to include type="button" so it does not act as a submit button.
721-724:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winAdd accessible label for email input.
The email input in the footer form lacks an associated label or
aria-labelfor screen reader accessibility.🤖 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 `@DevDashboard/cloud/landing/obsidian-terminal/index.html` around lines 721 - 724, The email input lacks an accessible label; add one by giving the input an id (e.g., id="newsletter-email") and either add a visually-hidden <label for="newsletter-email">Email address</label> or add an aria-label/aria-labelledby on the input (e.g., aria-label="Email address"), targeting the input element (input[type="email"]) inside the form with onsubmit="return false;"; ensure the required attribute remains and the label text is descriptive such as "Email address".DevDashboard/mobile/e2e/specs/quick-commands.spec.ts (1)
25-25:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winError swallowing could hide real test failures.
Same issue as in
reminders-todos.spec.tsandnetwork-status.spec.ts: the.catch(() => false)pattern silently converts all errors intofalse, making debugging harder when tests fail unexpectedly.🤖 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 `@DevDashboard/mobile/e2e/specs/quick-commands.spec.ts` at line 25, The test currently swallows all errors by using .catch(() => false) on connectPage.isShown(), which hides real failures; update the check around connectPage.isShown() to stop swallowing errors—either await connectPage.isShown() directly (letting the test fail on unexpected exceptions) or catch only expected/known errors and rethrow or log unexpected ones; locate the use of connectPage.isShown() in quick-commands.spec.ts and replace the blanket .catch(() => false) with proper error handling that surfaces real failures.DevDashboard/mobile/e2e/specs/network-status.spec.ts (1)
18-18:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winError swallowing could hide real test failures.
Same issue as in
reminders-todos.spec.ts: the.catch(() => false)pattern silently converts all errors intofalse, making debugging harder when tests fail unexpectedly.🤖 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 `@DevDashboard/mobile/e2e/specs/network-status.spec.ts` at line 18, The test is swallowing errors by using connectPage.isShown().catch(() => false), which hides real failures; change this to await the call inside a try/catch (or remove the catch) so errors propagate or are explicitly asserted/logged. For example, wrap await connectPage.isShown() in a try { const shown = await connectPage.isShown(); if (shown) { ... } } catch (err) { throw err; } or replace the catch handler with one that logs and rethrows—target the connectPage.isShown() call in network-status.spec.ts (same pattern as in reminders-todos.spec.ts).DevDashboard/mobile/src/components/connect/QrScanner.tsx (1)
40-47:⚠️ Potential issue | 🟠 Major | ⚡ Quick win
scannedOncepermanently blocks retries after the first scan.After one QR detection (including invalid codes or failed pairing),
scannedOnceis set and further scans are blocked for the component's lifetime. If the user scans an invalid QR or pairing fails, they cannot retry without remounting.🔄 Proposed fix
Change
onScannedto return a boolean indicating success, and only setscannedOnce(true)when successful:-export function QrScanner({ onScanned }: { onScanned: (data: string) => void }) { +export function QrScanner({ onScanned }: { onScanned: (data: string) => Promise<boolean> | boolean }) { @@ - onBarcodeScanned={({ data }) => { + onBarcodeScanned={async ({ data }) => { if (scannedOnce) { return; } - setScannedOnce(true); - onScanned(data); + try { + const ok = await onScanned(data); + if (ok) { + setScannedOnce(true); + } + } catch { + // Allow retry on exception + } }}Then update the parent
onQrScannedinconnect.tsxto returntrueon success,falseon failure.🤖 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 `@DevDashboard/mobile/src/components/connect/QrScanner.tsx` around lines 40 - 47, The onBarcodeScanned handler currently sets the scannedOnce flag unconditionally, blocking all further scans; change the contract so onScanned (the prop passed from connect.tsx, e.g. onQrScanned) returns a boolean indicating success, then only call setScannedOnce(true) when onScanned(data) returns true; update the handler in QrScanner.tsx (onBarcodeScanned, scannedOnce, setScannedOnce) to await/capture the boolean result and conditionally set the flag, and update the parent onQrScanned implementation in connect.tsx to return true on successful pairing and false on failure.DevDashboard/mobile/src/app/connect.tsx (2)
153-153:⚠️ Potential issue | 🟠 Major | ⚡ Quick winValidate the parsed port before using it.
Number.parseIntcan returnNaNor out-of-range values, which would then be written intosetLanandbaseUrl.🛡️ Proposed fix
const url = new URL(cleaned.startsWith("http") ? cleaned : `http://${cleaned}`); - const port = url.port ? Number.parseInt(url.port, 10) : 3042; + const parsedPort = url.port ? Number.parseInt(url.port, 10) : 3042; + if (!Number.isInteger(parsedPort) || parsedPort < 1 || parsedPort > 65535) { + throw new Error("Invalid port. Use a value between 1 and 65535."); + } + const port = parsedPort;🤖 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 `@DevDashboard/mobile/src/app/connect.tsx` at line 153, The parsed port assigned to port via Number.parseInt(url.port, 10) must be validated before use: replace the direct assignment with logic that parses url.port, checks Number.isInteger and that the value is between 1 and 65535, and falls back to the default 3042 if it is NaN or out of range; then ensure subsequent uses (setLan and baseUrl) consume this validated port variable rather than the raw parse result so no NaN or invalid port ever flows into setLan/baseUrl.
192-205:⚠️ Potential issue | 🟠 Major | ⚡ Quick winWrap
applyPairingUriin try/catch to handle exceptions.If
applyPairingUrithrows, the reachability state remains stuck in "probing" and no error message is shown to the user.🔒 Proposed fix
dispatchReach({ type: "probe-start" }); - const result = await applyPairingUri(data, password); - - if (result.ok) { - console.log("[connect] reachability dispatch: probe-ok (pairing)"); - dispatchReach({ type: "probe-ok" }); - } else { - setError(result.error ?? "Pairing failed."); - - if (tier) { - console.log(`[connect] reachability dispatch: probe-fail tier=${tier} (pairing)`); - dispatchReach({ type: "probe-fail", tier, paired: tier !== "managed" }); + try { + const result = await applyPairingUri(data, password); + if (result.ok) { + console.log("[connect] reachability dispatch: probe-ok (pairing)"); + dispatchReach({ type: "probe-ok" }); + } else { + setError(result.error ?? "Pairing failed."); + if (tier) { + console.log(`[connect] reachability dispatch: probe-fail tier=${tier} (pairing)`); + dispatchReach({ type: "probe-fail", tier, paired: tier !== "managed" }); + } } + } catch (err) { + setError(err instanceof Error ? err.message : "Pairing failed."); + if (tier) { + dispatchReach({ type: "probe-fail", tier, paired: tier !== "managed" }); + } }🤖 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 `@DevDashboard/mobile/src/app/connect.tsx` around lines 192 - 205, The call to applyPairingUri can throw and leaves reachability stuck in "probing"; wrap the await applyPairingUri(data, password) call in a try/catch inside the same block so any exception sets an error and updates reachability. In the catch, call setError with the caught error message (e.g. error?.message ?? "Pairing failed.") and ensure you dispatchReach({ type: "probe-fail", tier, paired: tier !== "managed" }) when tier is present; also log the caught error for debugging. This keeps probe-start paired with a corresponding probe-fail on exceptions while reusing the existing dispatchReach, setError, and tier variables.
🤖 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 `@DevDashboard/cloud/shared/account-model.ts`:
- Around line 78-103: CLOUD_PERSISTABLE_FIELDS contains an obsolete "accounts"
entry and a corresponding unused Account interface which causes CloudTable (and
runtime guards like assertNoKeyMaterial) to include a non-existent table; remove
the "accounts" key from CLOUD_PERSISTABLE_FIELDS and delete the unused Account
interface (or alternatively reconcile names if you intended to map to
schema.ts's account/accountSettings naming), then run TypeScript checks to
ensure CloudTable and any usages in cloud-store.ts and assertNoKeyMaterial are
updated to only reference real tables defined in schema.ts (subscriptions,
devices, managed_subdomains/managedSubdomains,
account_settings/accountSettings).
In `@DevDashboard/cloud/web/docs/install-the-agent.md`:
- Around line 56-61: The fenced code blocks containing the commands `tools
dev-dashboard tunnel setup` and `tools dev-dashboard pair` need blank lines
before and after the triple-backtick fences; update the markdown in
install-the-agent.md so each fenced block is preceded and followed by an empty
line (i.e., ensure there is a blank line above the opening ``` and a blank line
below the closing ``` surrounding the `tools dev-dashboard tunnel setup` and
`tools dev-dashboard pair` blocks and the surrounding explanatory text).
In `@DevDashboard/cloud/web/docs/security-and-trust.md`:
- Around line 40-43: The fenced code block showing forbidden field names
(privateKey, secretKey, sessionKey, sharedSecret, derivedSecret, pairingSecret,
symmetricKey, aeadKey, nonceSecret) lacks a language identifier; update the
markdown fenced block to include an explicit language tag (e.g., "text") so it
renders/accesses correctly — locate the block containing those symbol names and
add the language identifier immediately after the opening triple backticks.
In `@DevDashboard/cloud/web/package.json`:
- Line 32: The package.json currently pins the "nitro" dependency to "latest",
which breaks reproducible builds; run npm list nitro to determine the installed
version and replace the "nitro": "latest" entry in package.json with that exact
version (e.g., "nitro": "X.Y.Z"), then run npm install to update the lockfile so
the change is persisted; target the "nitro" key in package.json to make this
edit.
- Line 30: The package.json currently depends on a prerelease h3 ("h3":
"^2.0.1-rc.7"); either replace that dependency with the latest stable 2.x
release ("h3": "^2.0.0") or explicitly pin the RC (e.g., "h3": "2.0.1-rc.7") and
add a short justification comment in your repo docs explaining why the
prerelease is required; update the DevDashboard/cloud/web/package.json entry for
"h3" and commit the reasoning if you choose to keep the RC.
In `@DevDashboard/cloud/web/src/components/landing/Features.tsx`:
- Around line 59-65: In the mapping inside the Features component where {[42,
64, 38, 80, 52, 70, 46, 88].map((h, i) => ( ... ))} is used, replace the fragile
key={`${h}-${i}`} with the index-only key={i} so each generated <span> has a
stable identity for this static list; update the key attribute in that map
callback to use i instead of the template string.
In `@DevDashboard/cloud/web/src/lib/provision/cloudflare.ts`:
- Around line 28-47: The NAME_RE currently permits single-character names but
the validation message in provisionManagedSubdomain claims a 3–32 char minimum;
update validation to enforce the intended 3–32 length or adjust the message.
Specifically, change the regex constant NAME_RE (used by isValidSubdomainName)
so it requires at least 3 characters (e.g., ensure pattern enforces start char +
middle {1,30} + end char for 3–32 total), or alternatively modify the error
string thrown in provisionManagedSubdomain to reflect the actual allowed length;
keep references to NAME_RE, isValidSubdomainName, and provisionManagedSubdomain
when applying the fix.
In `@DevDashboard/cloud/web/src/routes/api.stripe.webhook.ts`:
- Around line 62-77: The handler for events "customer.subscription.updated" and
"customer.subscription.deleted" silently skips when
subscription.metadata?.accountId is missing; add a warning log before the break
that includes identifying details (e.g., event.type, subscription.id and full
subscription.metadata) so missing metadata can be diagnosed. Update the block
around where you call cloudStore.updateSubscription to log something like a
processLogger.warn or logger.warn when accountId is falsy, referencing the
subscription and event type for context.
In `@DevDashboard/cloud/web/src/routes/dashboard.settings.tsx`:
- Around line 83-114: The Toggle component lacks an accessible name; update the
Toggle function signature to accept a label prop (e.g., label: string) and apply
it as the accessible name (use aria-label={label} or aria-labelledby pointing to
a visible label) on the button element so screen readers can announce the
control, then update the place where Toggle is rendered (the call site that
currently renders Toggle) to pass the actual label text such as "Push alerts".
In `@DevDashboard/cloud/web/tests-e2e/api.spec.ts`:
- Line 17: Replace the complex boolean assertion that checks body === "null" ||
body === "" || body === "{}" with a clearer containment assertion: assert that
the array of allowed string values contains the body value (use the testing
framework's toContain-style assertion) so the test reads as "body is one of
these allowed values" rather than evaluating a chained boolean expression.
In `@DevDashboard/cloud/web/tests-e2e/auth.setup.ts`:
- Line 32: The for-loop iterates over a long inline array of routes which is
hard to scan; update the for (const path of [...]) loop so the array of route
strings is written one-per-line (vertical list) for readability — locate the for
(const path of ["/", "/signin", "/signup", "/dashboard", "/dashboard/setup",
"/dashboard/devices", "/dashboard/settings", "/dashboard/billing"]) loop and
break the array into multiple lines inside the brackets, keeping the same string
values and preserving the surrounding loop logic.
In `@DevDashboard/cloud/web/tests-e2e/dashboard-setup.spec.ts`:
- Line 48: Replace the direct call to .fill() with the hydration-aware helper
.fillHydrated() on the setup input; specifically, change the invocation on
page.getByTestId("setup-subdomain-input") from .fill("A_B C!") to
.fillHydrated("A_B C!") so the test uses the same hydration timing helper as the
valid-subdomain and pairing tests and avoids potential hydration races.
In `@DevDashboard/mobile/app.config.ts`:
- Line 28: The NSCameraUsageDescription string is duplicated between the
top-level infoPlist key (NSCameraUsageDescription) and the expo-camera plugin's
cameraPermission option; extract the permission text into a single constant
(e.g., CAMERA_PERMISSION_MSG) and reference that constant in the iOS infoPlist
(the system-controlled entry) while removing the duplicate cameraPermission
setting from the expo-camera plugin config so only the infoPlist key
(NSCameraUsageDescription) supplies the runtime permission message.
In `@DevDashboard/mobile/e2e/pages/QuickCommandsPage.page.ts`:
- Around line 96-106: The waitForLabelPresent function interpolates label
directly into an XPath which is vulnerable to breaking or injection when label
contains quotes or XPath metacharacters; update waitForLabelPresent to
sanitize/escape the label before building the XPath (or switch to a safer
selector API) so that characters like ", ', [, ] are handled correctly; locate
the XPath expression in waitForLabelPresent (the
$(`//*[contains(`@label`,"${label}")]`) usage) and replace it with an
escaped/quoted-safe version of label or a parameterized selector, ensuring the
check still uses this.ids.screen visibility plus the sanitized lookup.
In `@DevDashboard/mobile/e2e/README.md`:
- Around line 127-140: Add a blank line immediately before the TypeScript code
fence that begins the TerminalsPage example so the markdown linter rule MD031 is
satisfied; update the README so there is an empty line before the block that
defines class TerminalsPage (and its isShown method) and the exported
terminalsPage instance.
In `@DevDashboard/mobile/e2e/specs/connections.spec.ts`:
- Around line 54-56: The tests repeat the defensive null-guard `if (activeId ===
null) { return; }`; extract this into a shared helper (e.g., getActiveIdOrThrow
or ensureActiveId) or move the lookup into a beforeEach that sets a non-null
`activeId` for the tests to use, and replace the inline guards with calls to
that helper or rely on the shared setup; update occurrences referencing the
`activeId` variable in connections.spec.ts (the three spots around lines where
`activeId` is read) so tests either assert/throw when null or always have a
valid value from beforeEach.
In `@DevDashboard/mobile/e2e/specs/port-killer.spec.ts`:
- Around line 45-46: The current assertion uses (await
portKillerPage.rowExists(3042)) || (await portKillerPage.isShown()), which
falsely passes whenever the screen is visible; change it to explicitly check for
one of the valid resolved states by replacing the fallback to isShown() with
checks for either a specific empty-state or any row. Update the test to await
portKillerPage.hasAnyRow() || portKillerPage.isEmptyStateShown() ||
portKillerPage.rowExists(3042) (or add hasAnyRow()/isEmptyStateShown() helpers
to the portKillerPage if they don't exist) so the expectation verifies the
screen resolved to lsof-unavailable, an empty state, or at least one port row
before asserting truthy.
In `@DevDashboard/mobile/e2e/specs/reminders-todos.spec.ts`:
- Line 25: The current call to connectPage.isShown().catch(() => false) swallows
all errors; replace it with an explicit try/catch around the await
connectPage.isShown() call (referencing connectPage.isShown()) and either log
the caught error (e.g., console.error or your test logger) before returning
false, or only catch specific expected errors (e.g., a WebDriver/TimeoutError)
and rethrow other exceptions so real failures surface; ensure the catch block
records the error message and stack so debugging remains possible.
In `@DevDashboard/mobile/e2e/specs/tmux-presets.spec.ts`:
- Around line 56-59: The test contains an unreachable "return" immediately after
calling this.skip(); remove the redundant "return" following this.skip() in the
tmux-presets.spec.ts test (the block using tmuxPresetsPage.rowExists(name) and
this.skip())—just leave the this.skip() call and delete the trailing return to
clean up the code (same cleanup as done for the other occurrence around the
earlier tmuxPresetsPage.rowExists check).
- Around line 42-45: In the test block that checks
tmuxPresetsPage.rowExists(name), remove the unreachable "return" following
"this.skip()" since Mocha's this.skip() throws to abort the test; keep the
conditional that calls this.skip() when !await tmuxPresetsPage.rowExists(name)
but delete the subsequent "return" to clean up dead code.
In `@DevDashboard/mobile/e2e/specs/weather.spec.ts`:
- Around line 51-54: The return after calling this.skip() is unreachable because
Mocha's this.skip() throws to abort the test; locate the block using
weatherPage.hasTemp() (the async check) and remove the trailing return so only
this.skip() is invoked when the condition is false (i.e., delete the `return;`
following `this.skip();` in the conditional).
In `@DevDashboard/mobile/package.json`:
- Line 59: The package.json declares nativewind@4.2.4 but still depends on
tailwindcss@^3; update the Tailwind dependency to a v4-compatible range (e.g.,
"tailwindcss": "^4") in package.json so NativeWind v4 requirements are met, then
run your package manager (npm/yarn/pnpm install) and re-generate any Tailwind
artifacts (postcss/tailwind config) if needed to ensure config keys match
Tailwind v4; verify compatibility with the NativeWind usage in your project
after installation.
In `@DevDashboard/mobile/README.md`:
- Line 55: Summary: The phrase "open source platform" is used as a compound
adjective and should be hyphenated. Replace the occurrence of the string "open
source platform" in the README line (the link text "Expo on GitHub: View our
open source platform and contribute.") with "open-source platform" so it reads
"open-source platform". Ensure the change is made only to that descriptive text
and preserves the link and punctuation.
In `@DevDashboard/mobile/src/app/connect.tsx`:
- Line 173: The parsed port value (const port = portStr ?
Number.parseInt(portStr, 10) : 3042;) can be NaN or out of valid TCP range;
update the parsing logic in connect (and the same logic in connectLan) to
validate Number.parseInt output with Number.isInteger and ensure 1 <= port <=
65535, and if validation fails fall back to the default port (3042) or surface a
clear error; apply this to the port variable in the connect function and the
port parsing in connectLan to prevent using invalid ports.
In `@DevDashboard/mobile/src/components/animated-icon.web.tsx`:
- Around line 43-56: The keyframe step in glowKeyframe is using [DURATION /
1000] (which evaluates to 0.3) but Reanimated Keyframe keys are percentages
0–100; replace that dynamic numeric key with the intended percentage (e.g., 30)
or compute percent correctly (e.g., (DURATION / totalDuration)*100) so the
middle keyframe is at the proper 30% position; update the Keyframe object used
by glowKeyframe and ensure DURATION usage is adjusted accordingly.
In `@DevDashboard/mobile/src/components/themed-text.tsx`:
- Around line 63-67: The style object for linkPrimary currently hard-codes color
"`#3c87f7`" which bypasses theming; update the linkPrimary style in
themed-text.tsx (the linkPrimary style entry) to use a theme token instead
(e.g., theme.colors.linkPrimary or theme.colors.link) by reading the theme
passed into the component or adding a new token to the theme and referencing it
from the component so the color is derived from the theme system rather than a
literal string.
In `@DevDashboard/mobile/src/components/themed-view.tsx`:
- Around line 7-8: ThemedView currently destructures lightColor and darkColor
props but never uses them; either remove these props from the ThemedViewProps
and the destructuring in the ThemedView component, or add a short comment next
to the prop declarations and the destructuring in ThemedView explaining they are
intentionally retained for API compatibility with ThemedText/future use. Update
ThemedViewProps (and the function signature for ThemedView) to reflect the
chosen approach and run type checks to ensure no unused variable warnings
remain.
In `@DevDashboard/PRODUCT-ROADMAP.md`:
- Line 45: Update the phrase in the product roadmap where it currently reads
"keypairs" to use the correct two-word form "key pairs" — specifically in the
sentence starting "Killer feature: Multi-machine fleet view with hard
per-machine isolation." and the portion "separate E2E keypairs per pairing"
should become "separate E2E key pairs per pairing".
---
Duplicate comments:
In `@DevDashboard/cloud/landing/obsidian-terminal/index.html`:
- Line 168: The menu button with id "menuBtn" is missing an explicit type and
can submit surrounding forms unintentionally; update the <button id="menuBtn"
...> element (the hamburger menu) to include type="button" so it does not act as
a submit button.
- Around line 721-724: The email input lacks an accessible label; add one by
giving the input an id (e.g., id="newsletter-email") and either add a
visually-hidden <label for="newsletter-email">Email address</label> or add an
aria-label/aria-labelledby on the input (e.g., aria-label="Email address"),
targeting the input element (input[type="email"]) inside the form with
onsubmit="return false;"; ensure the required attribute remains and the label
text is descriptive such as "Email address".
In `@DevDashboard/cloud/web/.gitignore`:
- Around line 22-24: The .gitignore entry '*.png' is too broad and will ignore
all PNGs under the repo; narrow it to only the Playwright scratch artifacts by
replacing the global '*.png' pattern with a path-scoped pattern that targets the
.playwright-mcp directory (e.g., use '.playwright-mcp/*.png' or
'.playwright-mcp/**/.png' or similar), keeping the existing '.playwright-mcp/'
entry intact so only Playwright verification artifacts are ignored and
legitimate assets (favicons, public images) are not accidentally excluded.
In `@DevDashboard/cloud/web/src/components/auth/AuthCard.tsx`:
- Around line 28-45: The signup flow in AuthCard (signUp.email) does not persist
the selected plan and navigation to dashboard loses it; update the signUp.email
call in AuthCard.tsx to include the plan value (e.g., pass plan as part of the
payload/metadata) so the backend can store tier, and also ensure navigate({ to:
"/dashboard" }) preserves the plan by appending ?plan=... (or include plan in
the post-signup response handling) so downstream getOverview/ensureSubscription
can read and apply the selected tier; adjust the signUp.email payload and the
navigate invocation accordingly (references: signUp.email, navigate in
AuthCard).
In `@DevDashboard/mobile/app.config.ts`:
- Around line 3-8: The header comment incorrectly states both
react-native-zeroconf and expo-camera are config plugins; update it to match the
implementation by removing the claim that react-native-zeroconf is a config
plugin and instead note that react-native-zeroconf must NOT be listed in the
plugins array while expo-camera is applied via a config plugin. Reference the
existing symbols: the header text describing iOS Bonjour/_devdashboard._tcp,
local-network/camera usage strings, and the plugins array (which contains
expo-camera but not react-native-zeroconf) and adjust the wording accordingly.
In `@DevDashboard/mobile/e2e/specs/network-status.spec.ts`:
- Line 18: The test is swallowing errors by using connectPage.isShown().catch(()
=> false), which hides real failures; change this to await the call inside a
try/catch (or remove the catch) so errors propagate or are explicitly
asserted/logged. For example, wrap await connectPage.isShown() in a try { const
shown = await connectPage.isShown(); if (shown) { ... } } catch (err) { throw
err; } or replace the catch handler with one that logs and rethrows—target the
connectPage.isShown() call in network-status.spec.ts (same pattern as in
reminders-todos.spec.ts).
In `@DevDashboard/mobile/e2e/specs/quick-commands.spec.ts`:
- Line 25: The test currently swallows all errors by using .catch(() => false)
on connectPage.isShown(), which hides real failures; update the check around
connectPage.isShown() to stop swallowing errors—either await
connectPage.isShown() directly (letting the test fail on unexpected exceptions)
or catch only expected/known errors and rethrow or log unexpected ones; locate
the use of connectPage.isShown() in quick-commands.spec.ts and replace the
blanket .catch(() => false) with proper error handling that surfaces real
failures.
In `@DevDashboard/mobile/README.md`:
- Around line 26-36: Update the README text so the onboarding paths reflect the
actual project structure: replace references to the top-level "app" and
"app-example" directories with "src/app" and "src/app-example" and clarify that
the npm script "npm run reset-project" will move starter code to src/app-example
and create a blank src/app; edit the paragraph containing the command and the
two directory names to use these updated paths and wording.
In `@DevDashboard/mobile/src/app/`(tabs)/terminals.tsx:
- Around line 205-209: The MobileKeyBar is getting rendererRef.current during
initial render and can be null; instead promote the renderer to React state and
set it from DriverComponent's ref callback so MobileKeyBar always gets a stable
value. Introduce a state variable (e.g., renderer, setRenderer) and change the
DriverComponent ref to a callback that assigns rendererRef.current and also
calls setRenderer(rendererRef.current); then pass the state variable (renderer)
into MobileKeyBar rather than rendererRef.current so it updates when the ref is
first assigned. Ensure any existing uses of rendererRef.current continue to work
by keeping the ref in sync in the callback.
In `@DevDashboard/mobile/src/app/connect.tsx`:
- Line 153: The parsed port assigned to port via Number.parseInt(url.port, 10)
must be validated before use: replace the direct assignment with logic that
parses url.port, checks Number.isInteger and that the value is between 1 and
65535, and falls back to the default 3042 if it is NaN or out of range; then
ensure subsequent uses (setLan and baseUrl) consume this validated port variable
rather than the raw parse result so no NaN or invalid port ever flows into
setLan/baseUrl.
- Around line 192-205: The call to applyPairingUri can throw and leaves
reachability stuck in "probing"; wrap the await applyPairingUri(data, password)
call in a try/catch inside the same block so any exception sets an error and
updates reachability. In the catch, call setError with the caught error message
(e.g. error?.message ?? "Pairing failed.") and ensure you dispatchReach({ type:
"probe-fail", tier, paired: tier !== "managed" }) when tier is present; also log
the caught error for debugging. This keeps probe-start paired with a
corresponding probe-fail on exceptions while reusing the existing dispatchReach,
setError, and tier variables.
In `@DevDashboard/mobile/src/components/connect/QrScanner.tsx`:
- Around line 40-47: The onBarcodeScanned handler currently sets the scannedOnce
flag unconditionally, blocking all further scans; change the contract so
onScanned (the prop passed from connect.tsx, e.g. onQrScanned) returns a boolean
indicating success, then only call setScannedOnce(true) when onScanned(data)
returns true; update the handler in QrScanner.tsx (onBarcodeScanned,
scannedOnce, setScannedOnce) to await/capture the boolean result and
conditionally set the flag, and update the parent onQrScanned implementation in
connect.tsx to return true on successful pairing and false on failure.
🪄 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: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: eb277c33-0b8b-4edd-b29e-f5d619572ec7
⛔ Files ignored due to path filters (24)
DevDashboard/cloud/web/bun.lockis excluded by!**/*.lockDevDashboard/mobile/assets/expo.icon/Assets/expo-symbol 2.svgis excluded by!**/*.svgDevDashboard/mobile/assets/expo.icon/Assets/grid.pngis excluded by!**/*.pngDevDashboard/mobile/assets/images/android-icon-background.pngis excluded by!**/*.pngDevDashboard/mobile/assets/images/android-icon-foreground.pngis excluded by!**/*.pngDevDashboard/mobile/assets/images/android-icon-monochrome.pngis excluded by!**/*.pngDevDashboard/mobile/assets/images/expo-badge-white.pngis excluded by!**/*.pngDevDashboard/mobile/assets/images/expo-badge.pngis excluded by!**/*.pngDevDashboard/mobile/assets/images/expo-logo.pngis excluded by!**/*.pngDevDashboard/mobile/assets/images/favicon.pngis excluded by!**/*.pngDevDashboard/mobile/assets/images/icon.pngis excluded by!**/*.pngDevDashboard/mobile/assets/images/logo-glow.pngis excluded by!**/*.pngDevDashboard/mobile/assets/images/react-logo.pngis excluded by!**/*.pngDevDashboard/mobile/assets/images/react-logo@2x.pngis excluded by!**/*.pngDevDashboard/mobile/assets/images/react-logo@3x.pngis excluded by!**/*.pngDevDashboard/mobile/assets/images/splash-icon.pngis excluded by!**/*.pngDevDashboard/mobile/assets/images/tabIcons/explore.pngis excluded by!**/*.pngDevDashboard/mobile/assets/images/tabIcons/explore@2x.pngis excluded by!**/*.pngDevDashboard/mobile/assets/images/tabIcons/explore@3x.pngis excluded by!**/*.pngDevDashboard/mobile/assets/images/tabIcons/home.pngis excluded by!**/*.pngDevDashboard/mobile/assets/images/tabIcons/home@2x.pngis excluded by!**/*.pngDevDashboard/mobile/assets/images/tabIcons/home@3x.pngis excluded by!**/*.pngDevDashboard/mobile/assets/images/tutorial-web.pngis excluded by!**/*.pngDevDashboard/mobile/bun.lockis excluded by!**/*.lock
📒 Files selected for processing (276)
DevDashboard/DECISIONS.mdDevDashboard/PRODUCT-ROADMAP.mdDevDashboard/README.mdDevDashboard/cloud/landing/daylight/index.htmlDevDashboard/cloud/landing/field-notes/index.htmlDevDashboard/cloud/landing/obsidian-terminal/index.htmlDevDashboard/cloud/shared/account-model.tsDevDashboard/cloud/shared/data-boundary.test.tsDevDashboard/cloud/shared/data-boundary.tsDevDashboard/cloud/shared/tier-policy.test.tsDevDashboard/cloud/shared/tier-policy.tsDevDashboard/cloud/web/.env.exampleDevDashboard/cloud/web/.gitignoreDevDashboard/cloud/web/README.mdDevDashboard/cloud/web/biome.jsonDevDashboard/cloud/web/db/migrations/0000_true_energizer.sqlDevDashboard/cloud/web/db/migrations/meta/0000_snapshot.jsonDevDashboard/cloud/web/db/migrations/meta/_journal.jsonDevDashboard/cloud/web/docs/getting-started.mdDevDashboard/cloud/web/docs/install-the-agent.mdDevDashboard/cloud/web/docs/pricing-and-tiers.mdDevDashboard/cloud/web/docs/security-and-trust.mdDevDashboard/cloud/web/docs/troubleshooting.mdDevDashboard/cloud/web/drizzle.config.tsDevDashboard/cloud/web/package.jsonDevDashboard/cloud/web/playwright.config.tsDevDashboard/cloud/web/src/components/RouteError.tsxDevDashboard/cloud/web/src/components/RouteNotFound.tsxDevDashboard/cloud/web/src/components/auth/AuthCard.tsxDevDashboard/cloud/web/src/components/dashboard/Card.tsxDevDashboard/cloud/web/src/components/landing/Features.tsxDevDashboard/cloud/web/src/components/landing/Footer.tsxDevDashboard/cloud/web/src/components/landing/Hero.tsxDevDashboard/cloud/web/src/components/landing/HowItWorks.tsxDevDashboard/cloud/web/src/components/landing/Nav.tsxDevDashboard/cloud/web/src/components/landing/Pricing.tsxDevDashboard/cloud/web/src/components/landing/TrustStory.tsxDevDashboard/cloud/web/src/components/landing/icons.tsxDevDashboard/cloud/web/src/content/copy.tsDevDashboard/cloud/web/src/integrations/tanstack-query/root-provider.tsxDevDashboard/cloud/web/src/lib/auth/auth-client.tsDevDashboard/cloud/web/src/lib/auth/auth-service.tsDevDashboard/cloud/web/src/lib/auth/auth.functions.tsDevDashboard/cloud/web/src/lib/auth/auth.server.tsDevDashboard/cloud/web/src/lib/auth/auth.test.tsDevDashboard/cloud/web/src/lib/billing/billing.functions.tsDevDashboard/cloud/web/src/lib/billing/env-gating.test.tsDevDashboard/cloud/web/src/lib/billing/stripe.tsDevDashboard/cloud/web/src/lib/dashboard/dashboard.functions.tsDevDashboard/cloud/web/src/lib/db/cloud-store.tsDevDashboard/cloud/web/src/lib/db/index.tsDevDashboard/cloud/web/src/lib/db/migrate.tsDevDashboard/cloud/web/src/lib/db/schema.tsDevDashboard/cloud/web/src/lib/provision/cloudflare.tsDevDashboard/cloud/web/src/lib/server/env.tsDevDashboard/cloud/web/src/router.tsxDevDashboard/cloud/web/src/routes/__root.tsxDevDashboard/cloud/web/src/routes/api.auth.$.tsDevDashboard/cloud/web/src/routes/api.stripe.webhook.tsDevDashboard/cloud/web/src/routes/dashboard.billing.tsxDevDashboard/cloud/web/src/routes/dashboard.devices.tsxDevDashboard/cloud/web/src/routes/dashboard.index.tsxDevDashboard/cloud/web/src/routes/dashboard.settings.tsxDevDashboard/cloud/web/src/routes/dashboard.setup.tsxDevDashboard/cloud/web/src/routes/dashboard.tsxDevDashboard/cloud/web/src/routes/index.tsxDevDashboard/cloud/web/src/routes/signin.tsxDevDashboard/cloud/web/src/routes/signup.tsxDevDashboard/cloud/web/src/start.tsDevDashboard/cloud/web/src/styles/app.cssDevDashboard/cloud/web/tests-e2e/api.spec.tsDevDashboard/cloud/web/tests-e2e/auth-flow.spec.tsDevDashboard/cloud/web/tests-e2e/auth-guard.spec.tsDevDashboard/cloud/web/tests-e2e/auth.setup.tsDevDashboard/cloud/web/tests-e2e/constants.tsDevDashboard/cloud/web/tests-e2e/dashboard-billing.spec.tsDevDashboard/cloud/web/tests-e2e/dashboard-devices.spec.tsDevDashboard/cloud/web/tests-e2e/dashboard-overview.spec.tsDevDashboard/cloud/web/tests-e2e/dashboard-settings.spec.tsDevDashboard/cloud/web/tests-e2e/dashboard-setup.spec.tsDevDashboard/cloud/web/tests-e2e/helpers.tsDevDashboard/cloud/web/tests-e2e/landing.spec.tsDevDashboard/cloud/web/tests-e2e/prepare-db.tsDevDashboard/cloud/web/tests-e2e/signin.spec.tsDevDashboard/cloud/web/tests-e2e/signup.spec.tsDevDashboard/cloud/web/tsconfig.jsonDevDashboard/cloud/web/vite.config.tsDevDashboard/mobile/.claude/settings.jsonDevDashboard/mobile/.gitignoreDevDashboard/mobile/.vscode/extensions.jsonDevDashboard/mobile/.vscode/settings.jsonDevDashboard/mobile/AGENTS.mdDevDashboard/mobile/CLAUDE.mdDevDashboard/mobile/README.mdDevDashboard/mobile/app.config.tsDevDashboard/mobile/app.jsonDevDashboard/mobile/assets/expo.icon/icon.jsonDevDashboard/mobile/babel.config.jsDevDashboard/mobile/e2e/README.mdDevDashboard/mobile/e2e/pages/ActivityTimelinePage.page.tsDevDashboard/mobile/e2e/pages/BuildLogTailPage.page.tsDevDashboard/mobile/e2e/pages/ClaudeUsagePage.page.tsDevDashboard/mobile/e2e/pages/ConnectPage.page.tsDevDashboard/mobile/e2e/pages/ConnectionsPage.page.tsDevDashboard/mobile/e2e/pages/ContainersPage.page.tsDevDashboard/mobile/e2e/pages/DaemonPage.page.tsDevDashboard/mobile/e2e/pages/DiskJanitorPage.page.tsDevDashboard/mobile/e2e/pages/MoreNav.page.tsDevDashboard/mobile/e2e/pages/NeedsInputInboxPage.page.tsDevDashboard/mobile/e2e/pages/NetworkStatusPage.page.tsDevDashboard/mobile/e2e/pages/ObsidianPage.page.tsDevDashboard/mobile/e2e/pages/PortKillerPage.page.tsDevDashboard/mobile/e2e/pages/ProcessMonitorPage.page.tsDevDashboard/mobile/e2e/pages/PulsePage.page.tsDevDashboard/mobile/e2e/pages/QaPage.page.tsDevDashboard/mobile/e2e/pages/QuickCommandsPage.page.tsDevDashboard/mobile/e2e/pages/RemindersTodosPage.page.tsDevDashboard/mobile/e2e/pages/TerminalsPage.page.tsDevDashboard/mobile/e2e/pages/TmuxPresetsPage.page.tsDevDashboard/mobile/e2e/pages/WeatherPage.page.tsDevDashboard/mobile/e2e/pages/app.page.tsDevDashboard/mobile/e2e/pages/base.page.tsDevDashboard/mobile/e2e/pages/testAgent.tsDevDashboard/mobile/e2e/specs/activity-timeline.spec.tsDevDashboard/mobile/e2e/specs/build-log-tail.spec.tsDevDashboard/mobile/e2e/specs/claude-usage.spec.tsDevDashboard/mobile/e2e/specs/connect.spec.tsDevDashboard/mobile/e2e/specs/connections.spec.tsDevDashboard/mobile/e2e/specs/containers.spec.tsDevDashboard/mobile/e2e/specs/daemon.spec.tsDevDashboard/mobile/e2e/specs/disk-janitor.spec.tsDevDashboard/mobile/e2e/specs/features-rest.smoke.spec.tsDevDashboard/mobile/e2e/specs/more.spec.tsDevDashboard/mobile/e2e/specs/needs-input-inbox.spec.tsDevDashboard/mobile/e2e/specs/network-status.spec.tsDevDashboard/mobile/e2e/specs/obsidian.spec.tsDevDashboard/mobile/e2e/specs/port-killer.spec.tsDevDashboard/mobile/e2e/specs/process-monitor.spec.tsDevDashboard/mobile/e2e/specs/pulse.spec.tsDevDashboard/mobile/e2e/specs/qa.spec.tsDevDashboard/mobile/e2e/specs/quick-commands.spec.tsDevDashboard/mobile/e2e/specs/reminders-todos.spec.tsDevDashboard/mobile/e2e/specs/smoke.spec.tsDevDashboard/mobile/e2e/specs/terminals.spec.tsDevDashboard/mobile/e2e/specs/tmux-presets.spec.tsDevDashboard/mobile/e2e/specs/weather.spec.tsDevDashboard/mobile/e2e/tsconfig.jsonDevDashboard/mobile/e2e/wdio.conf.tsDevDashboard/mobile/eslint.config.jsDevDashboard/mobile/metro.config.jsDevDashboard/mobile/nativewind-env.d.tsDevDashboard/mobile/package.jsonDevDashboard/mobile/patches/react-native-css-interop@0.2.4.patchDevDashboard/mobile/patches/react-native-webview@13.16.0.patchDevDashboard/mobile/scripts/reset-project.jsDevDashboard/mobile/src/api/client-provider.tsxDevDashboard/mobile/src/api/mock-client.tsDevDashboard/mobile/src/api/query-keys.tsDevDashboard/mobile/src/app/(more)/_layout.tsxDevDashboard/mobile/src/app/(more)/activity-timeline.tsxDevDashboard/mobile/src/app/(more)/build-log-tail.tsxDevDashboard/mobile/src/app/(more)/claude-usage.tsxDevDashboard/mobile/src/app/(more)/connections.tsxDevDashboard/mobile/src/app/(more)/containers.tsxDevDashboard/mobile/src/app/(more)/daemon.tsxDevDashboard/mobile/src/app/(more)/disk-janitor.tsxDevDashboard/mobile/src/app/(more)/needs-input-inbox.tsxDevDashboard/mobile/src/app/(more)/network-status.tsxDevDashboard/mobile/src/app/(more)/port-killer.tsxDevDashboard/mobile/src/app/(more)/process-monitor.tsxDevDashboard/mobile/src/app/(more)/quick-commands.tsxDevDashboard/mobile/src/app/(more)/reminders-todos.tsxDevDashboard/mobile/src/app/(more)/tmux-presets.tsxDevDashboard/mobile/src/app/(more)/weather.tsxDevDashboard/mobile/src/app/(tabs)/_layout.tsxDevDashboard/mobile/src/app/(tabs)/index.tsxDevDashboard/mobile/src/app/(tabs)/more.tsxDevDashboard/mobile/src/app/(tabs)/obsidian.tsxDevDashboard/mobile/src/app/(tabs)/qa.tsxDevDashboard/mobile/src/app/(tabs)/terminals.tsxDevDashboard/mobile/src/app/_layout.tsxDevDashboard/mobile/src/app/connect.tsxDevDashboard/mobile/src/app/pair.tsxDevDashboard/mobile/src/components/animated-icon.module.cssDevDashboard/mobile/src/components/animated-icon.tsxDevDashboard/mobile/src/components/animated-icon.web.tsxDevDashboard/mobile/src/components/connect/QrScanner.tsxDevDashboard/mobile/src/components/connect/ReachabilityBadge.tsxDevDashboard/mobile/src/components/connect/TierPicker.tsxDevDashboard/mobile/src/components/external-link.tsxDevDashboard/mobile/src/components/hint-row.tsxDevDashboard/mobile/src/components/themed-text.tsxDevDashboard/mobile/src/components/themed-view.tsxDevDashboard/mobile/src/components/ui/collapsible.tsxDevDashboard/mobile/src/components/web-badge.tsxDevDashboard/mobile/src/constants/theme.tsDevDashboard/mobile/src/features/activity-timeline/components/EventRow.tsxDevDashboard/mobile/src/features/activity-timeline/components/HourGroup.tsxDevDashboard/mobile/src/features/activity-timeline/components/Timeline.tsxDevDashboard/mobile/src/features/activity-timeline/hooks.tsDevDashboard/mobile/src/features/activity-timeline/queries.test.tsDevDashboard/mobile/src/features/activity-timeline/queries.tsDevDashboard/mobile/src/features/activity-timeline/types.tsDevDashboard/mobile/src/features/activity-timeline/units.test.tsDevDashboard/mobile/src/features/activity-timeline/units.tsDevDashboard/mobile/src/features/build-log-tail/components/LogStream.tsxDevDashboard/mobile/src/features/build-log-tail/components/RunPicker.tsxDevDashboard/mobile/src/features/build-log-tail/hooks.tsDevDashboard/mobile/src/features/build-log-tail/queries.test.tsDevDashboard/mobile/src/features/build-log-tail/queries.tsDevDashboard/mobile/src/features/build-log-tail/subscription.test.tsDevDashboard/mobile/src/features/build-log-tail/subscription.tsDevDashboard/mobile/src/features/build-log-tail/types.tsDevDashboard/mobile/src/features/build-log-tail/units.test.tsDevDashboard/mobile/src/features/build-log-tail/units.tsDevDashboard/mobile/src/features/claude-usage/components/AccountHistoryCharts.tsxDevDashboard/mobile/src/features/claude-usage/components/AccountUsageCard.tsxDevDashboard/mobile/src/features/claude-usage/components/RangeSelector.tsxDevDashboard/mobile/src/features/claude-usage/hooks.tsDevDashboard/mobile/src/features/claude-usage/queries.test.tsDevDashboard/mobile/src/features/claude-usage/queries.tsDevDashboard/mobile/src/features/claude-usage/units.test.tsDevDashboard/mobile/src/features/claude-usage/units.tsDevDashboard/mobile/src/features/connections/ConnectionForm.tsxDevDashboard/mobile/src/features/connections/ConnectionRow.tsxDevDashboard/mobile/src/features/connections/ConnectionsScreen.tsxDevDashboard/mobile/src/features/connections/components.tsxDevDashboard/mobile/src/features/connections/format.tsDevDashboard/mobile/src/features/connections/index.tsDevDashboard/mobile/src/features/connections/store.tsDevDashboard/mobile/src/features/connections/types.tsDevDashboard/mobile/src/features/containers/components/ContainerRow.tsxDevDashboard/mobile/src/features/containers/hooks.tsDevDashboard/mobile/src/features/containers/queries.test.tsDevDashboard/mobile/src/features/containers/queries.tsDevDashboard/mobile/src/features/containers/units.test.tsDevDashboard/mobile/src/features/containers/units.tsDevDashboard/mobile/src/features/daemon/components/DaemonStatusHeader.tsxDevDashboard/mobile/src/features/daemon/components/RunLogSheet.tsxDevDashboard/mobile/src/features/daemon/components/RunRow.tsxDevDashboard/mobile/src/features/daemon/hooks.tsDevDashboard/mobile/src/features/daemon/queries.test.tsDevDashboard/mobile/src/features/daemon/queries.tsDevDashboard/mobile/src/features/daemon/units.test.tsDevDashboard/mobile/src/features/daemon/units.tsDevDashboard/mobile/src/features/disk-janitor/components/UsageBars.tsxDevDashboard/mobile/src/features/disk-janitor/hooks.tsDevDashboard/mobile/src/features/disk-janitor/queries.test.tsDevDashboard/mobile/src/features/disk-janitor/queries.tsDevDashboard/mobile/src/features/disk-janitor/units.test.tsDevDashboard/mobile/src/features/disk-janitor/units.tsDevDashboard/mobile/src/features/needs-input-inbox/attention-target-store.tsDevDashboard/mobile/src/features/needs-input-inbox/components/AttentionItem.tsxDevDashboard/mobile/src/features/needs-input-inbox/components/AttentionList.tsxDevDashboard/mobile/src/features/needs-input-inbox/hooks.tsDevDashboard/mobile/src/features/needs-input-inbox/index.tsxDevDashboard/mobile/src/features/needs-input-inbox/queries.test.tsDevDashboard/mobile/src/features/needs-input-inbox/queries.tsDevDashboard/mobile/src/features/needs-input-inbox/select.test.tsDevDashboard/mobile/src/features/needs-input-inbox/select.tsDevDashboard/mobile/src/features/needs-input-inbox/types.tsDevDashboard/mobile/src/features/network-status/components/RepairButton.tsxDevDashboard/mobile/src/features/network-status/components/StatusCard.tsxDevDashboard/mobile/src/features/network-status/hooks.tsDevDashboard/mobile/src/features/network-status/queries.test.tsDevDashboard/mobile/src/features/network-status/queries.tsDevDashboard/mobile/src/features/network-status/types.tsDevDashboard/mobile/src/features/network-status/units.test.tsDevDashboard/mobile/src/features/network-status/units.tsDevDashboard/mobile/src/features/obsidian/components/NewFolderModal.tsxDevDashboard/mobile/src/features/obsidian/components/NoteReader.tsxDevDashboard/mobile/src/features/obsidian/components/NoteRenderer.tsxDevDashboard/mobile/src/features/obsidian/components/VaultTree.tsxDevDashboard/mobile/src/features/obsidian/components/VaultTreeNode.tsxDevDashboard/mobile/src/features/obsidian/expanded-dirs.test.tsDevDashboard/mobile/src/features/obsidian/expanded-dirs.ts
| /** The exhaustive allow-list of fields the cloud is PERMITTED to persist, per table. */ | ||
| export const CLOUD_PERSISTABLE_FIELDS = { | ||
| accounts: ["id", "email", "name", "createdAt"], | ||
| subscriptions: [ | ||
| "id", | ||
| "accountId", | ||
| "tier", | ||
| "status", | ||
| "stripeCustomerId", | ||
| "stripeSubscriptionId", | ||
| "currentPeriodEnd", | ||
| "createdAt", | ||
| ], | ||
| devices: ["id", "accountId", "label", "kind", "publicKey", "pairedAt"], | ||
| managed_subdomains: [ | ||
| "id", | ||
| "accountId", | ||
| "hostname", | ||
| "name", | ||
| "routingTarget", | ||
| "vendorFronted", | ||
| "status", | ||
| "createdAt", | ||
| ], | ||
| account_settings: ["accountId", "pushAlertsEnabled", "theme", "updatedAt"], | ||
| } as const; |
There was a problem hiding this comment.
Remove "accounts" entry — table does not exist in schema.
Line 80 includes accounts: ["id", "email", "name", "createdAt"] in the allow-list, but no accounts table is defined in schema.ts. The schema contains Better-Auth tables (user, session, account for OAuth, verification) and domain tables (subscriptions, devices, managedSubdomains, accountSettings). The cloud-store.ts never writes to an "accounts" table. This creates dead configuration and type confusion: CloudTable (line 105) will include "accounts" as a valid table name, but calling assertNoKeyMaterial("accounts", ...) will guard a non-existent table.
Remove the accounts entry and the unused Account interface (lines 13-18), or clarify the intent if this is forward-looking configuration.
🗑️ Proposed fix
-export interface Account {
- id: string;
- email: string;
- name: string | null;
- createdAt: string;
-}
-
export interface Subscription { export const CLOUD_PERSISTABLE_FIELDS = {
- accounts: ["id", "email", "name", "createdAt"],
subscriptions: [🤖 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 `@DevDashboard/cloud/shared/account-model.ts` around lines 78 - 103,
CLOUD_PERSISTABLE_FIELDS contains an obsolete "accounts" entry and a
corresponding unused Account interface which causes CloudTable (and runtime
guards like assertNoKeyMaterial) to include a non-existent table; remove the
"accounts" key from CLOUD_PERSISTABLE_FIELDS and delete the unused Account
interface (or alternatively reconcile names if you intended to map to
schema.ts's account/accountSettings naming), then run TypeScript checks to
ensure CloudTable and any usages in cloud-store.ts and assertNoKeyMaterial are
updated to only reference real tables defined in schema.ts (subscriptions,
devices, managed_subdomains/managedSubdomains,
account_settings/accountSettings).
| - **Setup:** one guided command — | ||
| ```bash | ||
| tools dev-dashboard tunnel setup | ||
| ``` | ||
| It installs `cloudflared`, walks you through the Cloudflare login, and prints a pairing QR. No | ||
| copy-paste. |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial | 💤 Low value
Add blank lines around fenced code blocks.
Markdown best practice requires blank lines before and after fenced code blocks for better parser compatibility.
📝 Proposed fix
- **Setup:** one guided command —
+
```bash
tools dev-dashboard tunnel setup- It installs
cloudflared, walks you through the Cloudflare login, and prints a pairing QR. No
```diff
2. To register a device against your cloud account out-of-band, the agent prints a short
**device code**:
+
```bash
tools dev-dashboard pair
```
+
The device code is proof that *your Mac agent consents* — the cloud never validates or decrypts
Also applies to: 88-92
🧰 Tools
🪛 markdownlint-cli2 (0.22.1)
[warning] 57-57: Fenced code blocks should be surrounded by blank lines
(MD031, blanks-around-fences)
[warning] 59-59: Fenced code blocks should be surrounded by blank lines
(MD031, blanks-around-fences)
🤖 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 `@DevDashboard/cloud/web/docs/install-the-agent.md` around lines 56 - 61, The
fenced code blocks containing the commands `tools dev-dashboard tunnel setup`
and `tools dev-dashboard pair` need blank lines before and after the
triple-backtick fences; update the markdown in install-the-agent.md so each
fenced block is preceded and followed by an empty line (i.e., ensure there is a
blank line above the opening ``` and a blank line below the closing ```
surrounding the `tools dev-dashboard tunnel setup` and `tools dev-dashboard
pair` blocks and the surrounding explanatory text).
| ``` | ||
| privateKey secretKey sessionKey sharedSecret derivedSecret | ||
| pairingSecret symmetricKey aeadKey nonceSecret | ||
| ``` |
There was a problem hiding this comment.
Specify language identifier for fenced code block.
The fenced code block listing forbidden field names should specify a language identifier for proper rendering and accessibility.
📝 Proposed fix
-```
+```text
privateKey secretKey sessionKey sharedSecret derivedSecret
pairingSecret symmetricKey aeadKey nonceSecret</details>
<!-- suggestion_start -->
<details>
<summary>📝 Committable suggestion</summary>
> ‼️ **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.
```suggestion
🧰 Tools
🪛 markdownlint-cli2 (0.22.1)
[warning] 40-40: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
🤖 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 `@DevDashboard/cloud/web/docs/security-and-trust.md` around lines 40 - 43, The
fenced code block showing forbidden field names (privateKey, secretKey,
sessionKey, sharedSecret, derivedSecret, pairingSecret, symmetricKey, aeadKey,
nonceSecret) lacks a language identifier; update the markdown fenced block to
include an explicit language tag (e.g., "text") so it renders/accesses correctly
— locate the block containing those symbol names and add the language identifier
immediately after the opening triple backticks.
| "better-auth": "^1.2.7", | ||
| "better-sqlite3": "^12.6.0", | ||
| "drizzle-orm": "^0.45.1", | ||
| "h3": "^2.0.1-rc.7", |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Check if a stable h3 2.x version is available
npm view h3 versions --json | jq -r '.[]' | grep '^2\.' | grep -v 'rc\|beta\|alpha' | tail -1Repository: genesiscz/GenesisTools
Length of output: 72
Consider switching h3 prerelease to a stable version for production
DevDashboard/cloud/web/package.json uses "h3": "^2.0.1-rc.7" (a prerelease). The latest stable 2.x non-prerelease available is 2.0.0, so if a stable dependency is desired, use ^2.0.0/2.0.0; otherwise, pin the exact RC version and document why a prerelease is required (e.g., a specific runtime constraint).
🤖 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 `@DevDashboard/cloud/web/package.json` at line 30, The package.json currently
depends on a prerelease h3 ("h3": "^2.0.1-rc.7"); either replace that dependency
with the latest stable 2.x release ("h3": "^2.0.0") or explicitly pin the RC
(e.g., "h3": "2.0.1-rc.7") and add a short justification comment in your repo
docs explaining why the prerelease is required; update the
DevDashboard/cloud/web/package.json entry for "h3" and commit the reasoning if
you choose to keep the RC.
| "drizzle-orm": "^0.45.1", | ||
| "h3": "^2.0.1-rc.7", | ||
| "lucide-react": "^0.562.0", | ||
| "nitro": "latest", |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win
Pin nitro to a specific version instead of "latest".
Using "latest" prevents reproducible builds and can introduce unexpected breaking changes. The lockfile will capture a specific version, but the intent in package.json should be explicit.
📦 Recommended fix
Check the currently installed version:
npm list nitroThen pin to that version (example, replace X.Y.Z with actual):
- "nitro": "latest",
+ "nitro": "^X.Y.Z",🤖 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 `@DevDashboard/cloud/web/package.json` at line 32, The package.json currently
pins the "nitro" dependency to "latest", which breaks reproducible builds; run
npm list nitro to determine the installed version and replace the "nitro":
"latest" entry in package.json with that exact version (e.g., "nitro": "X.Y.Z"),
then run npm install to update the lockfile so the change is persisted; target
the "nitro" key in package.json to make this edit.
|
|
||
| try { | ||
| const [host, portStr] = lanHost.replace(/^https?:\/\//, "").split(":"); | ||
| const port = portStr ? Number.parseInt(portStr, 10) : 3042; |
There was a problem hiding this comment.
Validate the parsed port before using it.
Number.parseInt can return NaN or out-of-range values. The same validation issue exists in connectLan (line 153).
🛡️ Proposed fix
const [host, portStr] = lanHost.replace(/^https?:\/\//, "").split(":");
- const port = portStr ? Number.parseInt(portStr, 10) : 3042;
+ const parsedPort = portStr ? Number.parseInt(portStr, 10) : 3042;
+ if (!Number.isInteger(parsedPort) || parsedPort < 1 || parsedPort > 65535) {
+ throw new Error("Invalid port. Use a value between 1 and 65535.");
+ }
+ const port = parsedPort;📝 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 port = portStr ? Number.parseInt(portStr, 10) : 3042; | |
| const [host, portStr] = lanHost.replace(/^https?:\/\//, "").split(":"); | |
| const parsedPort = portStr ? Number.parseInt(portStr, 10) : 3042; | |
| if (!Number.isInteger(parsedPort) || parsedPort < 1 || parsedPort > 65535) { | |
| throw new Error("Invalid port. Use a value between 1 and 65535."); | |
| } | |
| const port = parsedPort; |
🤖 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 `@DevDashboard/mobile/src/app/connect.tsx` at line 173, The parsed port value
(const port = portStr ? Number.parseInt(portStr, 10) : 3042;) can be NaN or out
of valid TCP range; update the parsing logic in connect (and the same logic in
connectLan) to validate Number.parseInt output with Number.isInteger and ensure
1 <= port <= 65535, and if validation fails fall back to the default port (3042)
or surface a clear error; apply this to the port variable in the connect
function and the port parsing in connectLan to prevent using invalid ports.
| const glowKeyframe = new Keyframe({ | ||
| 0: { | ||
| transform: [{ rotateZ: "-180deg" }, { scale: 0.8 }], | ||
| opacity: 0, | ||
| }, | ||
| [DURATION / 1000]: { | ||
| transform: [{ rotateZ: "0deg" }, { scale: 1 }], | ||
| opacity: 1, | ||
| easing: Easing.elastic(0.7), | ||
| }, | ||
| 100: { | ||
| transform: [{ rotateZ: "7200deg" }], | ||
| }, | ||
| }); |
There was a problem hiding this comment.
Keyframe step percentage appears incorrect.
Line 48 uses [DURATION / 1000] as a keyframe step, which evaluates to 0.3 (since DURATION = 300). In Reanimated's Keyframe API, keys represent percentages from 0-100, so 0.3 means 0.3% of the animation duration—nearly the start. This is likely unintentional.
🔧 Proposed fix
If the intent was to place this keyframe at 30% of the animation:
const glowKeyframe = new Keyframe({
transform: [{ rotateZ: "-180deg" }, { scale: 0.8 }],
opacity: 0,
},
- [DURATION / 1000]: {
+ 30: {
transform: [{ rotateZ: "0deg" }, { scale: 1 }],
opacity: 1,
easing: Easing.elastic(0.7),
},
transform: [{ rotateZ: "7200deg" }],
},
});Alternatively, verify the intended keyframe timing and use the correct percentage value.
📝 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 glowKeyframe = new Keyframe({ | |
| 0: { | |
| transform: [{ rotateZ: "-180deg" }, { scale: 0.8 }], | |
| opacity: 0, | |
| }, | |
| [DURATION / 1000]: { | |
| transform: [{ rotateZ: "0deg" }, { scale: 1 }], | |
| opacity: 1, | |
| easing: Easing.elastic(0.7), | |
| }, | |
| 100: { | |
| transform: [{ rotateZ: "7200deg" }], | |
| }, | |
| }); | |
| const glowKeyframe = new Keyframe({ | |
| 0: { | |
| transform: [{ rotateZ: "-180deg" }, { scale: 0.8 }], | |
| opacity: 0, | |
| }, | |
| 30: { | |
| transform: [{ rotateZ: "0deg" }, { scale: 1 }], | |
| opacity: 1, | |
| easing: Easing.elastic(0.7), | |
| }, | |
| 100: { | |
| transform: [{ rotateZ: "7200deg" }], | |
| }, | |
| }); |
🤖 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 `@DevDashboard/mobile/src/components/animated-icon.web.tsx` around lines 43 -
56, The keyframe step in glowKeyframe is using [DURATION / 1000] (which
evaluates to 0.3) but Reanimated Keyframe keys are percentages 0–100; replace
that dynamic numeric key with the intended percentage (e.g., 30) or compute
percent correctly (e.g., (DURATION / totalDuration)*100) so the middle keyframe
is at the proper 30% position; update the Keyframe object used by glowKeyframe
and ensure DURATION usage is adjusted accordingly.
| linkPrimary: { | ||
| lineHeight: 30, | ||
| fontSize: 14, | ||
| color: "#3c87f7", | ||
| }, |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial | ⚡ Quick win
Hard-coded color bypasses the theme system.
Line 66 sets color: "#3c87f7" for the linkPrimary type, which circumvents the component's theming mechanism. The link type correctly relies on the theme color, but linkPrimary hard-codes a blue value.
Consider using a theme token instead, or document why this color must be fixed.
♻️ Proposed fix to use theme token
linkPrimary: {
lineHeight: 30,
fontSize: 14,
- color: "`#3c87f7`",
},Then in the component:
<Text
style={[
{ color: theme[themeColor ?? "text"] },
// ... other type checks ...
- type === "linkPrimary" && styles.linkPrimary,
+ type === "linkPrimary" && [styles.linkPrimary, { color: theme.primary }],
style,
]}Or add a dedicated theme token like linkPrimary to the theme definition.
🤖 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 `@DevDashboard/mobile/src/components/themed-text.tsx` around lines 63 - 67, The
style object for linkPrimary currently hard-codes color "`#3c87f7`" which bypasses
theming; update the linkPrimary style in themed-text.tsx (the linkPrimary style
entry) to use a theme token instead (e.g., theme.colors.linkPrimary or
theme.colors.link) by reading the theme passed into the component or adding a
new token to the theme and referencing it from the component so the color is
derived from the theme system rather than a literal string.
| lightColor?: string; | ||
| darkColor?: string; |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial | ⚡ Quick win
Remove unused props or document their purpose.
The lightColor and darkColor props are destructured but never used in the component body. If these are intended for future use or API compatibility with ThemedText, add a comment explaining this; otherwise, remove them.
♻️ Proposed fix to remove unused props
export type ThemedViewProps = ViewProps & {
- lightColor?: string;
- darkColor?: string;
type?: ThemeColor;
};
-export function ThemedView({ style, lightColor, darkColor, type, ...otherProps }: ThemedViewProps) {
+export function ThemedView({ style, type, ...otherProps }: ThemedViewProps) {
const theme = useTheme();🤖 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 `@DevDashboard/mobile/src/components/themed-view.tsx` around lines 7 - 8,
ThemedView currently destructures lightColor and darkColor props but never uses
them; either remove these props from the ThemedViewProps and the destructuring
in the ThemedView component, or add a short comment next to the prop
declarations and the destructuring in ThemedView explaining they are
intentionally retained for API compatibility with ThemedText/future use. Update
ThemedViewProps (and the function signature for ThemedView) to reflect the
chosen approach and run type checks to ensure no unused variable warnings
remain.
|
|
||
| ### 1.7 Agencies / consultants (multi-client machines) | ||
| - **JTBD:** "I run work for five clients on five machines/VMs. I need them side by side, clearly separated, and I must never cross client data." | ||
| - **Killer feature: Multi-machine fleet view with hard per-machine isolation.** One list of all paired machines, each its own Pulse tile + terminal set + isolated credentials/keys. Switch client context in one tap; nothing bleeds across machines (separate E2E keypairs per pairing). |
There was a problem hiding this comment.
Use two words for "key pairs".
The term should be "key pairs" (two words) rather than "keypairs".
📝 Proposed fix
-...clearly separated, and I must never cross client data." - **Killer feature: Multi-machine fleet view with hard per-machine isolation.** One list of all paired machines, each its own Pulse tile + terminal set + isolated credentials/keys. Switch client context in one tap; nothing bleeds across machines (separate E2E keypairs per pairing).
+...clearly separated, and I must never cross client data." - **Killer feature: Multi-machine fleet view with hard per-machine isolation.** One list of all paired machines, each its own Pulse tile + terminal set + isolated credentials/keys. Switch client context in one tap; nothing bleeds across machines (separate E2E key pairs per pairing).📝 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.
| - **Killer feature: Multi-machine fleet view with hard per-machine isolation.** One list of all paired machines, each its own Pulse tile + terminal set + isolated credentials/keys. Switch client context in one tap; nothing bleeds across machines (separate E2E keypairs per pairing). | |
| - **Killer feature: Multi-machine fleet view with hard per-machine isolation.** One list of all paired machines, each its own Pulse tile + terminal set + isolated credentials/keys. Switch client context in one tap; nothing bleeds across machines (separate E2E key pairs per pairing). |
🧰 Tools
🪛 LanguageTool
[grammar] ~45-~45: Ensure spelling is correct
Context: ...ng bleeds across machines (separate E2E keypairs per pairing). ### 1.8 Content creators...
(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)
🤖 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 `@DevDashboard/PRODUCT-ROADMAP.md` at line 45, Update the phrase in the product
roadmap where it currently reads "keypairs" to use the correct two-word form
"key pairs" — specifically in the sentence starting "Killer feature:
Multi-machine fleet view with hard per-machine isolation." and the portion
"separate E2E keypairs per pairing" should become "separate E2E key pairs per
pairing".
…killer, disk janitor)
…ds, tmux presets)
… tail, claude usage)
…, settings, setup)
b31a369 to
9af300c
Compare
|
Warning Review the following alerts detected in dependencies. According to your organization's Security Policy, it is recommended to resolve "Warn" alerts. Learn more about Socket for GitHub.
|
9af300c to
2418627
Compare
DevDashboard Mobile + Cloud + Agent extraction
Stacked on
feat/tmux-cmux-dev-dashboard(the parent dev-dashboard line) — base this PR on that branch, notmaster.Turns the personal
src/dev-dashboard/web app into a commercial product across three deliverables, all merged into this branch.What's here
DevDashboard/mobile/. 5 tabs: Pulse, Terminals (D12 dual WebView/xterm driver), QA (SSE live stream), Obsidian (vault + WebView note render), More → Claude Usage / Daemon / Containers / Weather / Connections. NativeWind v4, victory-native/Skia charts, TanStack Query v5 (D32), Zustand, pluggable transport (LAN/mDNS, Tailscale, self-cloudflared, managed-with-E2E).src/dev-dashboard/server/extracted fromui/vite-middleware.ts,dev-dashboard agentsubcommand, mDNS advertiser, E2E rpc (tweetnacl X25519), device-code pairing.DevDashboard/cloud/web/(TanStack Start + Nitro): Obsidian-Terminal landing, Better-Auth + SQLite (Postgres-ready, pluggable), customer dashboard + setup wizard, env-gated Cloudflare-for-SaaS + Stripe.Parent merge
Integrates the parent's tmux/cmux work into the extraction branch (merge
3b2a36f63, 19 conflicts resolved):tmux create --attach, session presets/restart-matching,/api/tmux/sessionsperf, NO_COLOR/COLORTERM color fix, keep-dialog-open-on-attach + focus-new-tab — preserved on top of the extracted architecture.Simulator status (iPhone 17 Pro Max, iOS 26.5) — ✅ verified end-to-end
App builds, installs, launches; Connect screen renders cleanly; the full post-Connect tab walkthrough is now verified via a real-assertion Appium suite (13 specs, 53 assertions green — not smoke clicks). Native/RN fixes that got us there: iOS bundleId
dev.foltyn.dev-dashboard+ deploymentTarget 16.0, removed unused lottie, Skia binary install, HermesEvent/EventTarget/CloseEventpolyfill via metrogetPolyfills(partysocket startup crash),/pairdeep-link route (was Unmatched Route), iOS ATS local-networking + Bonjour, ATS localhost cleartext exception for sim LAN connect.This session — bug list cleared + backend join + e2e suite
Filed bug list (all fixed + live-verified on sim):
allchips pinned outside the horizontal scroll; multi-select project + tag filters (OR within facet); answers render as web-parity HTML in a WebView when expanded.(more)stack), regrouped into spaced sections with icons, normal-case section titles, real glowing-dot live indicator (StatusPill web-parity).Boot-restore fix (
20768fc2d) — a paired relaunch now lands in the app, not/connect: gate boot onrestore()behind an Obsidian-style splash so the expo-routerStack.Protectedguard sees the finalbaseUrlon first render (the guard gates access but doesn't auto-navigate).3 backend items (
4d2814858,d8007462e,eb39e3179) — cmux ↔ tmux ↔ ttyd join, with manual renames always sticky:lastCommand(precedence: manual name → meaningful command → tmux session name → command); automatic naming never overwrites a hand-set name./api/cmux/snapshotresolves each pane → ttyd session id so a client can open a cmux pane as a real terminal. Unit-tested (naming.test.ts,enrich-ttyd.test.ts,sessions.test.ts).testID / a11y polish (
b2c296396) — a full testID sweep (282 ids, consistent naming, zero broken e2e selectors) surfaced a few accessibility-identifier gaps, now closed: the QA filterChipexposesaccessibilityState={{ selected }}(parity with the range/driver/cmux-tab selectors);ListRow(passive mode) +KeyValueRowforward anaccessibilityLabel; the QA answerWebViewgets a distinctqa-answer-<id>-webviewid (parity withobsidian-note-webview); and the active connection now carries an explicitconnection-active-<id>marker (the Connections e2e page-object prefers it over inferring active-by-absence). Additive only — no testID renamed or removed. Deliberately leftsession-row-<name>ids keyed on the tmux session name: names are unique by tmux and are the cmux↔tmux↔ttyd join key, so changing them would break both the e2e selectors and the join.Gates
mobile
tscclean (1 pre-existing QA test-fixture error), e2etsc0,bun test src/green, backendtsgo0. Cloudtsc/biome/build clean + tests. CI on this PR green (CodeRabbit pass, Socket Security pass).Needs follow-up
Real Cloudflare-for-SaaS + Stripe creds (provisioning/billing are env-gated stubs); on-device (non-sim) build + Appium device run; confirm Better-Auth vs WorkOS (D35); the 1 pre-existing QA
queries.test.tsfixture typing.Summary by CodeRabbit
New Features
Bug Fixes
Documentation