Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 2 additions & 6 deletions canvas/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,7 @@ COPY --from=builder /app/public ./public
EXPOSE 3000
ENV PORT=3000
ENV HOSTNAME="0.0.0.0"
# Non-root runtime — node image defaults to root, explicitly drop.
# node:20-alpine ships with a `node` user at uid/gid 1000; remove it before
# claiming 1000 for `canvas` so `addgroup -g 1000` doesn't collide.
RUN deluser --remove-home node 2>/dev/null || true; \
delgroup node 2>/dev/null || true; \
addgroup -g 1000 canvas && adduser -u 1000 -G canvas -s /bin/sh -D canvas
# Non-root runtime — use addgroup/adduser without fixed GID/UID to avoid conflicts with base image
RUN addgroup canvas 2>/dev/null || true && adduser -G canvas -s /bin/sh -D canvas 2>/dev/null || true
USER canvas
CMD ["node", "server.js"]
28 changes: 17 additions & 11 deletions canvas/src/app/orgs/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ export default function OrgsPage() {
if (error) {
return (
<Shell>
<p className="text-red-400">Error: {error}</p>
<p role="alert" className="text-red-400">Error: {error}</p>
<button
onClick={() => window.location.reload()}
className="mt-4 rounded bg-zinc-800 px-4 py-2 text-sm text-zinc-200 hover:bg-zinc-700"
Expand Down Expand Up @@ -151,10 +151,10 @@ export default function OrgsPage() {

function CheckoutBanner() {
return (
<div className="mb-6 rounded-lg border border-emerald-700 bg-emerald-950 p-4">
<div role="status" aria-live="polite" className="mb-6 rounded-lg border border-emerald-700 bg-emerald-950 p-4">
<p className="text-sm text-emerald-200">
✓ Payment confirmed. Your workspace is spinning up now — this page
refreshes automatically when it's ready.
<span aria-hidden="true">✓</span> Payment confirmed. Your workspace is spinning up now — this page
refreshes automatically when it&apos;s ready.
</p>
</div>
);
Expand Down Expand Up @@ -364,28 +364,34 @@ function CreateOrgForm({ onCreated }: { onCreated: (slug: string) => void }) {

return (
<form onSubmit={submit} className="space-y-3">
<label className="block">
<span className="text-sm text-zinc-300">Slug (URL)</span>
<div>
<label htmlFor="org-slug" className="block text-sm text-zinc-300">Slug (URL)</label>
<input
id="org-slug"
value={slug}
onChange={(e) => setSlug(e.target.value.toLowerCase())}
pattern="^[a-z][a-z0-9-]{2,31}$"
placeholder="acme"
required
aria-describedby="org-slug-hint"
className="mt-1 w-full rounded border border-zinc-700 bg-zinc-800 px-3 py-2 text-sm text-zinc-100"
/>
</label>
<label className="block">
<span className="text-sm text-zinc-300">Display name</span>
<p id="org-slug-hint" className="mt-1 text-xs text-zinc-500">
Lowercase letters, numbers, and hyphens only. Cannot be changed later.
</p>
</div>
<div>
<label htmlFor="org-name" className="block text-sm text-zinc-300">Display name</label>
<input
id="org-name"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Acme Corp"
required
className="mt-1 w-full rounded border border-zinc-700 bg-zinc-800 px-3 py-2 text-sm text-zinc-100"
/>
</label>
{err && <p className="text-sm text-red-400">{err}</p>}
</div>
{err && <p role="alert" className="text-sm text-red-400">{err}</p>}
<button
type="submit"
disabled={submitting}
Expand Down
2 changes: 2 additions & 0 deletions canvas/src/components/ApprovalBanner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,12 +71,14 @@ export function ApprovalBanner() {
)}
<div className="flex gap-2 mt-3">
<button
type="button"
onClick={() => handleDecide(approval, "approved")}
className="px-3 py-1.5 bg-emerald-600 hover:bg-emerald-500 text-xs rounded-lg text-white font-medium transition-colors"
>
Approve
</button>
<button
type="button"
onClick={() => handleDecide(approval, "denied")}
className="px-3 py-1.5 bg-zinc-700 hover:bg-zinc-600 text-xs rounded-lg text-zinc-300 transition-colors"
>
Expand Down
16 changes: 14 additions & 2 deletions canvas/src/components/ConsoleModal.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use client";

import { useEffect, useState } from "react";
import { useEffect, useRef, useState } from "react";
import { createPortal } from "react-dom";
import { api } from "@/lib/api";
import { showToast } from "@/components/Toaster";
Expand All @@ -27,11 +27,21 @@ export function ConsoleModal({ workspaceId, workspaceName, open, onClose }: Prop
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [mounted, setMounted] = useState(false);
const closeButtonRef = useRef<HTMLButtonElement>(null);

useEffect(() => {
setMounted(true);
}, []);

// Focus close button when modal opens
useEffect(() => {
if (!open) return;
const raf = requestAnimationFrame(() => {
closeButtonRef.current?.focus();
});
return () => cancelAnimationFrame(raf);
}, [open]);

useEffect(() => {
if (!open) return;
let ignore = false;
Expand Down Expand Up @@ -80,7 +90,7 @@ export function ConsoleModal({ workspaceId, workspaceName, open, onClose }: Prop

return createPortal(
<div className="fixed inset-0 z-[9999] flex items-center justify-center">
<div className="absolute inset-0 bg-black/70 backdrop-blur-sm" onClick={onClose} />
<div aria-hidden="true" className="absolute inset-0 bg-black/70 backdrop-blur-sm" onClick={onClose} />
<div
role="dialog"
aria-modal="true"
Expand All @@ -99,6 +109,7 @@ export function ConsoleModal({ workspaceId, workspaceName, open, onClose }: Prop
)}
</div>
<button
ref={closeButtonRef}
onClick={onClose}
aria-label="Close"
className="text-zinc-400 hover:text-zinc-100 text-sm px-2"
Expand All @@ -115,6 +126,7 @@ export function ConsoleModal({ workspaceId, workspaceName, open, onClose }: Prop
)}
{!loading && error && (
<div
role="alert"
className="text-[12px] text-amber-300 bg-amber-950/30 border border-amber-900/40 rounded px-3 py-2"
data-testid="console-error"
>
Expand Down
1 change: 0 additions & 1 deletion canvas/src/components/ConversationTraceModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,6 @@ export function ConversationTraceModal({ open, workspaceId: _workspaceId, onClos
<Dialog.Content
className="fixed inset-0 z-[60] flex items-center justify-center p-4"
aria-label="Conversation trace"
aria-describedby={undefined}
>
{/* Modal panel */}
<div className="relative bg-zinc-900 border border-zinc-700 rounded-xl shadow-2xl max-w-[700px] w-full max-h-[85vh] flex flex-col overflow-hidden">
Expand Down
1 change: 1 addition & 0 deletions canvas/src/components/CookieConsent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ export function CookieConsent() {
return (
<div
role="dialog"
aria-modal="true"
aria-labelledby="cookie-consent-title"
aria-describedby="cookie-consent-body"
className="fixed bottom-0 left-0 right-0 z-[9999] border-t border-zinc-800 bg-zinc-950/95 backdrop-blur-sm p-4 shadow-[0_-4px_12px_rgba(0,0,0,0.4)]"
Expand Down
1 change: 0 additions & 1 deletion canvas/src/components/CreateWorkspaceDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,6 @@ export function CreateWorkspaceButton() {
<Dialog.Overlay className="fixed inset-0 z-50 bg-black/70 backdrop-blur-sm" />
<Dialog.Content
className="fixed z-50 left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 bg-zinc-900 border border-zinc-700/60 rounded-2xl shadow-2xl shadow-black/40 w-[400px] max-h-[90vh] overflow-y-auto p-6"
aria-describedby={undefined}
>
<Dialog.Title className="text-base font-semibold text-zinc-100 mb-1">
Create Workspace
Expand Down
4 changes: 2 additions & 2 deletions canvas/src/components/DeleteCascadeConfirmDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ export function DeleteCascadeConfirmDialog({
return createPortal(
<div className="fixed inset-0 z-[9999] flex items-center justify-center">
{/* Backdrop */}
<div className="absolute inset-0 bg-black/60 backdrop-blur-sm" onClick={onCancel} />
<div aria-hidden="true" className="absolute inset-0 bg-black/60 backdrop-blur-sm" onClick={onCancel} />

{/* Dialog */}
<div
Expand All @@ -101,7 +101,7 @@ export function DeleteCascadeConfirmDialog({
{/* Warning */}
<div className="flex gap-3 mb-4">
<div className="mt-0.5 shrink-0 w-8 h-8 rounded-full bg-red-900/30 flex items-center justify-center">
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" className="text-red-400">
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" className="text-red-400" aria-hidden="true">
<path d="M8 3L14 13H2L8 3Z" stroke="currentColor" strokeWidth="1.5" strokeLinejoin="round"/>
<path d="M8 7v3M8 11.5v.5" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round"/>
</svg>
Expand Down
30 changes: 22 additions & 8 deletions canvas/src/components/MissingKeysModal.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use client";

import { useState, useEffect, useCallback } from "react";
import { useState, useEffect, useCallback, useRef } from "react";
import { api } from "@/lib/api";
import { getKeyLabel } from "@/lib/deploy-preflight";

Expand Down Expand Up @@ -38,6 +38,7 @@ export function MissingKeysModal({
}: Props) {
const [entries, setEntries] = useState<KeyEntry[]>([]);
const [globalError, setGlobalError] = useState<string | null>(null);
const firstInputRef = useRef<HTMLInputElement>(null);

// Initialize entries when modal opens or missingKeys change
useEffect(() => {
Expand All @@ -55,7 +56,14 @@ export function MissingKeysModal({
setGlobalError(null);
}, [open, missingKeys]);

// Keyboard handler
// Focus first input when modal opens
useEffect(() => {
if (!open) return;
const raf = requestAnimationFrame(() => {
firstInputRef.current?.focus();
});
return () => cancelAnimationFrame(raf);
}, [open]);
useEffect(() => {
if (!open) return;
const handler = (e: KeyboardEvent) => {
Expand Down Expand Up @@ -129,17 +137,23 @@ export function MissingKeysModal({
<div className="fixed inset-0 z-50 flex items-center justify-center">
{/* Backdrop */}
<div
aria-hidden="true"
className="absolute inset-0 bg-black/70 backdrop-blur-sm"
onClick={onCancel}
/>

{/* Dialog */}
<div className="relative bg-zinc-900 border border-zinc-700 rounded-xl shadow-2xl shadow-black/50 max-w-[440px] w-full mx-4 overflow-hidden">
<div
role="dialog"
aria-modal="true"
aria-labelledby="missing-keys-title"
className="relative bg-zinc-900 border border-zinc-700 rounded-xl shadow-2xl shadow-black/50 max-w-[440px] w-full mx-4 overflow-hidden"
>
{/* Header */}
<div className="px-5 py-4 border-b border-zinc-800">
<div className="flex items-center gap-2 mb-1">
<div className="w-5 h-5 rounded-md bg-amber-600/20 border border-amber-500/30 flex items-center justify-center">
<svg width="12" height="12" viewBox="0 0 12 12" fill="none">
<div className="w-5 h-5 rounded-md bg-amber-600/20 border border-amber-500/30 flex items-center justify-center" aria-hidden="true">
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" aria-hidden="true">
<path
d="M6 1L11 10H1L6 1Z"
stroke="#fbbf24"
Expand All @@ -150,7 +164,7 @@ export function MissingKeysModal({
<circle cx="6" cy="8.5" r="0.5" fill="#fbbf24" />
</svg>
</div>
<h3 className="text-sm font-semibold text-zinc-100">
<h3 id="missing-keys-title" className="text-sm font-semibold text-zinc-100">
Missing API Keys
</h3>
</div>
Expand Down Expand Up @@ -178,7 +192,7 @@ export function MissingKeysModal({
</div>
{entry.saved && (
<span className="text-[9px] text-emerald-400 bg-emerald-900/30 px-1.5 py-0.5 rounded flex items-center gap-1">
<svg width="8" height="8" viewBox="0 0 8 8" fill="none">
<svg width="8" height="8" viewBox="0 0 8 8" fill="none" aria-hidden="true">
<path d="M1.5 4L3.5 6L6.5 2" stroke="currentColor" strokeWidth="1.2" strokeLinecap="round" strokeLinejoin="round" />
</svg>
Saved
Expand All @@ -193,7 +207,7 @@ export function MissingKeysModal({
onChange={(e) => updateEntry(index, { value: e.target.value.trimStart() })}
placeholder={entry.key.includes("API_KEY") ? "sk-..." : "Enter value"}
type="password"
autoFocus={index === 0}
ref={index === 0 ? firstInputRef : undefined}
onKeyDown={(e) => {
if (e.key === "Enter" && entry.value.trim()) {
handleSaveKey(index);
Expand Down
6 changes: 3 additions & 3 deletions canvas/src/components/ProvisioningTimeout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -196,8 +196,8 @@ export function ProvisioningTimeout({
>
<div className="flex items-start gap-3">
{/* Warning icon */}
<div className="w-8 h-8 rounded-lg bg-amber-600/20 border border-amber-500/30 flex items-center justify-center shrink-0 mt-0.5">
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
<div aria-hidden="true" className="w-8 h-8 rounded-lg bg-amber-600/20 border border-amber-500/30 flex items-center justify-center shrink-0 mt-0.5">
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true">
<path
d="M8 2L14 13H2L8 2Z"
stroke="#fbbf24"
Expand Down Expand Up @@ -252,7 +252,7 @@ export function ProvisioningTimeout({
{/* Cancel confirmation dialog */}
{confirmingCancel && (
<div className="fixed inset-0 z-50 flex items-center justify-center">
<div className="absolute inset-0 bg-black/60" onClick={() => setConfirmingCancel(null)} />
<div aria-hidden="true" className="absolute inset-0 bg-black/60" onClick={() => setConfirmingCancel(null)} />
<div className="relative bg-zinc-900 border border-zinc-700 rounded-xl shadow-2xl p-5 max-w-[340px] w-full mx-4">
<h3 className="text-sm font-semibold text-zinc-100 mb-2">
Cancel deployment?
Expand Down
15 changes: 10 additions & 5 deletions canvas/src/components/TermsGate.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,9 +77,14 @@ export function TermsGate({ children }: { children: React.ReactNode }) {
<>
{children}
{status === "pending" && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-zinc-950/80 backdrop-blur-sm">
<div className="mx-4 max-w-lg rounded-lg border border-zinc-700 bg-zinc-900 p-6 shadow-xl">
<h2 className="text-lg font-semibold text-white">Terms &amp; conditions</h2>
<div aria-hidden="true" className="fixed inset-0 z-50 flex items-center justify-center bg-zinc-950/80 backdrop-blur-sm">
<div
role="dialog"
aria-modal="true"
aria-labelledby="terms-dialog-title"
className="mx-4 max-w-lg rounded-lg border border-zinc-700 bg-zinc-900 p-6 shadow-xl"
>
<h2 id="terms-dialog-title" className="text-lg font-semibold text-white">Terms &amp; conditions</h2>
<p className="mt-3 text-sm text-zinc-300">
Before you create an organization, please review our{" "}
<a href="/legal/terms" className="text-sky-400 underline" target="_blank" rel="noreferrer">
Expand All @@ -94,7 +99,7 @@ export function TermsGate({ children }: { children: React.ReactNode }) {
<p className="mt-3 text-xs text-zinc-500">
By agreeing you acknowledge that workspace data is stored in AWS us-east-2 (Ohio, United States).
</p>
{error && <p className="mt-3 text-sm text-red-400">{error}</p>}
{error && <p role="alert" className="mt-3 text-sm text-red-400">{error}</p>}
<div className="mt-5 flex justify-end gap-2">
<button
onClick={accept}
Expand All @@ -108,7 +113,7 @@ export function TermsGate({ children }: { children: React.ReactNode }) {
</div>
)}
{status === "error" && (
<div className="fixed bottom-4 left-4 right-4 mx-auto max-w-md rounded border border-red-800 bg-red-950 p-3 text-sm text-red-200">
<div role="alert" className="fixed bottom-4 left-4 right-4 mx-auto max-w-md rounded border border-red-800 bg-red-950 p-3 text-sm text-red-200">
Couldn&apos;t check terms status: {error ?? "unknown error"}
</div>
)}
Expand Down
8 changes: 4 additions & 4 deletions canvas/src/components/Toolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ export function Toolbar() {
title={`Stop all running tasks (${counts.activeTasks} active)`}
aria-label={stopping ? "Stopping all running tasks" : `Stop all running tasks (${counts.activeTasks} active)`}
>
<svg width="10" height="10" viewBox="0 0 16 16" fill="currentColor" className="text-red-400">
<svg width="10" height="10" viewBox="0 0 16 16" fill="currentColor" className="text-red-400" aria-hidden="true">
<rect x="2" y="2" width="12" height="12" rx="2" />
</svg>
<span className="text-[10px] text-red-300 font-medium">
Expand All @@ -177,7 +177,7 @@ export function Toolbar() {
title={`Restart ${needsRestartNodes.length} workspace${needsRestartNodes.length === 1 ? "" : "s"} that need to pick up config or secret changes`}
aria-label={restartingAll ? "Restarting workspaces" : `Restart ${needsRestartNodes.length} workspace${needsRestartNodes.length === 1 ? "" : "s"} pending config or secret changes`}
>
<svg width="10" height="10" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.8" className="text-amber-400">
<svg width="10" height="10" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.8" className="text-amber-400" aria-hidden="true">
<path d="M2 8a6 6 0 1 1 1.76 4.24M2 13v-3h3" strokeLinecap="round" strokeLinejoin="round" />
</svg>
<span className="text-[10px] text-amber-300 font-medium">
Expand Down Expand Up @@ -253,7 +253,7 @@ export function Toolbar() {
onClick={() => useCanvasStore.getState().setSearchOpen(true)}
className="flex items-center gap-1.5 px-2.5 py-1 bg-zinc-800/50 hover:bg-zinc-700/50 border border-zinc-700/40 rounded-lg transition-colors"
>
<svg width="12" height="12" viewBox="0 0 16 16" fill="none" className="text-zinc-500">
<svg width="12" height="12" viewBox="0 0 16 16" fill="none" className="text-zinc-500" aria-hidden="true">
<circle cx="7" cy="7" r="5" stroke="currentColor" strokeWidth="1.5" />
<path d="M11 11l3 3" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
</svg>
Expand All @@ -269,7 +269,7 @@ export function Toolbar() {
aria-expanded={helpOpen}
aria-label="Open quick help"
>
<svg width="12" height="12" viewBox="0 0 16 16" fill="none" className="text-zinc-500">
<svg width="12" height="12" viewBox="0 0 16 16" fill="none" className="text-zinc-500" aria-hidden="true">
<path d="M8 12v.5M6.5 6.3A1.9 1.9 0 1 1 9 8.1c-.7.4-1 .8-1 1.7" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
<circle cx="8" cy="8" r="6" stroke="currentColor" strokeWidth="1.2" />
</svg>
Expand Down
Loading
Loading