- Language: English
- Landing Page: Login/Sign-in buttons
Auth (GitHub OAuth) → Home (Boards) → Board (Kanban) → Card Detail (right panel)
- Command Palette (⌘K)
- Settings — Display/Typography
- Shortcuts (?)
- Account — Profile, statistics, account deletion
- Privacy Policy (
/privacy) — GDPR-compliant privacy policy - Terms of Use (
/terms) — Terms of service
| Category | Technology |
|---|---|
| Framework | Next.js 16 (App Router) |
| UI Library | React 19.3 |
| Language | TypeScript 7 (strict, noUncheckedIndexedAccess) |
| Styling | Tailwind CSS 4 + shadcn/ui (OKLCH tokens) |
| State | Redux Toolkit + @laststance/redux-storage-middleware |
| Database | Supabase (PostgreSQL + Auth + RLS) |
| Rich Text | Plate.js (Platejs 53) |
| D&D | @dnd-kit (core + sortable + modifiers) |
| Testing | Vitest 5, Playwright 1.63, Storybook 10 |
| Monitoring | Sentry 11 |
| Analytics | Vercel Analytics |
| Validation | Zod 4 |
| Hosting | Vercel |
- OKLCH-based scale with aligned lightness (L) values
- Dark/Light mirror mapping for consistent contrast
- CSS variables from shadcn/ui theme system (no hardcoded gray-* classes in dark mode)
- Reference: Harmonizer
- Body text: 4.5:1 or higher (WCAG AA compliant)
- UI components/icons: 3:1 or higher
- Reference: W3C WCAG 2.1
- Base font: 16px
- Step: 1px increments (minimum 12px to recommended 28px)
- Source of truth: OKLCH palette (Tailwind tokens)
--background // Base layer
--card / --popover // Elevated surfaces
--muted // Subtle backgrounds for secondary content
--border / --input // Default borders / form input borders
--ring // Focus ring
--foreground // Primary text
--muted-foreground // Secondary text
--primary / --primary-foreground
--secondary / --secondary-foreground
--accent / --accent-foreground
--destructive / --destructive-foreground
- background L≈0.98, card L≈0.96, muted L≈0.92
- foreground contrast 4.5:1+, muted-foreground 3:1+
- primary/secondary/accent with saturation, primary-foreground always AA compliant (auto black/white switch)
- background L≈0.12, card L≈0.16, muted L≈0.20
- foreground with 4.5:1+ contrast via lightness difference
- primary with elevated luminance for button/selection visibility
- Light: Default / Sunrise / Sandstone / Mint / Sky / Lavender / Rose
- Dark: Dark / Midnight / Graphite / Forest / Ocean / Plum / Rust
- Combobox search (owner/repo, topics, visibility)
- Batch addition / duplicate detection
- Organization Filter: Filter repositories by user/org (persisted to Redux/localStorage)
- One repository, one card per user (#215): a repository can sit on at most one of a user's boards. Identity is
owner/name, case-insensitive. The database enforces it. - Already placed elsewhere: the combobox lists repos held by another board (
On <board name>) under their own heading, each with a link to that board, instead of offering them. Repos on the current board stay hidden. Placements are fetched every time the combobox opens. - Repository memory (#216): a repository keeps its note, links and comment when its card leaves the board (Remove from Board, Delete Column, Delete Board). Added again, to any board, the card shows them at once. The combobox marks a repository on no board that has something saved:
Saved: note, 3 links, comment, naming only the parts that exist ("1 link" in the singular). A note or a comment counts when it holds more than whitespace, a link when it has a URL; a comment color alone does not. The marks are looked up with the placements; when that lookup fails the combobox shows no mark and still lists the placements. There is no screen that deletes a memory: to clear one, add the repository again and empty its note, links and comment. Deleting the account deletes every memory. - Panel: the combobox panel is 480px wide. From 640px of window width it stands below the Add Repositories button and ends at the right edge of the header, so all of it is on the page; below 640px it spans the screen. It ends above the bottom edge of the window, however far down its button stands: its two lists give up height and scroll by themselves, and the search box and Cancel / Add stay in view. Where less than 26rem would be left below the button, the panel moves up over the header instead, as far as the top edge of the window; and it follows its button when the header changes while the panel is open (the sidebar opens, the title is edited)
- First Board auto-creation: On sign-in, a user who has no board gets a board named "First Board" with the Software Release columns (
createFirstBoardIfNeededin the OAuth callback; the board and its columns are written together by thecreate_board_with_status_listsdatabase function)
- No delay displaying candidates with 100+ repos
- D&D/Undo works smoothly
- Organization selection persists across sessions
- A repo held by another board cannot be added or moved in as a duplicate; the UI names the holding board
- A repository removed from a board, or whose column or board was deleted, comes back with its note, links and comment when it is added again, whatever the letter case GitHub spells it in; the combobox marks it before that
- Column = Status (e.g., Suspend / Spec designing / Active / Completed)
- Column CRUD operations (2D grid layout: gridRow, gridCol)
- Column Width Constraint: CSS variable
--column-width: 320px,minmax(280px, var(--column-width)) - Card: repo name, quick note, optional meta (Stars/Updated/Visibility/Language/Topics)
- ⋯ (Overflow menu): external links, Move to Another Board and Remove from Board (see 4.1). Notes and links open from the card's Note button
- A save without an answer: a drag, an undo or a removal that gets no answer from the server within 15 seconds stays on screen. A notice says that the change could not be confirmed and offers Reload page, and the board is fetched again once the request has settled
- Favorites: Add board to favorites (star icon, prioritized in sidebar)
- Board Subtitle: Optional description below board title (max 100 chars, inline editable)
- Inline Editable Header: Board title and subtitle are click-to-edit (
InlineEditableTextcomponent) - Names without a space: a board name, a column name, a repository name, a subtitle or the title of the boards page that is one long word (snake_case, no hyphen) breaks where its line ends: in the header of a board, in a column header, on a card, on a board's card in the boards list, in a dialog and in the lists of boards and columns of the move dialog. The controls beside it stay in reach (the buttons of the board header, "Column options", the menu of a card, "Create Board"), and a dialog and its lists stay as wide as the screen on a phone. A select shows one line of the chosen name. An ordinary word is not broken to make room: a board name of one word that fits its card stays on one line there (it may run on under the star and the menu of the card), and so does a title of the boards page that fits the line
- Boards page header: the title and the subtitle are click-to-edit. From 640px on "Create Board" stands at the right end of their row; below 640px it stands under them, as the buttons of a board's header do
- Board Settings: 3-tab dialog (General, Cards, Danger Zone). On a phone, where the dialog is as wide as the screen, tabs that do not fit in one row take a second row, so the dialog does not scroll sideways
- General: Rename board, subtitle edit form, "Show Subtitle" visibility toggle
- Cards: Card display settings (description, comment, font size/weight)
- Danger Zone: Delete board with confirmation. The cards go with the board; the notes, links and comments of their repositories are kept (see Repository memory in 3.1)
- Copy Link (board header): Copies the board's own address (
/board/<id>, without a query), with a note that only the account that owns the board can open it - Move to Another Board: Move repo cards between boards via dialog (DB RPC
move_card_to_board). The card keeps its note, links and comment- The dialog can create the destination board: New Board… under the board selector, or the create form directly when the user has no other board. A board made there has one column, "Repositories"
- The dialog opens on the board the last move from this board went to, when that board is still offered (remembered in this browser). Without one it opens on the newest board
- The columns of the destination are listed in the order its board shows them, and the first one (the left-most of the first row) is selected until another is picked
- While the move or a board creation waits for the server the dialog stays open and its controls are disabled. After 15 seconds without an answer it says that the outcome could not be confirmed and can be closed again. After every move that was not confirmed the board is fetched again, and the same move can be sent a second time without harm. That fetch waits until the request has settled, so the dialog offers Reload page next to the message of a move with an unknown outcome. A move the server itself could not confirm (the database gave it no answer) is shown the same way
- A board creation that could not be confirmed is not sent a second time from the dialog, because the board may exist: Reload boards loads the list again and shows it. When the creation got no answer in 15 seconds its request is still running and that load would wait behind it, so Reload page takes the place of Reload boards
- The keyboard focus is on the Board select once the boards are loaded, and after a request that did not go through on the control needed next (Move, the name field, Reload boards, Reload page)
- Remove from Board: Deletes the card after a confirmation. Its note, links and comment are kept and come back when the repository is added to a board again (see Repository memory in 3.1). The repository on GitHub is not affected
- Delete Column: Deletes the column and its cards after a confirmation that counts them. The notes, links and comments of their repositories are kept
- Board D&D Reorder: Drag & drop board cards on
/boardspage to reorder (position persisted to DB)- GripVertical drag handle (visible on hover)
- Optimistic UI with
useOptimistic+useStatedual pattern - DragOverlay for visual feedback during drag
- Disabled on
/boards/favorites(filtered view)
- Board = Status field basis
- D&D follows conventions (GitHub Docs)
- Favorites toggle reflects immediately
- Columns don't expand to fill available space (constrained to max width)
- Board card order persists after page reload (DB-backed position)
- A card moved to another board keeps its note, links and comment
Full-featured WYSIWYG editor accessible from each RepoCard's Note button.
- Plate Editor-based rich text editor (Platejs 53)
- Fixed Toolbar: Always-visible formatting toolbar (Bold, Italic, Headings, Lists, etc.)
- Floating Toolbar: Context toolbar on text selection
- Slash Commands:
/input shows command menu (/h1, /code, /table, etc.) - Markdown Autoformat:
#→H1,*→bullet,1.→numbered,>→quote - Character count: Maximum 20,000 characters, real-time display
- Auto draft save: Auto-save during editing (Redux + localStorage persistence)
| Category | Elements |
|---|---|
| Block | Heading (H1-H3), Paragraph, Blockquote, Code Block, Table |
| List | Bullet List, Numbered List |
| Inline | Bold, Italic, Underline, Strikethrough, Inline Code, Highlight |
| Media | Link (URL embed) |
- Storage format: JSON (Slate format)
- Legacy support: Auto-migration from plain text
- Character calculation: Extract text from JSON and count
- Keyboard shortcuts work (Cmd+B=Bold, Cmd+I=Italic)
- Slash commands insert blocks
- Markdown autoformat converts correctly
- Format preserved after save and reopen
- Data persists after page refresh
Accessible from all RepoCards. What it holds belongs to the repository, not to the card: it outlives the card and comes back with the repository (see Repository memory in 3.1).
-
Comment (inline comment, max 2,000 chars. Displayed on RepoCard. See 3.5)
-
Links (55 built-in link types + custom presets)
- Built-in Types: 55 link types in 13 categories
- Hosting (Vercel, Netlify, Cloudflare Pages, etc.)
- Cloud (AWS, GCP, Azure, etc.)
- Database (Supabase, MongoDB, Firebase, etc.)
- Storage (S3, R2, GCS, etc.)
- Auth (Auth0, Clerk, NextAuth, etc.)
- Email (SendGrid, Resend, Postmark, etc.)
- Monitoring (Sentry, DataDog, Grafana, etc.)
- Testing (Playwright, Cypress, BrowserStack, etc.)
- Project (GitHub, GitLab, Jira, Linear, etc.)
- Docs (Notion, Confluence, GitBook, etc.)
- Publishing (npm, PyPI, Docker Hub, etc.)
- Payment (Stripe, LemonSqueezy, Paddle, etc.)
- Custom (user-created presets)
- Custom Presets: User-defined link types (Lucide icon selection, stored in
user_link_presetstable) - EditableUrlItem UI: Inline editing component
- Single-Edit Coordination: Only one URL editable at a time (others auto-save)
- URL Validation: Max 2083 chars, http/https only
- Debounced Validation: 300ms delay for input validation
- Delete with Undo: 5-second undo window after deletion (Sonner toast)
- Blur Handler: Prevent accidental save on Combobox/Dropdown click
- Built-in Types: 55 link types in 13 categories
-
Note (rich text, max 20,000 chars. Edited with Plate Editor via NoteModal)
Display free-text status comments on RepoCard in Card-in-Card style.
- Inline display: Show comment directly on RepoCard (no modal needed)
- Card-in-Card UI: Left border accent + background color for visual distinction
- Full text display: No truncation, card height expands with content
- Inline editing: Click to edit directly, 2,000 character limit
- Color customization: 8 colors available (primary/blue/green/amber/purple/rose/cyan/neutral)
- Per-comment color: Color stored per comment in
projectinfo.comment_color - Actions menu: Edit, Color and Delete, from a button on the comment (shown on hover and on focus, and always where the main pointer is a finger). The colors open on hover, on a click or a tap, and from the keyboard. Of the menu, only Edit opens the editor: neither its button nor Color, a color or Delete does
┌─────────────────────────────────────────┐
│ laststance/redux-vanilla ⋯ │
│ │
│ [Unmaintained] 🍵 Zero Abstraction... │ ← GitHub desc (optional)
│ │
│ ┌─────────────────────────────────────┐ │
│ │ 💬 npm release done, no feature │ │ ← Comment
│ │ additions planned for now │ │ Card-in-Card
│ │ ✏️ │ │
│ └─────────────────────────────────────┘ │
│ │
│ 📄 Note │
└─────────────────────────────────────────┘
interface BoardSettings {
cardDisplay?: {
showGitHubDescription: boolean // default: true
showComment: boolean // default: true
commentText: {
fontSize: 'sm' | 'base' | 'lg' // default: 'sm'
fontWeight: 'normal' | 'medium' | 'semibold' // default: 'normal'
}
}
showSubtitle?: boolean // default: true — controls subtitle visibility in header
}| Column | Type | Limit | Description |
|---|---|---|---|
projectinfo.comment |
TEXT | 2,000 chars | Inline comment |
projectinfo.comment_color |
TEXT | - | 8 color choice (per-comment) |
- Comment displays on RepoCard
- Full text display (no truncate)
- Toggle visibility in Board Settings
- Inline editing with instant save
- Empty state shows "+ Add comment" placeholder
- Toggle Button: Button in the sidebar's header to expand/collapse: beside the name while the sidebar is expanded from 768px, in a row of its own under the logo as the 64px rail and as the sidebar that lies over the page
- Expanded State: Width 256px (w-64), full text display
- Collapsed State: Width 64px (w-16), icons only
- Transition Animation: 300ms ease-out smooth width change
- Tooltips: On collapse, tooltip on hover for each nav item (side="right")
- State Persistence: Persisted to localStorage via Redux Storage Middleware
- Hydration Safety:
useMounted()guard pattern to prevent SSR mismatch
- useSidebar Hook: Mounted state management for hydration safety
- CSS:
transition-[width] duration-300 ease-out - Responsive: Below 768px the sidebar is the 64px icon rail at the left edge, whatever state is stored, and the page lies beside it. The toggle opens the full sidebar over the page, which keeps its place; the backdrop, Escape, a link of the sidebar and a second press on the toggle, which stays where it was in the rail, close it again. The sidebar is as high as the part of the screen that is in view. This opened state is not stored
- SSR: Renders expanded (default) state on server, applies persisted state after mount
- Instant state change on toggle button click
- Smooth animation (60fps)
- All navigation functional when collapsed
- State persists after page reload
- Tooltips display correctly when collapsed
- No hydration mismatch errors
- Profile display: GitHub avatar, username, email
- Statistics display: Board count, card count
- Account deletion: Confirmation flow with "DELETE" input for complete account deletion
- Profile info correctly fetched from GitHub auth
- All related data deleted on account deletion
- Deletion confirmation requires exact "DELETE" match
- Library:
nextjs-toploader - Style: Algora-style green bar (
oklch(0.696 0.17 162.48)) - Configuration: Height 3px, no spinner, crawl enabled, speed 200ms
- Z-index: 9999 (above all content)
- Progress bar shows on page navigation
- Does not interfere with modal/dialog overlays
- Error Boundary (
src/app/error.tsx): Global error boundary with Sentry integration- Retry and "Go to Boards" recovery actions
- Global Error (
src/app/global-error.tsx): Root-level error boundary - Board Loading (
src/app/board/[id]/loading.tsx): Skeleton UI matching KanbanBoard layout- CardSkeleton, ColumnSkeleton components
- Per-Route Boundaries: All route segments have dedicated
loading.tsxanderror.tsx- Routes:
/boards,/board/[id],/settings,/account
- Routes:
- Algorithm: Sliding window, in-memory per-process
- Scope of a count: one server instance. Production runs many, so a client gets a multiple of the configured number through before it is refused
- Bypass: Disabled in test mode (
APP_ENV=test) - Edge: Fixed-window variant in
proxy.tsfor the OAuth callback and the Sentry tunnel. The tunnel limit also applies in test mode
| Key | Max Requests | Window | Scope |
|---|---|---|---|
auth/callback |
10 | 1 min | OAuth callback (edge) |
signInWithGitHub |
10 | 1 hour | GitHub OAuth login |
deleteAccount |
3 | 1 hour | Account deletion |
githubApi |
30 | 1 min | GitHub API proxy |
addReposToBoard |
10 | 1 min | Add repository |
batchDnD |
60 | 1 min | D&D batch operations |
boardCrud |
20 | 1 min | Board CRUD |
userSettings |
20 | 1 min | User settings |
sentryTunnel |
120 | 1 min | Error reports (edge) |
- Server Actions:
withAuthResultRateLimit(key, action)wrapper - Edge (proxy.ts):
edgeRateLimit(ip, max, window)→ HTTP 429 +Retry-After: 60 - Identifier: User ID (authenticated) or IP (unauthenticated)
- Error:
"Too many {description} requests. Please try again later."
| Header | Value |
|---|---|
X-DNS-Prefetch-Control |
on |
Strict-Transport-Security |
max-age=63072000; includeSubDomains; preload |
X-Frame-Options |
SAMEORIGIN |
X-Content-Type-Options |
nosniff |
Referrer-Policy |
strict-origin-when-cross-origin |
Permissions-Policy |
camera=(), microphone=(), geolocation=(), payment=(), usb=(), bluetooth=() |
Content-Security-Policy-Report-Only |
Report-only CSP (see below) |
default-src 'self'; script-src 'self' 'unsafe-inline' https://va.vercel-scripts.com;
style-src 'self' 'unsafe-inline'; img-src 'self' https://avatars.githubusercontent.com https://github.com data: blob:;
connect-src 'self' https://*.supabase.co http://127.0.0.1:* https://api.github.com https://*.ingest.us.sentry.io https://vitals.vercel-insights.com;
font-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self'; object-src 'none';
upgrade-insecure-requests; report-uri <SENTRY_CSP_REPORT_URI>
SENTRY_CSP_REPORT_URI is the security endpoint of this app's Sentry project, derived from SENTRY_DSN in src/lib/constants/sentry.ts. upgrade-insecure-requests has no effect while the policy is report-only.
Sentry-based audit trail via logSecurityEvent() (src/lib/security-events.ts).
| Event Type | Level | Trigger |
|---|---|---|
login_success |
info | Successful OAuth |
login_failure |
warning | Failed OAuth (code exchange error) |
logout |
info | User-initiated sign out |
account_deleted |
info | Account permanently deleted |
unauthorized_access |
warning | Unauthenticated route access (proxy.ts) |
rate_limited |
warning | Request blocked by rate limiter |
- DOMPurify: All rich text (Plate.js JSON) sanitized on render via
DOMPurify.sanitize() - Board subtitle/title: Server-side length validation + client-side sanitization
- Open Redirect: OAuth callback validates redirect URL against allowlist
- E2E Coverage:
xss-smoke.spec.tsverifies script injection is neutralized
Standardized return type for client-consumed Server Actions (src/lib/actions/types.ts).
type ActionResult<T> =
| { success: true; data: T }
| { success: false; error: string; errorCode?: ActionErrorCode }
type ActionErrorCode =
'GITHUB_TOKEN_MISSING' | 'BOARD_CREATION_UNCONFIRMED' | 'MOVE_UNCONFIRMED'error is the sentence the user reads. errorCode is set only where the browser has to tell one failure from another, and the browser branches on the code, never on the sentence. It signs in again without asking (GITHUB_TOKEN_MISSING). Or it treats the outcome as unknown: the board may exist, because the database gave the server no answer or one the server could not read (BOARD_CREATION_UNCONFIRMED), or the card may have moved, because the database gave no answer (MOVE_UNCONFIRMED).
| Wrapper | Auth | Rate Limit | Returns |
|---|---|---|---|
withAuthResult() |
Yes | No | ActionResult<T> |
withAuthResultRateLimit() |
Yes | Yes | ActionResult<T> (recommended) |
withAuthRateLimit() |
Yes | Yes | Throws on failure |
Three-dot menu on each RepoCard opens "Overflow menu" for quick actions (Material Design)
Open on GitHub
Open Production URL
Open Tracking dashboard
Open Supabase dashboard
Move to Another Board // Dialog with board selector; keeps the note, links and comment
Remove from Board // Destructive, confirmation dialog; deletes the card, keeps its note, links and comment for when it is added again
.— Focused card's Overflow menuEnter— Open card⌘K/Ctrl+K— Command PaletteZ— Undo last operation (not while a dialog, a menu, a list of options, the command palette or the Add Repositories panel is open, and not while the sidebar lies over the page on a phone)Tab— Focus navigationEscape— Close the topmost menu, list or dialog. In the Add Repositories panel the open list of organizations closes first, and the panel with the next press?— Shortcuts help
┌───────────────────────────────┐
│ [Logo] GitBox [«] │ ← Collapse toggle button, beside the name
│ ───────────────────────────── │
│ Boards [▼] │ ← Collapsible
│ • All Boards │
│ • Favorites │
│ ───────────────────────────── │
│ Settings │
│ Shortcuts │
│ Theme: [Sunrise ▾] │ ← Global theme selector (14 themes + System)
│ ───────────────────────────── │
│ [Avatar] User name │ ← Account: profile, stats, deletion
│ Sign Out │
└───────────────────────────────┘
┌────────┐
│ [Logo] │
│ ────── │
│ [»] │ ← Expand toggle button, in a row of its own under the logo
│ ────── │
│ [📋] │ ← All Boards (Tooltip: "All Boards")
│ [⭐] │ ← Favorites (Tooltip: "Favorites")
│ ────── │
│ [⚙️] │ ← Settings (Tooltip: "Settings")
│ [⌨️] │ ← Shortcuts (Tooltip: "Shortcuts")
│ [🎨] │ ← Theme (Tooltip: "Theme")
│ ────── │
│ [👤] │ ← Account (Tooltip: the user's name)
│ [🚪] │ ← Sign Out (Tooltip: "Sign Out")
└────────┘
| Item | Expanded | Collapsed |
|---|---|---|
| Width | 256px (w-64) | 64px (w-16) |
| Text | Full display | Hidden |
| Icons | Left of text | Centered |
| Tooltip | None | Right side on hover |
| Toggle button | Icon, named "Collapse sidebar" | Icon, named "Expand sidebar" |
- Transition:
transition-[width] duration-300 ease-out - Content: Text fades with opacity
- Icons: Smooth position movement
┌──────────────────────────────────────────────────────────────────────┐
│ AI Experiments ○ avatar [?] │ ← Click to edit title
│ Exploring ML workflows and tooling │ ← Subtitle (click to edit, toggleable)
│ [ Add Repositories ] [ Add Column ] [ Board Settings ] [ Copy Link ] │
│ │
│ Pending Planning Focus Development Production Release │
│ ┌───────────────────────────┐ ┌───────────────────────────┐ │
│ │ repo-ml-lab [⋯] │ │ ui-research [⋯] │ │
│ │ "On hold this week" │ │ "Draft spec" │ │
│ └───────────────────────────┘ └───────────────────────────┘ │
│ ↑ drag to reorder (priority) ← drag across lists to change status │
└──────────────────────────────────────────────────────────────────────┘
┌───────────────────────────────────────────────────┐
│ Project Info – laststance/app-studio │
│---------------------------------------------------│
│ Quick note │
│ [ "Heads down on auth rollout…" ] │
│---------------------------------------------------│
│ Links │
│ • Production URL(s): https://app.example.com │
│ • Tracking: https://plausible.io/... │
│ • Supabase: https://supabase.com/... │
│---------------------------------------------------│
│ [ Save ] [ Cancel ] │
└───────────────────────────────────────────────────┘
┌───────────────────────────────────────────────┐
│ Settings │
│-----------------------------------------------│
│ Display │
│ ☑ Compact Mode │
│ ☑ Show Card Metadata (stars, language, date) │
└───────────────────────────────────────────────┘
Theme (via Sidebar):
- Access: Sidebar Theme dropdown. Its list opens beside the sidebar; in the sidebar that lies over the page below 768px it opens below its button, where all of it is on the screen
- Scope: App-wide (no board-specific settings)
- 14 themes + System: Light 7 + Dark 7 + OS preference follow
Display Settings:
- Compact Mode: Reduce card/column padding for higher information density
- Show Card Metadata: Toggle stars, language, update date visibility
Persistence: Redux + localStorage (@laststance/redux-storage-middleware)
┌───────────────────────────────────────────────────────────┐
│ Project Note [×] │
│ Add notes about owner/repo-name. Use toolbar for │
│ formatting. │
│-----------------------------------------------------------│
│ ┌───────────────────────────────────────────────────────┐ │
│ │ [B] [I] [U] [S] [~] [<>] | [H▾] | [•] [1.] | [🔗] [📊] │ │ ← Fixed Toolbar
│ └───────────────────────────────────────────────────────┘ │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ # Project Overview │ │ ← H1 Heading
│ │ │ │
│ │ This project uses **React** and _TypeScript_. │ │ ← Bold, Italic
│ │ │ │
│ │ ## Key Features │ │ ← H2 Heading
│ │ • Authentication system │ │ ← Bullet List
│ │ • Dashboard with analytics │ │
│ │ • API integration │ │
│ │ │ │
│ │ ```typescript │ │ ← Code Block
│ │ const config = { theme: 'dark' } │ │
│ │ ``` │ │
│ │ │ │
│ │ > Note: Deploy to production after testing. │ │ ← Blockquote
│ │ │ │
│ │ | Header 1 | Header 2 | Header 3 | │ │ ← Table
│ │ |----------|----------|----------| │ │
│ │ | Cell 1 | Cell 2 | Cell 3 | │ │
│ │ │ │
│ │ Type / for commands... │ │ ← Placeholder
│ │ │ │
│ └───────────────────────────────────────────────────────┘ │
│ │
│ 1,234 / 20,000 │ ← Character Count
│ Draft saved 10:30 AM │ ← Auto-save Status
│ │
│ [ Cancel ] [ Save ] │
└───────────────────────────────────────────────────────────┘
| Icon | Function | Shortcut |
|---|---|---|
| B | Bold | Cmd+B |
| I | Italic | Cmd+I |
| U | Underline | Cmd+U |
| Strikethrough | - | |
<> |
Inline Code | - |
| 🖌️ | Highlight | - |
| H▾ | Heading (H1-H3) | - |
| • | Bullet List | - |
| 1. | Numbered List | - |
| 🔗 | Link | Cmd+K |
| 📊 | Table | - |
-- Board: Kanban Board
board {
id uuid PRIMARY KEY,
user_id uuid REFERENCES auth.users,
name text NOT NULL,
subtitle text, -- Optional description (max 100 chars)
is_favorite boolean DEFAULT false, -- Favorite board
position integer DEFAULT 0, -- Board order for D&D reordering on /boards
settings jsonb, -- Board-level settings (card display, showSubtitle, etc.)
created_at, updated_at
}
-- StatusList: Kanban Column (Grid layout support)
statuslist {
id uuid PRIMARY KEY,
board_id uuid REFERENCES board,
name text NOT NULL,
color text, -- #RRGGBB or NULL (CHECK statuslist_color_hex)
order integer,
grid_row integer DEFAULT 0, -- 2D grid row position
grid_col integer DEFAULT 0, -- 2D grid column position
created_at, updated_at
}
-- RepoCard: GitHub Repository Card
repocard {
id uuid PRIMARY KEY,
board_id uuid REFERENCES board,
status_id uuid REFERENCES statuslist, -- (status_id, board_id) references statuslist (id, board_id); a column that holds cards cannot change boards
repo_name text NOT NULL,
repo_owner text NOT NULL,
order integer,
meta jsonb, -- GitHub API metadata cache (stars, language, etc.)
user_id uuid NOT NULL REFERENCES auth.users, -- always the board owner (trigger set_repocard_user_id)
created_at, updated_at,
UNIQUE (board_id, repo_owner, repo_name), -- unique_repo_per_board
UNIQUE (user_id, lower(repo_owner), lower(repo_name)) -- repocard_unique_repo_per_user (#215)
}
-- ProjectInfo: Extended Project Details
projectinfo {
id uuid PRIMARY KEY,
user_id uuid NOT NULL DEFAULT auth.uid() REFERENCES auth.users ON DELETE CASCADE,
repo_owner text NOT NULL,
repo_name text NOT NULL,
repo_key text GENERATED ALWAYS AS (lower(repo_owner) || '/' || lower(repo_name)) STORED,
-- one row per user and repository (projectinfo_unique_repo_per_user, #216): it stays when the card goes.
-- A card reads it through projectinfo(repocard), a computed relationship: repocard(..., projectinfo(...)).
-- RLS: a user reads and writes their own rows, and writes only for a repository that has a card of theirs.
links jsonb, -- {type, url, icon}[] - 55 built-in types
note text, -- Rich text (Plate Editor, max 20,000 chars)
comment text, -- Inline comment (max 2,000 chars)
comment_color text DEFAULT 'primary', -- CHECK IN ('primary','blue','green','amber','purple','rose','cyan','neutral')
created_at, updated_at
}
-- UserLinkPresets: User-defined Link Types
user_link_presets {
id uuid PRIMARY KEY,
user_id uuid REFERENCES auth.users,
label text NOT NULL, -- Display label
value text NOT NULL, -- Programmatic value
icon text, -- Lucide icon name
created_at, updated_at
}
-- UserSettings: Per-user Page Customization
user_settings {
user_id uuid PRIMARY KEY REFERENCES auth.users ON DELETE CASCADE,
boards_page_title text, -- Custom /boards page title (NULL = default)
boards_page_subtitle text, -- Custom /boards page subtitle (NULL = default)
created_at, updated_at
}// Settings Slice (Persistence: localStorage)
interface SettingsState {
theme: ThemeType // 14 themes + system (ThemeId | 'system')
compactMode: boolean
showCardMetadata: boolean // Display stars, language, and last updated on cards
organizationFilter: string // AddRepositoryCombobox filter ('all' or org login)
sidebarCollapsed: boolean // Sidebar collapse state (default: false)
moveDestinationByBoard?: Record<string, string> // Source board id → destination of the last move from it (Move to Another Board opens on it)
}
// Board Slice (not persisted: the board on screen; the server sends it with every page and it is replaced when a board opens)
interface BoardState {
statusLists: StatusListDomain[]
repoCards: RepoCardForRedux[]
}
// Draft Slice (Persistence: localStorage)
interface DraftState {
notes: Record<string, DraftNote> // Map of cardId -> DraftNote
}
interface DraftNote {
cardId: string
content: string // Rich text JSON string
links: ProjectLink[] // Draft links
lastModified: number // Timestamp
}// Fetched from GitHub API, cached in repocard.meta JSONB column
interface RepoCardMeta {
stars?: number
updatedAt?: string
visibility?: 'public' | 'private'
language?: string
topics?: string[]
description?: string
githubId?: number // GitHub repo id reported by the browser; untrusted hint
}- Measure contrast across 14 themes × representative screens (Board/Modal)
- Standards: small text 4.5:1, UI 3:1
- Build fails if not met (aligns with axe rules)
- Reference: Deque University
- No delay displaying candidates with 100+ repositories
- D&D/Undo operations work smoothly
- Column width constrained (no expansion to fill available space)
noUncheckedIndexedAccessenabled in tsconfig- Knip for dead code detection
- ESLint with
react-you-might-not-need-an-effectplugin - Storybook for component-level testing & visual review
failOnFlakyTests: truein Playwright config (strict reliability enforcement)- E2E timeout: 30 seconds (reduced from default)
- Zero
waitForTimeoutin E2E tests (replaced with assertion-based waits)
- Rate limiting on all authenticated Server Actions and edge routes
- Security headers: HSTS, CSP (report-only), X-Frame-Options, Permissions-Policy
- Security event audit trail via Sentry (
logSecurityEvent()) - GitHub token cookie TTL aligned with OAuth token expiry, cleared on 401
- Build-time guards prevent test env vars (
APP_ENV=test,NEXT_PUBLIC_ENABLE_MSW_MOCK=true) in Vercel deployments
- Overflow menu opens with the shortcut (
.) on the focused card - The Note modal (notes and links) opens from the Note button of every RepoCard, and with Enter on the focused card
- All 14 themes meet 4.5:1 for body text, 3:1+ for UI elements (includes automated tests)
- The keyboard focus stays on the board when a dialog opened from a card's menu closes: on the card after Cancel, and after Move to Another Board or Remove from Board on the next card of the column (the card above when it was the last one, the column's "Add Repo" button when it was the only one). The same holds when a move without a confirmation went through all the same and the card is gone by the time its dialog is closed
- Board/Kanban basic features
- GitHub OAuth & Repository addition
- Project Info (Notes, Links)
- 14 theme support (Light 7 + Dark 7)
- Comment on RepoCard feature
- Account management (profile, stats, deletion)
- Favorites feature
- Sidebar collapse
- Navigation progress bar ✅
- Error boundaries & loading states ✅
- Column width constraint ✅
- Dark theme neutral color fix (OKLCH CSS variables) ✅
- Hydration safety (useMounted pattern) ✅
- E2E migration to real Supabase DB ✅
- Dead code cleanup (Knip) ✅
- noUncheckedIndexedAccess strict typing ✅
- Board D&D reorder on /boards page ✅
- Theme CSS variables validation script ✅
- RLS re-enabled with optimized policies ✅
- Rate limiting on auth and API endpoints ✅
- Security headers (CSP, HSTS, Permissions-Policy) ✅
- Security event logging via Sentry ✅
- ActionResult<T> discriminated union for Server Actions ✅
- Per-route loading.tsx and error.tsx boundaries ✅
- CalVer release workflow (GitHub Releases) ✅
- failOnFlakyTests enforcement in Playwright ✅
- Component decomposition (Landing, AddRepositoryCombobox) ✅
- Inline editable board title and subtitle ✅
- Board subtitle edit form and visibility toggle in Board Settings ✅
- XSS prevention (DOMPurify sanitization, open redirect protection) ✅
- Inline editable boards page header with DB persistence ✅
- user_settings table for per-user page customization ✅
- Move to Another Board feature (dialog with board selector) ✅
- Auth guard unification (withAuth removed, 3 wrappers remain) ✅
- NoteModal decomposition (NoteSection, LinkManager, useNoteModalDraft) ✅
- KanbanBoard God Component decomposition into focused hooks ✅
- Postgres best practices hardening migration ✅
- 404 page, empty states, and hover state UX improvements ✅
- Toast feedback for silent success operations ✅
Social media preview images and metadata.
| Item | File | Size |
|---|---|---|
| OG Image | src/app/opengraph-image.tsx |
1200×630 |
| Twitter Card | src/app/twitter-image.tsx |
1200×600 |
| Favicon | src/app/icon.svg |
SVG |
| Apple Touch Icon | src/app/apple-icon.tsx |
180×180 |
// src/app/layout.tsx
export const metadata: Metadata = {
title: {
default: 'GitBox - GitHub Repository Manager',
template: '%s | GitBox',
},
description: 'Manage GitHub repositories in Kanban board format',
applicationName: 'GitBox',
keywords: ['GitHub', 'Kanban', 'Repository', 'Project Management', 'Developer Tools'],
openGraph: { ... },
twitter: { card: 'summary_large_image', ... },
}src/app/manifest.ts enables PWA installation (dynamic manifest generation).
| Field | Value |
|---|---|
| Dashboard | https://vercel.com/laststance/gitbox |
| Project ID | prj_M4T9K5HjwFx0e9PIueEhOFn1UmUM |
| Team | laststance |
| Production URL | https://gitbox-laststance.vercel.app |
| Environment | Variables |
|---|---|
| Production | NEXT_PUBLIC_SUPABASE_URL, NEXT_PUBLIC_SUPABASE_ANON_KEY (prod) |
| Preview | NEXT_PUBLIC_SUPABASE_URL, NEXT_PUBLIC_SUPABASE_ANON_KEY (prod) |
| Development | .env with the local Supabase credentials |
| Environment | Supabase Project | Purpose |
|---|---|---|
| Local | http://127.0.0.1:54321 (Docker) |
Local dev (localhost:3008) |
| Production | https://zfcwvzazzzsefbkztsyp.supabase.co |
Production and preview deployments |
The local stack and the production project each use their own GitHub OAuth App, because their callback URLs differ:
| Setting | Local | Production |
|---|---|---|
| GitHub OAuth App | GitBox Local Dev |
GitBox Tokyo |
| Authorization callback | http://127.0.0.1:54321/auth/v1/callback |
https://zfcwvzazzzsefbkztsyp.supabase.co/auth/v1/callback |
Supabase CLI reads environment variables from supabase/.env (NOT root .env):
# supabase/.env
GITHUB_CLIENT_ID=your_client_id
GITHUB_CLIENT_SECRET=your_client_secret# supabase/config.toml
[auth]
additional_redirect_urls = [
"http://localhost:3008/auth/callback",
"https://*-laststance.vercel.app/auth/callback",
"https://gitbox-laststance.vercel.app/auth/callback",
]- Create migration:
supabase migration new <description> - Write SQL in
supabase/migrations/YYYYMMDDHHMMSS_<description>.sql - Test locally:
pnpm db:resetapplies every migration to a fresh local database - Merge to
main→ Production deploys via GitHub Actions
The workflow (.github/workflows/supabase-production.yml) installs a pinned Supabase CLI version. The pin and the local CLI are raised together.
All tables have RLS enabled. Users can only access their own data via user_id match.
The board's sensors listen for mouse, touch and key events, not for pointer events (OwnElementMouseSensor, OwnElementTouchSensor and OwnElementKeyboardSensor in src/lib/utils/own-element-sensors.ts). @dnd-kit never reads event.isTrusted: a drag starts from a mouse press that travels more than 8px, from a finger that rests 200ms without moving more than 6px, and from Space or Enter.
| Event Source | D&D Works |
|---|---|
| Real user interaction | ✅ Works |
Playwright CDP helper (e2e/helpers/cdp-drag.ts) |
✅ Works |
Playwright page.mouse (down, move in steps, up) |
✅ Works |
A finger through Input.dispatchTouchEvent |
✅ Works |
| Mouse events that a script dispatches | ✅ Lift the card |
| Pointer events that a script dispatches | ❌ Ignored: nothing listens |
Claude Chrome MCP left_click_drag |
❌ Recorded as not dragging |
E2E Testing: A drag and drop uses the Playwright CDP helper (e2e/helpers/cdp-drag.ts), which moves in steps and pauses before it releases. page.mouse sends its events through the same protocol, so a press that it moves in steps is a drag too (what-starts-a-drag.spec.ts). A finger that rests is sent with Input.dispatchTouchEvent: tap() lifts at once. @dnd-kit starts to listen for the keys of a keyboard drag in a timeout of its own, so a test lets one timeout of the page pass between the key that lifts and the next key
A card and a column header are picked up by a press or a key on themselves: a mouse that moves more than 8px while it is down, a finger that rests 200ms, or Space or Enter. On the card itself Enter opens the note: of a card's two stops of the Tab key, Enter picks it up from the outer one only. Two things take presses and keys of their own and never pick them up:
- A menu or a dialog of the card or the column. It is drawn elsewhere on the page, and React hands its events up to the element that rendered it all the same.
- A button, a link or a field inside the card or the header, such as "Open note" and "Column options". With @dnd-kit's own sensors a mouse that slipped more than 8px on "Open note", a finger that rested 200ms on it and Enter on it picked the card up, and the note did not open. The open comment editor counts as a whole (
data-no-drag): its counter and the space around its field are neither a field nor a button.
The three sensors of the board ask mayStartDrag (src/lib/utils/may-start-drag.ts) before they start. A comment is no button in this sense, although a click opens its editor: it is most of a card's surface, and a card is picked up there as anywhere else.
Escape cancels a drag, and so do a window that is resized or hidden during the drag and a touch that the browser takes over. Nothing is saved, and the board leaves the state of the drag: the drop zones of a column drag (new row, empty places) go away. @dnd-kit reports a cancelled drag to onDragCancel and not to onDragEnd, so the board handles both (handleDragCancel of useKanbanDnD).
┌──────────────┐ ┌──────────────┐
│ In Progress │ │ In Progress │
│┌────────────┐│ drag │┌────────────┐│
││ Card A ││ ───┐ ││ Card B ││
│├────────────┤│ │ │├────────────┤│
││ Card B ││ <──┘ ││ Card A ││ ← Swapped
│├────────────┤│ │├────────────┤│
││ Card C ││ ││ Card C ││
│└────────────┘│ │└────────────┘│
└──────────────┘ └──────────────┘
| Item | Value |
|---|---|
| Status | ✅ Implemented |
| E2E Coverage | ✅ Covered (should reorder cards within same column) |
| Server Action | batchUpdateRepoCardOrders() |
| CDP Helper | cdpCardDragAndDrop() |
┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ In Progress │ │ Review │ │ In Progress │ │ Review │
│┌────────────┐│ │ │ drag │ │ │┌────────────┐│
││ Card A ││ ─┼──────────────┼───► │ │ ││ Card A ││
│└────────────┘│ │ │ │ │ │└────────────┘│
└──────────────┘ └──────────────┘ └──────────────┘ └──────────────┘
| Item | Value |
|---|---|
| Status | ✅ Implemented |
| E2E Coverage | ✅ Covered (should move card to different column) |
| Server Action | updateRepoCardPosition() |
| CDP Helper | cdpCardToColumnDragAndDrop() |
Row 0: [ A ] [ B ] [ C ] → Row 0: [ B ] [ A ] [ C ]
↓___↑ (A and B swapped)
drag A → B
| Item | Value |
|---|---|
| Status | ✅ Implemented |
| E2E Coverage | ✅ Covered (grid position verification, drag exec) |
| Server Action | swapStatusListPositions() |
| CDP Helper | cdpColumnDragAndDrop() |
| Note | DropZone is sensitive - must exceed target center |
Row 0: [ A ] [ B ] [ C ] [ D ]
↓ drag down
┌─────────────────────────────┐
│ Drop column to create row │ ← NewRowDropZone
└─────────────────────────────┘
↓
Row 0: [ A ] [ B ] [ D ]
Row 1: [ C ] ← New row created
| Item | Value |
|---|---|
| Status | ✅ Implemented |
| E2E Coverage | ✅ Covered |
| Server Action | updateStatusListPosition() |
| CDP Helper | cdpColumnToNewRowDragAndDrop() |
Row 0: [ A ] [ ] [ B ] [ C ]
↑
drag A → empty slot
↓
Row 0: [ ] [ A ] [ B ] [ C ] (Subsequent columns shift right)
| Item | Value |
|---|---|
| Status | ✅ Implemented |
| E2E Coverage | ✅ Covered |
| Server Action | batchUpdateStatusListPositions() |
| CDP Helper | cdpColumnToInsertZone() |
Achieved via ColumnInsertZone or Column Swap
Row 0: [ A ] [ ] [ ] Row 0: [ B ] [ ] [ ]
Row 1: [ B ] [ ] [ ] → Row 1: [ A ] [ ] [ ]
↓___↑ (A and B swap rows)
drag A → B
Precondition: Multi-row configuration after moving column to Row 1+ via NewRowDropZone
| Item | Value |
|---|---|
| Status | ✅ Implemented |
| E2E Coverage | ✅ Covered |
| Server Action | swapStatusListPositions() |
| CDP Helper | cdpColumnDragAndDrop() |
| Note | Row position exchange between same gridCol columns |
Row 0: [ A ] [ ] [ ] Row 0: [ ] [ ] [ B ]
Row 1: [ ] [ ] [ B ] → Row 1: [ A ] [ ] [ ]
↘_________↗ (A and B swap diagonally)
drag A → B
| Item | Value |
|---|---|
| Status | ✅ Implemented |
| E2E Coverage | ✅ Covered |
| Server Action | swapStatusListPositions() |
| CDP Helper | cdpColumnDragAndDrop() |
| Note | Both gridRow and gridCol fully exchanged |
┌───────────────┐ ┌───────────────┐ ┌───────────────┐ ┌───────────────┐
│ Board A [≡] │ │ Board B [≡] │ drag │ Board B [≡] │ │ Board A [≡] │
└───────────────┘ └───────────────┘ → └───────────────┘ └───────────────┘
↓_______________↑ (A and B swapped, position persisted)
| Item | Value |
|---|---|
| Status | ✅ Implemented |
| E2E Coverage | ✅ Covered (board-dnd.spec.ts - 4 tests) |
| Server Action | updateBoardPositions() |
| CDP Helper | cdpBoardDragAndDrop() |
| Sensors | MouseSensor (distance: 8), TouchSensor (delay: 200), Keyboard |
| Collision | closestCenter + restrictToWindowEdges |
| State | useState + useOptimistic dual pattern with error revert |
| Note | Disabled on /boards/favorites (filtered view) |
| Item | Value |
|---|---|
| Status | ✅ Implemented (2025-12-28) |
| E2E Coverage | ✅ Covered (e2e/kanban.spec.ts - Column Auto-Height tests) |
| Files | KanbanBoard.tsx, SortableColumn.tsx, StatusColumn.tsx, cdp-drag.ts |
| Method | CSS Grid minmax(min-content, auto) + height constraint removal |
| Test Name | Status |
|---|---|
| Card: Column display | ✅ Added (should display cards in columns) |
| Card: Cross-column move | ✅ Added (should move card to different column) |
| Card: Same column reorder | ✅ Added (should reorder cards within same column) |
| Card: statusId update | ✅ Verified in above tests |
| Column: Grid position verify | ✅ Added (should have correct initial column grid positions) |
| Column: Swap operation | ✅ Added (should execute column drag operation successfully) |
| Column: Vertical Swap | ✅ Added (11.3.5 - 2 tests in column-dnd.spec.ts) |
| Column: Diagonal Swap | ✅ Added (11.3.6 - 3 tests in column-dnd.spec.ts) |
| Column: NewRowDropZone | ✅ Added (6 test cases in column-dnd.spec.ts) |
| Column: ColumnInsertZone | ✅ Added (5 test cases in column-dnd.spec.ts) |
| Column: Auto-Height | ✅ Added (e2e/kanban.spec.ts - 6 tests) |
| Column: Width constraint | ✅ Added (e2e/kanban-column-width.spec.ts - 4 tests) |
| Board: D&D reorder | ✅ Added (board-dnd.spec.ts - 4 tests) |
| Board: DB persistence | ✅ Added (position verified after reload) |
// e2e/helpers/cdp-drag.ts
export async function cdpColumnDragAndDrop(page, sourceId, targetId, options?)
export async function cdpCardDragAndDrop(page, sourceId, targetId, options?)
export async function cdpCardToColumnDragAndDrop(
page,
cardId,
columnId,
options?,
)
export async function cdpColumnToNewRowDragAndDrop(
page,
columnId,
row,
options?,
)
export async function cdpColumnToInsertZone(page, columnId, row, col, options?)
export async function cdpBoardDragAndDrop(
page,
sourceBoardId,
targetBoardId,
options?,
)- Runner: Vitest 5 + happy-dom
- Coverage: @vitest/coverage-v8
- React Testing: @testing-library/react + @testing-library/user-event
- Runner: Playwright 1.63
- Database: Real local Supabase (not mocked)
- GitHub API: MSW mocking (stability for external API)
- Auth: Pre-authenticated via
e2e/auth.setup.ts - D&D: CDP-based helpers (
e2e/helpers/cdp-drag.ts) - DB Reset:
pnpm db:resetviae2e/global-setup.tsbefore tests - Workers: 1 (for database state consistency)
- Timeout: 30 seconds (test + expect)
- Flaky Tests:
failOnFlakyTests: true(strict enforcement, 2 retries) - No
waitForTimeout: All waits use assertion-based patterns (expect().toPass(),waitFor)
- Version: Storybook 10 (@storybook/nextjs-vite)
- Addons: a11y, docs, vitest, chromatic
- MSW: msw-storybook-addon for API mocking
| Category | File | Description |
|---|---|---|
| Kanban D&D | kanban-dnd/card-dnd.spec.ts |
Card drag & drop |
| Kanban D&D | kanban-dnd/column-dnd.spec.ts |
Column drag & drop |
| Kanban D&D | what-starts-a-drag.spec.ts |
Menus and buttons do not start a drag |
| Kanban | kanban-basic.spec.ts |
Basic kanban operations |
| Kanban | kanban-scroll.spec.ts |
Scroll behavior |
| Kanban | kanban-auto-height.spec.ts |
Column auto-height |
| Kanban | kanban-column-width.spec.ts |
Column width constraint |
| Kanban | kanban-column-edit.spec.ts |
Column edit operations |
| Board | board-dnd.spec.ts |
Board card D&D reorder |
| Board | board-settings.spec.ts |
Board settings dialog |
| Board | board-settings-card-display.spec.ts |
Card display settings |
| Board | board-settings-subtitle.spec.ts |
Subtitle edit & visibility |
| Board | board-settings-narrow-viewport.spec.ts |
Board settings on a 375px phone |
| Board | board-header-inline-edit.spec.ts |
Inline editable board header |
| Board | boards-page-header-inline-edit.spec.ts |
Boards page header edit |
| Board | create-board.spec.ts |
Board creation |
| Board | boards.spec.ts |
Board list |
| Board | favorites.spec.ts |
Favorites feature |
| Account | account-deletion.spec.ts |
Account deletion flow |
| Repo | add-repository-combobox.spec.ts |
Add repository combobox |
| Repo | add-repository-pagination.spec.ts |
Repository pagination |
| Repo | repo-card-display.spec.ts |
Card display |
| Repo | repo-card-description.spec.ts |
Card description |
| Repo | remove-from-board.spec.ts |
Remove card from board |
| Repo | one-repo-across-boards.spec.ts |
One repo per user (#215) |
| Repo | repository-memory.spec.ts |
Note, links, comment kept (#216) |
| Repo | move-to-another-board.spec.ts |
Move card between boards |
| Comment | comment-display.spec.ts |
Comment display |
| Comment | comment-inline-edit.spec.ts |
Comment inline editing |
| Comment | comment-color-theme-independence.spec.ts |
Color theme independence |
| Comment | comment-actions-menu.spec.ts |
Comment menu (pointer, keys) |
| Note | note-modal.spec.ts |
Note modal (rich text) |
| Links | project-info-links.spec.ts |
Project info links |
| Settings | settings.spec.ts |
Settings page |
| Theme | sidebar-theme-toggle.spec.ts |
Theme toggle |
| Theme | theme-persistence.spec.ts |
Theme persistence |
| Theme | theme-visual-application.spec.ts |
Theme visual application |
| Sidebar | sidebar-collapse.spec.ts |
Sidebar collapse |
| Sidebar | sidebar-narrow-viewport.spec.ts |
Sidebar below 768px |
| Layout | names-that-cannot-break.spec.ts |
Long names and ordinary words |
| SSR | ssr-hydration-board.spec.ts |
SSR hydration (board) |
| Page | page-titles.spec.ts |
Page titles |
| Unauth | landing.spec.ts |
Landing page |
| Unauth | login.spec.ts |
Login page |
| Unauth | page-titles.spec.ts |
Unauthenticated page titles |
| Unauth | board-data-access.spec.ts |
Board database access |
| Unauth | ssr-hydration.spec.ts |
SSR hydration (unauth) |
| Security | xss-smoke.spec.ts |
XSS injection prevention |
| Coverage | shipped-todo-coverage.spec.ts |
Playwright for shipped TODO fixes |
- Format:
YYYY.M.DD(no zero-padding, novprefix) - Same-day suffix:
2026.2.16,2026.2.16.1,2026.2.16.2 - Trigger: Every push to
main(= every PR merge) - Release notes: Auto-generated from PR titles (
gh release create --generate-notes) - The tag is not the app version: CalVer is tag-only (web app, not distributed). The app version lives in
VERSIONandCHANGELOG.md(four parts, e.g.0.4.2.0) and inpackage.json(its first three parts) - Workflow:
.github/workflows/release.yml(pureghCLI, zero dependencies)
next.config.tsthrowsFATALerror if test env vars are present in Vercel buildsAPP_ENV=test→ blocks (would bypass all authentication)NEXT_PUBLIC_ENABLE_MSW_MOCK=true→ blocks (would enable mock data)
- Only guard
VERCELenv (E2E CI intentionally usesAPP_ENV=test+next build)