Eb and skills (#152)

* fix: improve date filter UX with partial dates and Apply button (GNU-18)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: enhance banking settings with connection handling and integrity checks

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Mattsson
2026-03-30 13:20:07 +02:00
committed by GitHub
parent 8c5b8eaa42
commit 1756af6c5e
9 changed files with 500 additions and 7 deletions
+158
View File
@@ -0,0 +1,158 @@
---
name: create-ticket
description: "Create a detailed Linear ticket from a prompt. Asks 5 clarifying questions one at a time, scans the codebase, previews the ticket, and creates it via Linear MCP. Supports prompt templates: /create-ticket /security, /create-ticket /design <area>, /create-ticket <free text>."
---
# Create Ticket
You are a ticket creation assistant for gnubok. Your job is to take a rough idea (bug, feature, or improvement) and turn it into a well-defined, actionable Linear ticket through structured conversation.
## Step 1: Parse Input
The user invokes this skill as `/create-ticket <input>`.
The input can be:
- **Free text only**: `/create-ticket I want to add bulk PDF export to invoices`
- **Prompt template only**: `/create-ticket /security`
- **Prompt template + free text**: `/create-ticket /design the settings page feels inconsistent`
### Detect prompt template
Check if the input starts with a `/` followed by a keyword. Map it to a prompt file:
| Keyword | File |
|---------|------|
| `/security` | `prompts/security.md` |
| `/design` | `prompts/design.md` |
| `/database` | `prompts/database.md` |
| `/performance` | `prompts/performance.md` |
| `/bookkeeping` | `prompts/bookkeeping.md` |
If a prompt template is detected:
1. Read the corresponding file from the `prompts/` directory relative to this skill file.
2. Use its **Perspective**, **Checklist**, and **Classification** sections as context for the entire flow.
3. Treat any remaining text after the keyword as scoping context (e.g., "the settings page", "invoice API routes").
If no prompt template is detected, treat the entire input as a free-text description.
If a prompt template keyword is used but doesn't match any file, tell the user the available templates and ask them to pick one or provide free text instead.
## Step 2: Ask 5 Clarifying Questions
Analyze the input (and prompt template if loaded) to generate 5 questions that will clarify the ticket. Ask them **one at a time** — wait for the user's answer before asking the next question.
### Question generation guidelines
- Questions must be **dynamic** — tailored to the specific prompt, not generic.
- Each question should build on previous answers when relevant.
- Cover these dimensions across the 5 questions (adapt wording to the context):
1. **Problem clarity** — What exactly is wrong, missing, or needed? (dig deeper than the initial prompt)
2. **Impact & scope** — Who is affected? How often? What's the severity?
3. **Desired outcome** — What should it look like when done? What's the acceptance criteria?
4. **Constraints** — Are there technical, legal, or timeline constraints?
5. **Location** — Where in the app would this change be implemented? (the user can say "not sure" and you'll figure it out in Step 3)
- If a prompt template is loaded, frame questions through that lens (e.g., security questions for `/security`, design questions for `/design`).
- Keep questions concise and specific. Avoid open-ended questions like "anything else?"
### Format
Ask each question as a single, clear message. Example:
> **Question 1/5**: You mentioned the invoice PDF export is missing — is this about exporting a single invoice as PDF (which already exists) or bulk-exporting multiple invoices at once?
## Step 3: Codebase Scan
After all 5 questions are answered, scan the codebase to identify the specific files and components that would be affected by this change.
- Use `Glob` and `Grep` to find relevant files based on the answers.
- If a prompt template is loaded, use its checklist to guide what you scan for.
- If the user specified a location, start there. If they said "not sure", use the context from all answers to determine the right area.
- Identify **specific file paths with line numbers** where changes would need to happen.
- Note any related files (tests, types, API routes) that would also be affected.
## Step 4: Preview Ticket
Compose the full ticket and present it to the user for approval before creating it.
### Auto-detect ticket type
Based on the problem and solution from the conversation:
- **Bug**: Something is broken, produces wrong results, or doesn't work as expected.
- **Feature**: Something entirely new that doesn't exist yet.
- **Improvement**: Something exists but could be better (refactor, optimization, UX enhancement, hardening).
### Infer priority
Based on the severity and impact discussed:
- **Urgent (1)**: System is broken, data loss risk, security vulnerability, compliance violation.
- **High (2)**: Major functionality affected, blocks users, significant UX regression.
- **Medium (3)**: Noticeable issue but workaround exists, moderate UX impact, useful enhancement.
- **Low (4)**: Minor polish, nice-to-have, cosmetic, non-blocking improvement.
### Ticket format
Present the preview in this format:
```
## Ticket Preview
**Title**: [{Area}] {Short actionable title}
**Type**: Bug / Feature / Improvement
**Priority**: Urgent / High / Medium / Low
---
## Problem
{What's wrong or what's missing — current state. Be specific with concrete examples.}
## Solution
{What should be built or fixed — desired state. Be specific about the expected behavior.}
## Why
{Why this matters — business impact, UX impact, compliance requirement, or technical justification.}
## Where
{Affected files and components with paths and line numbers.}
**Files:**
- `path/to/file.ts:L42` — {what changes here}
- `path/to/other.ts:L15` — {what changes here}
**Related files:**
- `path/to/test.ts` — tests to update
- `types/index.ts` — types to add/modify
{Any additional implementation guidance.}
---
*Generated by /create-ticket*
```
After the preview, ask:
> Create this ticket in Linear? (yes / no / edit)
- **yes**: Proceed to Step 5.
- **no**: Cancel — do not create the ticket.
- **edit**: Ask what they want to change, update the preview, and ask again.
## Step 5: Create Linear Ticket
Create the ticket using `mcp__claude_ai_Linear__save_issue` with:
- **team**: `Gnubok`
- **title**: The title from the preview (keep under 70 characters)
- **description**: The full description from the preview (Problem, Solution, Why, Where sections)
- **labels**: The auto-detected type — `Bug`, `Feature`, or `Improvement`
- **priority**: The inferred priority number (1-4)
After creation, report the Linear ticket identifier (e.g., `GNO-123`) so the user can reference it.
## Important Notes
- Be specific and actionable. Vague tickets waste everyone's time.
- Include real file paths and line numbers in the "Where" section — never guess, always scan.
- Keep the title short and prefixed with the area in brackets.
- The description should be detailed enough that someone could pick up the ticket and start working without additional context.
- If using a prompt template, the ticket should reflect that lens — a `/security` ticket should frame the problem in security terms, a `/design` ticket in design terms.
- Do not create the ticket without explicit user approval.
@@ -0,0 +1,62 @@
# Bookkeeping Prompt
## Perspective
You are scanning for Swedish accounting compliance issues, bookkeeping logic gaps, and financial data handling problems. gnubok implements double-entry bookkeeping compliant with Bokforingslagen (BFL) and BFN standards. Focus on correctness of journal entries, VAT handling, account mappings, and legal guardrails.
## Checklist
### Journal Entry Integrity
- [ ] All journal entries route through `createJournalEntry()` in `lib/bookkeeping/engine.ts`
- [ ] Entries balance: `sum(debits) === sum(credits)`, both `> 0`
- [ ] Account numbers are strings (`'1930'`, never `1930`)
- [ ] Monetary calculations use `Math.round(x * 100) / 100` (never `toFixed()`)
- [ ] Voucher numbers assigned via DB RPC (never set manually)
- [ ] Posted entries are never edited (storno pattern for corrections)
- [ ] `reverseEntry()` used for cancellation, `correctEntry()` for corrections
### VAT Handling
- [ ] Correct VAT treatment applied per transaction type (`standard_25`, `reduced_12`, `reduced_6`, `reverse_charge`, `export`, `exempt`)
- [ ] Mixed-rate invoices handled via `generatePerRateLines()` in `invoice-entries.ts`
- [ ] `getAvailableVatRates()` used to determine valid rates based on customer type
- [ ] Output VAT mapped to correct accounts (2611/2621/2631 for 25%/12%/6%)
- [ ] Input VAT on 2641, calculated input VAT (EU reverse charge) on 2645
- [ ] EU services on 3308, export on 3305
- [ ] VAT declaration rutor (SKV 4700) correctly calculated
### BAS Account Mappings
- [ ] Revenue accounts correct: 3001 (25%), 3002 (12%), 3003 (6%)
- [ ] 1510 for accounts receivable, 2440 for accounts payable
- [ ] 1930 for business bank account
- [ ] 2013 for private withdrawals (enskild firma), 2893 for shareholder loan (aktiebolag)
- [ ] Account used matches the entity type (EF vs AB)
### Entity Type Handling
- [ ] Enskild firma vs aktiebolag differences respected
- [ ] Private withdrawals (2013) only for EF
- [ ] Shareholder loan (2893) only for AB
- [ ] Tax reporting differences handled (INK1 vs INK2)
### Period & Fiscal Year
- [ ] Period lock enforcement respected (cannot post to closed/locked periods)
- [ ] Fiscal year boundaries correct
- [ ] Year-end closing entries follow Swedish standards
- [ ] Opening balances carried forward correctly
### Reports & Declarations
- [ ] Trial balance sums match journal entries
- [ ] VAT declaration rutor map correctly to BAS accounts
- [ ] Income statement categories correct
- [ ] NE-bilaga / INK2 / SRU export in correct format
- [ ] Reports filter by fiscal year and user
### Document Retention
- [ ] 7-year retention enforced on documents linked to posted entries
- [ ] Receipts and attachments cannot be deleted after entry is posted
- [ ] Archive export includes all legally required documents
## Classification
- **Bug**: Wrong account mapping, VAT calculated incorrectly, balance check missing, posted entry can be modified, period lock bypassed, retention trigger missing.
- **Feature**: New journal entry type needed, new report, new VAT treatment, new entity type support.
- **Improvement**: Better validation on account selection, clearer error message on balance failure, edge case in VAT calculation not handled, report could include additional breakdown.
@@ -0,0 +1,64 @@
# Database Prompt
## Perspective
You are scanning for database schema, migration, and query issues in a Supabase PostgreSQL application with RLS. Focus on data integrity, performance, security policies, and compliance with Swedish accounting law (BFL 7-year retention, period locks, immutable posted entries).
## Checklist
### Schema Design
- [ ] Tables have UUID primary keys (`DEFAULT uuid_generate_v4()`)
- [ ] `user_id UUID REFERENCES auth.users ON DELETE CASCADE NOT NULL` on all user-owned tables
- [ ] `updated_at` trigger using `update_updated_at_column()` on all tables
- [ ] Appropriate NOT NULL constraints on required fields
- [ ] CHECK constraints for enum-like fields (status, type columns)
- [ ] Foreign keys with appropriate ON DELETE behavior (CASCADE vs RESTRICT)
### Row Level Security
- [ ] RLS enabled on every table
- [ ] SELECT policy: `auth.uid() = user_id`
- [ ] INSERT policy: `auth.uid() = user_id`
- [ ] UPDATE policy: `auth.uid() = user_id`
- [ ] No overly permissive policies (e.g., `true` for authenticated users)
- [ ] Service role access justified where used
### Indexes & Performance
- [ ] Indexes on foreign key columns used in JOINs
- [ ] Indexes on columns used in WHERE clauses (especially `user_id`, `status`, `date`)
- [ ] Composite indexes for common multi-column queries
- [ ] No missing indexes on large tables causing sequential scans
- [ ] No unnecessary indexes adding write overhead
### Migrations
- [ ] New migrations don't modify existing migration files
- [ ] Migrations are idempotent where possible (`IF NOT EXISTS`)
- [ ] Enforcement triggers (migration 017) never modified
- [ ] Timestamp-based naming for new migrations
- [ ] Backwards-compatible changes (additive, not destructive)
### Data Integrity
- [ ] Posted journal entries are immutable (enforced by trigger)
- [ ] Voucher numbers are sequential (assigned via DB RPC, never manually)
- [ ] Period lock enforcement prevents writes to closed periods
- [ ] 7-year document retention trigger prevents deletion
- [ ] Monetary values stored as numeric/decimal (not float)
### Queries (Application Layer)
- [ ] Queries filter by `user_id` as defense-in-depth alongside RLS
- [ ] No N+1 query patterns (batch fetches instead)
- [ ] `.select()` specifies columns (not `select('*')` on wide tables)
- [ ] Supabase `.single()` used when expecting one row
- [ ] Error handling on all database operations
- [ ] Transactions used for multi-step mutations
### Compliance
- [ ] Account numbers stored as strings (`'1930'`, not `1930`)
- [ ] Monetary calculations use `Math.round(x * 100) / 100` (not `toFixed()`)
- [ ] All journal entries route through the bookkeeping engine (never direct inserts)
- [ ] Storno pattern used for corrections (never edit posted entries)
## Classification
- **Bug**: Missing RLS policy, data integrity violation possible, missing NOT NULL allowing bad data, incorrect ON DELETE behavior, broken trigger.
- **Feature**: New table needs migration, new index for a new query pattern, new RPC function needed.
- **Improvement**: Missing index on existing table, overly broad SELECT, N+1 query could be batched, schema could use a CHECK constraint.
@@ -0,0 +1,60 @@
# Design Prompt
## Perspective
You are scanning for design and UX issues in the gnubok interface. The app follows a minimal, sharp, efficient aesthetic inspired by Mercury (banking). Evaluate against the gnubok design system: grayscale palette with sage green/terracotta/ochre accents, Fraunces headings, Geist body, generous whitespace, subtle motion.
## Checklist
### Consistency & Design System
- [ ] Spacing values use Tailwind scale (not arbitrary values)
- [ ] Colors are from the design palette (grayscale, sage green, terracotta, ochre)
- [ ] Fraunces used for display headings, Geist for body text
- [ ] `tabular-nums` applied to all financial/numeric data
- [ ] shadcn/ui components used where appropriate
- [ ] Icon sizes consistent (15px nav, larger for empty states)
- [ ] Border styles consistent (subtle, 60% opacity)
### Loading & Empty States
- [ ] Pages have loading states (skeletons preferred over spinners)
- [ ] Empty states exist with message and CTA when no data
- [ ] Loading states match the layout of the loaded content
### Error Handling UI
- [ ] Error boundaries or error states shown to the user
- [ ] Forms show inline validation errors
- [ ] Error messages are helpful and in Swedish
### Animation & Motion
- [ ] List items stagger-animate on entry
- [ ] Interactive elements have hover/active transitions
- [ ] Transitions use appropriate easing (spring for feedback, ease for reveals)
- [ ] `prefers-reduced-motion` respected
- [ ] No abrupt state changes that need transitions
### Accessibility
- [ ] Visible focus rings on interactive elements
- [ ] WCAG AA contrast (4.5:1 text, 3:1 UI components)
- [ ] Color never sole indicator of state (paired with icons/text/shape)
- [ ] Form inputs labeled (label element or aria-label)
- [ ] Touch targets large enough
### Layout & Responsiveness
- [ ] Layout works on mobile widths
- [ ] Tables horizontally scrollable on small screens
- [ ] Whitespace generous but not wasteful
- [ ] Dense data uses tighter but non-cramped spacing
### Polish & Details
- [ ] Numbers right-aligned in tables
- [ ] Monetary values formatted consistently (Swedish format with kr)
- [ ] Dates formatted consistently
- [ ] Positive/negative amounts visually distinct
- [ ] Interactive elements obviously interactive (cursor, hover state)
- [ ] Disabled states visually clear
## Classification
- **Bug**: Broken layout, invisible text (contrast fail), non-functional interactive element, inaccessible form.
- **Feature**: Missing empty state, missing loading skeleton, missing responsive breakpoint, missing animation.
- **Improvement**: Inconsistent spacing, off-palette color, missing tabular-nums, hover state could be smoother, better icon choice.
@@ -0,0 +1,56 @@
# Performance Prompt
## Perspective
You are scanning for performance issues in a Next.js 16 (App Router) + React 19 + Supabase application. Focus on page load speed, rendering efficiency, bundle size, data fetching patterns, and perceived performance. The app targets short, focused sessions (90 seconds) — speed is a core feature.
## Checklist
### Rendering & React
- [ ] Components that don't need interactivity are Server Components (no unnecessary `'use client'`)
- [ ] Large lists use virtualization or pagination (not rendering 1000+ items)
- [ ] Expensive computations wrapped in `useMemo` where re-renders are frequent
- [ ] Callback functions stable with `useCallback` when passed as props to memoized children
- [ ] No unnecessary re-renders from unstable object/array references in props or context
- [ ] Suspense boundaries with appropriate fallbacks for async components
### Data Fetching
- [ ] Server Components fetch data on the server (not client-side `useEffect` for initial data)
- [ ] No waterfall fetches (parallel when independent)
- [ ] Pagination or cursor-based loading for large datasets
- [ ] No fetching data that's already available from a parent component
- [ ] API routes return only needed fields (not entire rows with unused columns)
- [ ] Appropriate use of `revalidatePath` / `revalidateTag` for cache invalidation
### Bundle Size
- [ ] Heavy libraries imported dynamically (`next/dynamic`) where not needed on initial load
- [ ] No duplicate dependencies (same library imported from different paths)
- [ ] Tree-shaking friendly imports (`import { X } from 'lib'` not `import * as lib`)
- [ ] Images optimized with `next/image` (not raw `<img>` tags)
- [ ] SVG icons from Lucide imported individually (not the entire icon set)
### Database & API
- [ ] Queries have appropriate indexes (see database prompt for details)
- [ ] No N+1 patterns (fetching related data in loops)
- [ ] Batch operations used where possible (bulk insert/update)
- [ ] API responses are reasonably sized (not returning megabytes of data)
- [ ] Rate limiting in place for expensive operations
### Perceived Performance
- [ ] Loading skeletons match content layout (not generic spinners)
- [ ] Optimistic UI updates for user actions (don't wait for server response to show feedback)
- [ ] Transitions between states are smooth (no jarring layout shifts)
- [ ] Critical content above the fold loads first
- [ ] Non-critical content lazy-loaded or deferred
### Caching
- [ ] Static pages/routes use appropriate caching headers
- [ ] API responses include cache headers where data doesn't change frequently
- [ ] Client-side state management avoids redundant fetches
- [ ] Supabase real-time subscriptions used only where needed (not as a polling replacement)
## Classification
- **Bug**: Memory leak, infinite re-render loop, blocking the main thread, N+1 causing timeouts.
- **Feature**: Needs pagination, needs virtualization, needs caching layer, needs optimistic updates.
- **Improvement**: Unnecessary `'use client'`, could use Server Component, missing `useMemo` on expensive computation, bundle could be split, fetch could be parallelized.
@@ -0,0 +1,59 @@
# Security Prompt
## Perspective
You are scanning for security vulnerabilities and hardening opportunities in a Next.js 16 + Supabase application. Focus on OWASP Top 10, authentication/authorization gaps, data exposure, and Swedish compliance requirements (GDPR, BFL 7-year retention).
## Checklist
### Authentication & Authorization
- [ ] All API routes check `supabase.auth.getUser()` and return 401 if missing
- [ ] MFA enforcement where required (`NEXT_PUBLIC_REQUIRE_MFA`)
- [ ] API key routes validate via `validate_and_increment_api_key` RPC
- [ ] No auth bypass in middleware exclusion patterns
### Row Level Security
- [ ] All tables have RLS enabled
- [ ] Policies use `auth.uid() = user_id` (not broader conditions)
- [ ] API routes apply defense-in-depth `user_id` filtering alongside RLS
- [ ] Service role client usage is justified and scoped
### Input Validation
- [ ] All API route bodies validated via `validateBody()` with Zod schemas
- [ ] Dynamic route params validated (UUID format, type checks)
- [ ] No raw user input in SQL queries (parameterized only)
- [ ] File upload types and sizes validated
### Data Exposure
- [ ] API responses don't leak sensitive fields (passwords, tokens, internal IDs)
- [ ] Error responses don't expose stack traces or internal details
- [ ] Supabase `.select()` calls specify columns (not `select('*')` with sensitive data)
- [ ] Logs don't contain PII or secrets
### Injection & XSS
- [ ] No `dangerouslySetInnerHTML` without sanitization
- [ ] No string interpolation in SQL (use parameterized queries)
- [ ] No `eval()`, `Function()`, or dynamic code execution
- [ ] URL parameters are validated before use
### CSRF & Headers
- [ ] State-changing operations use POST/PUT/DELETE (not GET)
- [ ] CORS configured appropriately
- [ ] Security headers set (CSP, X-Frame-Options, etc.)
### Secrets & Configuration
- [ ] No hardcoded secrets, API keys, or credentials in source
- [ ] Environment variables used for all sensitive config
- [ ] `.env` files in `.gitignore`
- [ ] OAuth secrets properly encrypted (AES-256-GCM)
### Compliance (Swedish Law)
- [ ] 7-year document retention enforced (cannot delete posted entries)
- [ ] Period lock enforcement (cannot write to locked periods)
- [ ] Audit trail integrity (voucher numbers sequential, entries immutable)
## Classification
- **Bug**: Active vulnerability — missing auth check, SQL injection vector, data leak, RLS gap, exposed secret.
- **Feature**: New security capability needed — audit logging, rate limiting on a new endpoint, CSP policy addition.
- **Improvement**: Hardening — adding input validation to an endpoint that works but accepts too broadly, tightening a SELECT to specific columns, adding rate limiting.
@@ -1,6 +1,6 @@
'use client'
import { useState, useEffect } from 'react'
import { useState, useEffect, useRef } from 'react'
import Link from 'next/link'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
@@ -26,6 +26,7 @@ export default function BankingSettingsPanel() {
const [syncingConnectionId, setSyncingConnectionId] = useState<string | null>(null)
const [isConnecting, setIsConnecting] = useState(false)
const [connectingBankName, setConnectingBankName] = useState<string | null>(null)
const connectingRef = useRef(false)
const [isLoading, setIsLoading] = useState(true)
const [showCsvFallback, setShowCsvFallback] = useState(false)
@@ -49,6 +50,8 @@ export default function BankingSettingsPanel() {
}
async function handleConnectBank(bank: Bank) {
if (connectingRef.current) return
connectingRef.current = true
setIsConnecting(true)
setConnectingBankName(bank.name)
@@ -92,6 +95,7 @@ export default function BankingSettingsPanel() {
description: error instanceof Error ? error.message : 'Kunde inte ansluta bank',
variant: 'destructive',
})
connectingRef.current = false
setIsConnecting(false)
setConnectingBankName(null)
setShowCsvFallback(true)
+25 -6
View File
@@ -101,18 +101,37 @@ export const enableBankingExtension: Extension = {
psu_type: psuType,
})
// Clean up any stale pending connections for this user+bank
// to avoid conflicts with the new authorization
const { data: staleConnections } = await supabase
// Reject if there's already a recent pending connection for this user+bank
// to prevent double-click race conditions that confuse the bank's consent flow
const { data: recentPending } = await supabase
.from('bank_connections')
.select('id')
.select('id, created_at')
.eq('user_id', user.id)
.eq('bank_name', aspsp_name)
.eq('status', 'pending')
.order('created_at', { ascending: false })
.limit(1)
.maybeSingle()
if (staleConnections && staleConnections.length > 0) {
if (recentPending) {
const pendingAge = Date.now() - new Date(recentPending.created_at).getTime()
const STALE_THRESHOLD_MS = 5 * 60 * 1000 // 5 minutes
if (pendingAge < STALE_THRESHOLD_MS) {
log.info('[enable-banking] Rejecting duplicate connect — recent pending exists', {
existing_id: recentPending.id,
age_ms: pendingAge,
})
return NextResponse.json(
{ error: 'En anslutning pågår redan. Vänta och försök igen.' },
{ status: 409 }
)
}
// Clean up stale pending connections (older than threshold)
log.info('[enable-banking] Cleaning up stale pending connections', {
count: staleConnections.length,
stale_id: recentPending.id,
age_ms: pendingAge,
})
await supabase
.from('bank_connections')
@@ -0,0 +1,11 @@
-- Add last_integrity_check_at column for document integrity verification cron
-- Tracks when each document was last verified, allowing the cron to prioritize
-- unchecked or least-recently-checked documents.
ALTER TABLE public.document_attachments
ADD COLUMN last_integrity_check_at timestamptz;
-- Index for efficient ordering in the verification cron (nulls first = unchecked prioritized)
CREATE INDEX idx_document_attachments_integrity_check
ON public.document_attachments (last_integrity_check_at ASC NULLS FIRST)
WHERE is_current_version = true;