Files
thrilltrack-explorer/docs/DATABASE_DIRECT_EDIT.md
gpt-engineer-app[bot] dae687292b Add documentation for hooks
2025-11-02 19:09:45 +00:00

41 KiB

Database Direct Edit System

Overview

A full-featured database management interface for administrators (admin/superuser roles only) that allows direct CRUD operations on all database tables with advanced spreadsheet-like functionality, comprehensive filtering, sorting, and inline editing capabilities.

Status: 📋 Planned (Not Yet Implemented)

Target Users: Administrators and Superusers only

Security Level: Requires AAL2 (MFA verification)


Table of Contents

  1. Architecture & Security
  2. Core Components
  3. Feature Specifications
  4. Database Requirements
  5. Implementation Roadmap
  6. Dependencies
  7. Safety & UX Guidelines

Architecture & Security

Access Control

  • Role Restriction: Only admin and superuser roles can access
  • AAL2 Enforcement: All database operations require MFA verification via useSuperuserGuard()
  • Audit Logging: Every modification logged to admin_audit_log
  • Warning Banner: Display risk disclaimer about direct database access
  • Read-Only Mode: Toggle to prevent accidental edits

Route Structure

/admin/database              # Main database browser (table list)
/admin/database/:tableName   # Spreadsheet editor for specific table

Navigation

  • Add "Database Editor" link to AdminSidebar
  • Icon: Database from lucide-react
  • Position: Below "User Management"
  • Visibility: Superuser only (isSuperuser())

Core Components

File Structure

src/
├── pages/admin/
│   └── AdminDatabase.tsx                    # Main page with routing
│
├── components/admin/database/
│   ├── index.ts                             # Barrel exports
│   ├── DatabaseTableBrowser.tsx             # Table selector & overview
│   ├── DatabaseTableEditor.tsx              # Main spreadsheet editor (TanStack Table)
│   ├── DatabaseTableFilters.tsx             # Advanced filtering UI
│   ├── DatabaseColumnConfig.tsx             # Column visibility/order management
│   ├── DatabaseRowEditor.tsx                # Detailed row editor dialog
│   ├── DatabaseBulkActions.tsx              # Bulk edit/delete operations
│   ├── DatabaseExportImport.tsx             # CSV/JSON export/import
│   ├── DatabaseSchemaViewer.tsx             # Table schema & ERD viewer
│   ├── DatabaseCellEditors.tsx              # Type-specific cell editors
│   └── types.ts                             # TypeScript definitions
│
├── hooks/
│   ├── useTableSchema.ts                    # Fetch table schema from Supabase
│   ├── useTableData.ts                      # Fetch/edit table data with optimistic updates
│   ├── useDatabaseAudit.ts                  # Audit logging utilities
│   └── useDatabaseValidation.ts             # Validation functions
│
└── lib/
    ├── database/
    │   ├── cellEditors.tsx                  # Cell editor component factory
    │   ├── filterFunctions.ts               # Custom filter functions per data type
    │   ├── validationRules.ts               # Validation rules per column type
    │   └── schemaParser.ts                  # Parse Supabase schema to table config
    └── utils/
        ├── csvExport.ts                     # CSV export utilities
        └── jsonImport.ts                    # JSON import/validation

Feature Specifications

Phase 1: Table Browser & Navigation

DatabaseTableBrowser Component

Purpose: Display all database tables with metadata and quick navigation

Features:

  • Table List Display:

    • Grid or list view toggle
    • Show table name, row count, size, last modified
    • Search/filter tables by name
    • Sort by name, row count, or date
  • Table Categorization:

const tableCategories = {
  auth: {
    color: 'red',
    tables: ['profiles', 'user_roles', 'user_preferences', 'user_sessions'],
    icon: 'Shield'
  },
  content: {
    color: 'yellow',
    tables: ['parks', 'rides', 'companies', 'ride_models', 'locations'],
    icon: 'MapPin'
  },
  submissions: {
    color: 'green',
    tables: ['content_submissions', 'submission_items', 'photo_submissions'],
    icon: 'FileText'
  },
  moderation: {
    color: 'blue',
    tables: ['reports', 'admin_audit_log', 'review_reports'],
    icon: 'Flag'
  },
  versioning: {
    color: 'purple',
    tables: ['park_versions', 'ride_versions', 'company_versions'],
    icon: 'History'
  },
  system: {
    color: 'gray',
    tables: ['admin_settings', 'notification_logs', 'rate_limits'],
    icon: 'Settings'
  }
}
  • Quick Stats Cards:

    • Total tables count
    • Total rows across all tables
    • Database size
    • Last modified timestamp
  • Table Actions:

    • Click table to open editor
    • Quick view schema (hover tooltip)
    • Export table data
    • View recent changes (from versions tables)

Data Fetching:

// Use Supabase RPC to get table metadata
const { data: tables } = await supabase.rpc('get_table_metadata')

interface TableMetadata {
  table_name: string;
  row_count: bigint;
  total_size: string;
  last_modified: string;
  category?: string;
}

Phase 2: Spreadsheet-Style Table Editor

DatabaseTableEditor Component

Core Technology: TanStack Table v8 with advanced features

2.1 Data Grid Display

Features:

  • Virtual Scrolling: Handle 10,000+ rows efficiently using @tanstack/react-virtual
  • Sticky Headers: Column headers remain visible on scroll
  • Row Numbers: Display row index in first column
  • Column Resizing: Drag column borders to resize
  • Column Reordering: Drag-drop column headers to reorder
  • Row Selection:
    • Single click to select row
    • Shift+Click for range selection
    • Ctrl+Click for multi-selection
    • Checkbox column for bulk selection
  • Zebra Striping: Alternate row colors for readability
  • Cell Highlighting: Hover effect on cells
  • Responsive Design: Horizontal scroll on smaller screens

Implementation:

const table = useReactTable({
  data: tableData,
  columns: dynamicColumns,
  getCoreRowModel: getCoreRowModel(),
  getFilteredRowModel: getFilteredRowModel(),
  getSortedRowModel: getSortedRowModel(),
  getPaginationRowModel: getPaginationRowModel(),
  enableRowSelection: true,
  enableMultiSort: true,
  enableColumnResizing: true,
  columnResizeMode: 'onChange',
  state: {
    sorting,
    columnFilters,
    columnVisibility,
    rowSelection,
    pagination
  }
})

2.2 Inline Editing

Cell Editor Types (auto-detected from column type):

Data Type Editor Component Features
text, varchar <Input> Text input with validation
integer, bigint, numeric <Input type="number"> Number input with min/max
boolean <Switch> Toggle switch
timestamp, date <DatePicker> Calendar popup with time
uuid <Select> or <Input> FK lookup or manual entry
jsonb, json <Textarea> + JSON validator Syntax highlighting
enum <Select> Dropdown with allowed values
text[] <TagInput> Multi-value chips

Edit Flow:

  1. Click cell → Enter edit mode
  2. Show appropriate editor component
  3. Type/select new value
  4. Press Enter to save OR Escape to cancel
  5. Optimistic update → DB save → Rollback on error
  6. Visual feedback: Yellow (editing) → Green (saved) → Red (error)

Validation:

  • Check NOT NULL constraints
  • Validate data type compatibility
  • Verify foreign key references exist
  • Check max length for strings
  • Validate JSON syntax
  • Custom validation rules per table

Implementation:

const EditableCell = ({ getValue, row, column, table }) => {
  const initialValue = getValue()
  const [value, setValue] = useState(initialValue)
  const [isEditing, setIsEditing] = useState(false)
  
  const handleSave = async () => {
    const meta = table.options.meta
    await meta?.updateData(row.index, column.id, value)
    setIsEditing(false)
  }
  
  return (
    <div onClick={() => setIsEditing(true)}>
      {isEditing ? (
        <Input 
          value={value} 
          onChange={(e) => setValue(e.target.value)}
          onBlur={handleSave}
          onKeyDown={(e) => {
            if (e.key === 'Enter') handleSave()
            if (e.key === 'Escape') setIsEditing(false)
          }}
        />
      ) : (
        <span>{value}</span>
      )}
    </div>
  )
}

2.3 Advanced Filtering

Filter Panel (collapsible left sidebar):

Filter Builder UI:

  • Add filter button
  • Each filter row contains:
    • Column selector dropdown
    • Operator selector (based on column type)
    • Value input (type-appropriate)
    • Remove filter button
  • AND/OR logic toggle between filters
  • Clear all filters button
  • Save filter preset (with name)
  • Load saved preset

Filter Types by Data Type

Text Columns:

  • Contains
  • Equals
  • Starts with
  • Ends with
  • Regex match
  • Is empty / Is not empty

Number Columns:

  • Equals
  • Greater than (>)
  • Less than (<)
  • Greater than or equal (>=)
  • Less than or equal (<=)
  • Between (min, max)
  • Is null / Is not null

Date Columns:

  • Before
  • After
  • Between (start, end)
  • Is null
  • Last N days
  • Next N days

Boolean Columns:

  • Is true
  • Is false
  • Is null

UUID Columns:

  • Equals
  • In list (comma-separated)
  • Is null

JSON Columns:

  • Path exists
  • Path equals value
  • Contains key

Quick Filters Bar (above table):

  • Chips showing active filters
  • Click chip to edit filter
  • X to remove filter
  • "5 filters active" counter

Implementation:

interface ColumnFilter {
  id: string;          // column name
  operator: FilterOperator;
  value: any;
  logicGate?: 'AND' | 'OR';
}

const filterFunctions = {
  text: {
    contains: (row, id, filterValue) => 
      row.getValue(id)?.toLowerCase().includes(filterValue.toLowerCase()),
    equals: (row, id, filterValue) => 
      row.getValue(id) === filterValue,
    startsWith: (row, id, filterValue) => 
      row.getValue(id)?.startsWith(filterValue),
    // ... more
  },
  number: {
    equals: (row, id, filterValue) => row.getValue(id) === filterValue,
    gt: (row, id, filterValue) => row.getValue(id) > filterValue,
    // ... more
  }
}

2.4 Sorting & Pagination

Sorting:

  • Click column header to sort ascending
  • Click again to sort descending
  • Click third time to remove sort
  • Shift+Click to add multi-column sort
  • Sort indicators: ↑ (asc), ↓ (desc)
  • Multi-sort shows order numbers (1, 2, 3)
  • Custom sort functions for complex types (JSON, arrays)

Pagination:

  • Rows per page selector: 25, 50, 100, 500, 1000
  • Page navigation: First, Prev, Next, Last
  • Page input: "Jump to page X"
  • Display: "Showing 1-25 of 1,234 rows"
  • "Load all" button (with warning for tables > 1000 rows)
  • Virtual scrolling for large datasets

Implementation:

const [pagination, setPagination] = useState({
  pageIndex: 0,
  pageSize: 50
})

const table = useReactTable({
  // ...
  getPaginationRowModel: getPaginationRowModel(),
  onPaginationChange: setPagination,
  state: { pagination }
})

// Pagination controls
<div className="flex items-center gap-2">
  <Button onClick={() => table.setPageIndex(0)}>First</Button>
  <Button onClick={() => table.previousPage()}>Prev</Button>
  <Input 
    type="number" 
    value={table.getState().pagination.pageIndex + 1}
    onChange={(e) => table.setPageIndex(Number(e.target.value) - 1)}
  />
  <span>of {table.getPageCount()}</span>
  <Button onClick={() => table.nextPage()}>Next</Button>
  <Button onClick={() => table.setPageIndex(table.getPageCount() - 1)}>Last</Button>
</div>

2.5 Column Management

Column Config Panel (collapsible right sidebar):

Features:

  • Column Visibility:

    • Checkbox list of all columns
    • Toggle individual columns on/off
    • "Show all" / "Hide all" buttons
    • Search column names
  • Column Reordering:

    • Drag-drop list to reorder columns
    • Visual drag handle icon
    • Smooth reorder animation
  • Column Pinning:

    • Pin columns to left (e.g., ID, name)
    • Pin columns to right (e.g., actions)
    • Unpin button
  • Column Presets:

    • Save current column layout with name
    • Load saved layouts
    • Default layouts per table:
      • "Essential" (ID, name, status, dates)
      • "All" (show everything)
      • "Metadata" (created_at, updated_at, created_by)
  • Reset to Default:

    • Restore original column order/visibility

Implementation:

const [columnVisibility, setColumnVisibility] = useState({})
const [columnOrder, setColumnOrder] = useState([])
const [columnPinning, setColumnPinning] = useState({ left: ['id'], right: [] })

const table = useReactTable({
  // ...
  state: {
    columnVisibility,
    columnOrder,
    columnPinning
  },
  onColumnVisibilityChange: setColumnVisibility,
  onColumnOrderChange: setColumnOrder,
  onColumnPinningChange: setColumnPinning
})

2.6 Search & Navigation

Global Search:

  • Search input above table
  • Searches all visible columns
  • Highlight matching text in cells
  • "X matches found" counter
  • Next/Prev navigation between matches
  • Clear search button

Quick Jump:

  • "Jump to row by ID" input
  • Autofocus and scroll to row
  • Highlight target row

Keyboard Navigation:

  • Arrow keys: Navigate cells
  • Tab: Move to next editable cell
  • Enter: Edit current cell / Save edit
  • Escape: Cancel edit
  • Page Up/Down: Scroll page
  • Home/End: Jump to first/last row
  • Ctrl+F: Focus search
  • Ctrl+C: Copy cell value
  • Del: Delete selected rows (with confirmation)

Phase 3: Advanced Features

3.1 Bulk Operations (DatabaseBulkActions)

Features:

  • Row Selection: Checkboxes in first column

  • Selection Actions:

    • Select all on page
    • Select all matching filter
    • Deselect all
    • Invert selection
  • Bulk Edit:

    • Select column to edit
    • Set value for all selected rows
    • Preview changes before applying
    • Apply button with confirmation
  • Bulk Delete:

    • Delete all selected rows
    • Show count: "Delete 23 rows?"
    • Confirmation dialog with row preview
    • Cannot be undone warning
  • Batch Update:

    • Apply formula/function to column
    • Examples:
      • Set status to 'approved' for all pending
      • Increment version number by 1
      • Update timestamps to NOW()
  • Action History:

    • Show last 10 bulk operations
    • Display: operation, row count, timestamp, user
    • Undo last operation (if safe and within 5 minutes)

UI:

<div className="bulk-actions-toolbar">
  <span>{selectedRows.length} rows selected</span>
  <Button onClick={handleBulkEdit}>Edit Selected</Button>
  <Button onClick={handleBulkDelete} variant="destructive">Delete Selected</Button>
  <Button onClick={handleDeselectAll} variant="ghost">Deselect All</Button>
</div>

3.2 Export/Import (DatabaseExportImport)

Export Features:

  • Export Formats:

    • CSV (with headers)
    • JSON (array of objects)
    • SQL INSERT statements
  • Export Scope:

    • All rows
    • Filtered rows only
    • Selected rows only
    • Visible columns only
  • Export Options:

    • Include/exclude NULL values
    • Date format selection
    • JSON pretty print
    • SQL: Include CREATE TABLE statement
  • Batch Export:

    • Export all tables as ZIP file
    • Progress indicator for large exports

Import Features:

  • Import Formats:

    • CSV (with column mapping UI)
    • JSON (validate schema first)
  • Import Modes:

    • Insert (add new rows)
    • Update (match by ID)
    • Upsert (insert or update)
  • Import Validation:

    • Dry-run preview
    • Show errors before import
    • Skip invalid rows option
  • Import Progress:

    • Progress bar for large files
    • Success/error counts
    • Download error log

Implementation:

const exportToCSV = (data: any[], columns: Column[]) => {
  const headers = columns.map(col => col.header).join(',')
  const rows = data.map(row => 
    columns.map(col => JSON.stringify(row[col.id] ?? '')).join(',')
  ).join('\n')
  
  const csv = `${headers}\n${rows}`
  downloadFile(csv, 'table_export.csv', 'text/csv')
}

const importFromCSV = async (file: File) => {
  const parsed = Papa.parse(file, { header: true })
  
  // Validate schema
  const validationErrors = validateImportData(parsed.data, tableSchema)
  if (validationErrors.length > 0) {
    showErrorDialog(validationErrors)
    return
  }
  
  // Insert rows
  for (const row of parsed.data) {
    await supabase.from(tableName).insert(row)
  }
}

3.3 Schema Viewer (DatabaseSchemaViewer)

Features:

  • Table Structure Display:

    • Column list with details:
      • Name
      • Data type
      • Nullable (✓/✗)
      • Default value
      • Max length (for text)
    • Primary key indicator (🔑)
    • Foreign key indicators (→ target_table)
    • Unique constraint indicators
    • Index indicators (📊)
  • Relationships View:

    • Visual diagram (ERD style)
    • Show foreign key relationships
    • Click table to navigate
    • Zoom/pan controls
  • Constraints:

    • Primary keys
    • Foreign keys (with ON DELETE/UPDATE actions)
    • Unique constraints
    • Check constraints
    • Indexes (btree, gin, gist)
  • RLS Policies (read-only):

    • List all policies for table
    • Show policy name, operation, USING clause
    • Syntax highlighting for SQL
  • SQL DDL View:

    • Show CREATE TABLE statement
    • Show CREATE INDEX statements
    • Show RLS policies
    • Copy to clipboard button
    • Syntax highlighting

Implementation:

const SchemaViewer = ({ tableName }: { tableName: string }) => {
  const { data: schema } = useTableSchema(tableName)
  
  return (
    <Tabs>
      <TabsList>
        <TabsTrigger value="columns">Columns</TabsTrigger>
        <TabsTrigger value="relationships">Relationships</TabsTrigger>
        <TabsTrigger value="constraints">Constraints</TabsTrigger>
        <TabsTrigger value="rls">RLS Policies</TabsTrigger>
        <TabsTrigger value="sql">SQL DDL</TabsTrigger>
      </TabsList>
      
      <TabsContent value="columns">
        <Table>
          <TableHeader>
            <TableRow>
              <TableHead>Name</TableHead>
              <TableHead>Type</TableHead>
              <TableHead>Nullable</TableHead>
              <TableHead>Default</TableHead>
            </TableRow>
          </TableHeader>
          <TableBody>
            {schema.columns.map(col => (
              <TableRow key={col.name}>
                <TableCell>
                  {col.isPrimaryKey && '🔑 '}
                  {col.name}
                </TableCell>
                <TableCell>{col.type}</TableCell>
                <TableCell>{col.isNullable ? '✓' : '✗'}</TableCell>
                <TableCell><code>{col.default}</code></TableCell>
              </TableRow>
            ))}
          </TableBody>
        </Table>
      </TabsContent>
      
      {/* Other tabs */}
    </Tabs>
  )
}

3.4 Row Editor Dialog (DatabaseRowEditor)

Purpose: Detailed form-based editor for complex edits

Features:

  • Trigger: Double-click row to open

  • Layout: Vertical form with all columns

  • Field Types:

    • Text: <Textarea> for long text
    • JSON: Syntax-highlighted editor with validation
    • Foreign Keys: Searchable <Combobox> with preview
    • Images: Preview thumbnail + URL input
    • Markdown: Split view (editor + preview)
    • Arrays: Chip input for multi-value
    • Timestamps: Calendar + time picker
  • Actions:

    • Save: Update row
    • Save & Close: Update and return to table
    • Clone Row: Duplicate with new ID
    • Delete Row: With confirmation
    • Cancel: Discard changes
  • Validation:

    • Real-time field validation
    • Show error messages inline
    • Disable save until valid
  • Metadata Section:

    • Show created_at, updated_at, created_by
    • Show version history (if versioned table)
    • Link to audit log entries

Implementation:

<Dialog open={isOpen} onOpenChange={setIsOpen}>
  <DialogContent className="max-w-2xl max-h-[80vh] overflow-y-auto">
    <DialogHeader>
      <DialogTitle>Edit Row: {rowId}</DialogTitle>
    </DialogHeader>
    
    <form onSubmit={handleSave}>
      {columns.map(col => (
        <div key={col.name} className="space-y-2">
          <Label>{col.name}</Label>
          {renderFieldEditor(col, formData[col.name], handleChange)}
          {errors[col.name] && (
            <p className="text-sm text-destructive">{errors[col.name]}</p>
          )}
        </div>
      ))}
      
      <DialogFooter>
        <Button variant="ghost" onClick={() => setIsOpen(false)}>Cancel</Button>
        <Button variant="outline" onClick={handleClone}>Clone Row</Button>
        <Button variant="destructive" onClick={handleDelete}>Delete</Button>
        <Button type="submit">Save</Button>
      </DialogFooter>
    </form>
  </DialogContent>
</Dialog>

3.5 Real-time Updates

Features:

  • Realtime Subscriptions:

    • Subscribe to table changes via Supabase Realtime
    • Listen for INSERT, UPDATE, DELETE events
    • Show notification when another admin edits
  • Conflict Detection:

    • Detect concurrent edits on same row
    • Show warning: "User X is editing this row"
    • Lock row to prevent conflicts (optional)
  • Auto-refresh:

    • Toggle auto-refresh on/off
    • Refresh interval: 10s, 30s, 60s
    • Manual refresh button
  • Change Notifications:

    • Toast notification: "Table updated by Admin (3 rows changed)"
    • Click to refresh table
    • Dismiss notification

Implementation:

useEffect(() => {
  const channel = supabase
    .channel(`table:${tableName}`)
    .on('postgres_changes', 
      { 
        event: '*', 
        schema: 'public', 
        table: tableName 
      },
      (payload) => {
        toast.info(`Table updated: ${payload.eventType}`)
        refetch() // Refetch table data
      }
    )
    .subscribe()
  
  return () => {
    supabase.removeChannel(channel)
  }
}, [tableName])

3.6 Audit Trail Integration

Features:

  • Automatic Logging:

    • Every edit logged to admin_audit_log
    • Log: admin_user_id, table, row_id, action, old_values, new_values, timestamp
  • Recent Changes Panel (sidebar):

    • Show last 20 changes on current table
    • Display: timestamp, user, action, affected columns
    • Click to view full details
  • Change Details Dialog:

    • Show before/after diff
    • Highlight changed fields
    • Show user who made change
    • Link to row in table
  • Revert Functionality:

    • Revert single change (if safe)
    • Check for dependent changes
    • Confirmation dialog
    • Cannot revert if related data deleted

Implementation:

const logDatabaseEdit = async (
  tableName: string,
  rowId: string,
  operation: 'INSERT' | 'UPDATE' | 'DELETE',
  oldValues: any,
  newValues: any
) => {
  await supabase.rpc('log_database_direct_edit', {
    _table_name: tableName,
    _row_id: rowId,
    _operation: operation,
    _old_values: oldValues,
    _new_values: newValues
  })
}

// In updateData function:
const handleUpdate = async (rowIndex, columnId, value) => {
  const row = tableData[rowIndex]
  const oldValue = row[columnId]
  
  // Update database
  await supabase.from(tableName).update({ [columnId]: value }).eq('id', row.id)
  
  // Log audit trail
  await logDatabaseEdit(
    tableName,
    row.id,
    'UPDATE',
    { [columnId]: oldValue },
    { [columnId]: value }
  )
}

Database Requirements

Supabase RPC Functions

1. get_table_metadata()

Returns metadata for all tables in public schema.

CREATE OR REPLACE FUNCTION get_table_metadata()
RETURNS TABLE (
  table_name text,
  row_count bigint,
  total_size text,
  last_modified timestamp with time zone
) 
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = 'public'
AS $$
BEGIN
  RETURN QUERY
  SELECT 
    t.tablename::text as table_name,
    COALESCE(c.reltuples::bigint, 0) as row_count,
    pg_size_pretty(pg_total_relation_size(t.schemaname||'.'||t.tablename)) as total_size,
    COALESCE(
      (SELECT MAX(created_at) FROM information_schema.tables WHERE table_name = t.tablename),
      NOW()
    ) as last_modified
  FROM pg_tables t
  LEFT JOIN pg_class c ON c.relname = t.tablename
  WHERE t.schemaname = 'public'
  ORDER BY t.tablename;
END;
$$;

-- Grant execute to admins only
REVOKE EXECUTE ON FUNCTION get_table_metadata() FROM PUBLIC;
GRANT EXECUTE ON FUNCTION get_table_metadata() TO authenticated;

-- RLS policy: only admins/superusers can execute
CREATE POLICY "admin_only_table_metadata" ON ... -- (handled in calling code)

2. get_table_schema(table_name text)

Returns detailed schema information for a specific table.

CREATE OR REPLACE FUNCTION get_table_schema(_table_name text)
RETURNS jsonb
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = 'public'
AS $$
DECLARE
  schema_info jsonb;
BEGIN
  -- Get column information
  SELECT jsonb_agg(
    jsonb_build_object(
      'name', column_name,
      'type', data_type,
      'isNullable', is_nullable = 'YES',
      'maxLength', character_maximum_length,
      'numericPrecision', numeric_precision,
      'defaultValue', column_default,
      'isPrimaryKey', EXISTS (
        SELECT 1 FROM information_schema.table_constraints tc
        JOIN information_schema.key_column_usage kcu
          ON tc.constraint_name = kcu.constraint_name
        WHERE tc.table_name = _table_name
          AND tc.constraint_type = 'PRIMARY KEY'
          AND kcu.column_name = c.column_name
      ),
      'foreignKey', (
        SELECT jsonb_build_object(
          'table', ccu.table_name,
          'column', ccu.column_name
        )
        FROM information_schema.table_constraints tc
        JOIN information_schema.key_column_usage kcu
          ON tc.constraint_name = kcu.constraint_name
        JOIN information_schema.constraint_column_usage ccu
          ON ccu.constraint_name = tc.constraint_name
        WHERE tc.table_name = _table_name
          AND tc.constraint_type = 'FOREIGN KEY'
          AND kcu.column_name = c.column_name
        LIMIT 1
      ),
      'enumValues', (
        SELECT array_agg(enumlabel)
        FROM pg_enum e
        JOIN pg_type t ON t.oid = e.enumtypid
        WHERE t.typname = c.udt_name
      )
    )
  )
  INTO schema_info
  FROM information_schema.columns c
  WHERE table_name = _table_name
    AND table_schema = 'public';
  
  RETURN schema_info;
END;
$$;

3. log_database_direct_edit(...)

Logs all direct database edits to audit trail.

CREATE OR REPLACE FUNCTION log_database_direct_edit(
  _table_name text,
  _row_id uuid,
  _operation text,
  _old_values jsonb,
  _new_values jsonb
)
RETURNS void
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = 'public'
AS $$
BEGIN
  INSERT INTO admin_audit_log (
    admin_user_id,
    target_user_id,
    action,
    details
  ) VALUES (
    auth.uid(),
    NULL, -- No specific target user for direct DB edits
    'direct_database_edit',
    jsonb_build_object(
      'table', _table_name,
      'row_id', _row_id,
      'operation', _operation,
      'old_values', _old_values,
      'new_values', _new_values,
      'timestamp', NOW()
    )
  );
END;
$$;

RLS Policies

All tables must enforce AAL2 for admin/superuser direct edits:

-- Example policy for parks table
CREATE POLICY "admin_direct_edit_requires_aal2"
ON parks
FOR ALL
USING (
  (
    EXISTS (
      SELECT 1 FROM user_roles
      WHERE user_id = auth.uid()
      AND role IN ('admin', 'superuser')
    )
  )
  AND (
    (auth.jwt() ->> 'aal') = 'aal2'
    OR NOT EXISTS (
      SELECT 1 FROM auth.mfa_factors
      WHERE user_id = auth.uid()
      AND status = 'verified'
    )
  )
);

-- Apply to all tables:
-- parks, rides, companies, ride_models, locations, profiles, etc.

Implementation Roadmap

Sprint 1: Foundation (Days 1-3)

Goal: Set up routes, navigation, and basic table browser

Tasks:

  1. Create /admin/database route in router
  2. Create AdminDatabase.tsx page component
  3. Add "Database Editor" link to AdminSidebar (superuser only)
  4. Implement access control:
    • Check isAdmin() or isSuperuser()
    • Enforce AAL2 with useSuperuserGuard()
    • Redirect if unauthorized
  5. Create DatabaseTableBrowser.tsx component
  6. Implement get_table_metadata() RPC function
  7. Fetch and display table list with metadata
  8. Add table categorization and color coding
  9. Implement table search/filter
  10. Add warning banner about direct DB access

Deliverables:

  • Working /admin/database page
  • Table list with categories
  • Navigation from sidebar
  • AAL2 enforcement

Sprint 2: Core Editor (Days 4-7)

Goal: Build main spreadsheet editor with basic editing

Tasks:

  1. Install dependencies: @tanstack/react-virtual
  2. Create /admin/database/:tableName route
  3. Create DatabaseTableEditor.tsx component
  4. Implement get_table_schema() RPC function
  5. Fetch table schema and generate dynamic columns
  6. Set up TanStack Table with:
    • Core row model
    • Pagination
    • Row selection
  7. Implement basic inline editing:
    • Text fields
    • Number fields
    • Boolean switches
  8. Add optimistic updates with rollback
  9. Implement save/error handling
  10. Add visual feedback (yellow → green → red)
  11. Create log_database_direct_edit() RPC function
  12. Integrate audit logging on all edits

Deliverables:

  • Working table editor page
  • Inline editing for basic types
  • Audit logging functional

Sprint 3: Advanced Editing (Days 8-10)

Goal: Complete all cell editors, filtering, sorting, column config

Tasks:

  1. Implement all cell editor types:
    • Date/Timestamp picker
    • UUID foreign key selector
    • JSON editor with validation
    • Enum dropdown
    • Array/tag input
  2. Create DatabaseTableFilters.tsx component
  3. Implement filter builder UI
  4. Add filter functions for all data types
  5. Integrate TanStack Table filtering
  6. Add quick filters bar
  7. Implement saved filter presets
  8. Add multi-column sorting (Shift+Click)
  9. Create DatabaseColumnConfig.tsx component
  10. Implement column visibility toggle
  11. Add column reordering (drag-drop)
  12. Implement column pinning
  13. Add column layout presets
  14. Implement keyboard navigation
  15. Add global search

Deliverables:

  • All cell editors working
  • Advanced filtering UI
  • Multi-column sorting
  • Column management panel
  • Keyboard shortcuts

Sprint 4: Bulk & Export (Days 11-12)

Goal: Add bulk operations and import/export functionality

Tasks:

  1. Install dependencies: papaparse
  2. Create DatabaseBulkActions.tsx component
  3. Implement bulk edit functionality
  4. Implement bulk delete with confirmation
  5. Add batch update feature
  6. Create action history with undo
  7. Create DatabaseExportImport.tsx component
  8. Implement CSV export
  9. Implement JSON export
  10. Implement SQL export
  11. Add export options (scope, format)
  12. Implement CSV import with validation
  13. Implement JSON import
  14. Add import preview/dry-run
  15. Create batch export (all tables as ZIP)

Deliverables:

  • Bulk operations working
  • Export in multiple formats
  • Import with validation
  • Action history with undo

Sprint 5: Polish & Safety (Days 13-14)

Goal: Add schema viewer, safety features, and final polish

Tasks:

  1. Create DatabaseSchemaViewer.tsx component
  2. Implement columns view
  3. Add relationships/ERD view
  4. Display constraints and indexes
  5. Show RLS policies (read-only)
  6. Add SQL DDL view with syntax highlighting
  7. Implement read-only mode toggle
  8. Add confirmation dialogs for all destructive actions
  9. Implement real-time updates (Supabase Realtime)
  10. Create recent changes panel
  11. Add change details dialog with diff view
  12. Implement revert functionality (if safe)
  13. Add all safety warnings and checks
  14. Full testing (manual + automated if time)
  15. Write documentation (this file)

Deliverables:

  • Complete schema viewer
  • All safety features
  • Real-time updates
  • Full testing
  • Documentation

Dependencies

Required npm Packages

# Virtual scrolling for large tables
npm install @tanstack/react-virtual

# JSON viewer/editor
npm install react-json-view-lite

# Syntax highlighting
npm install prismjs
npm install @types/prismjs --save-dev

# CSV parsing/generation
npm install papaparse
npm install @types/papaparse --save-dev

Already Installed ()

  • @tanstack/react-table - Table management
  • lucide-react - Icons
  • react-hook-form - Form handling
  • zod - Validation
  • date-fns - Date utilities
  • sonner - Toast notifications

Safety & UX Guidelines

Safety Features Checklist

Confirmation Dialogs

  • Delete single row → "Delete this row?"
  • Delete multiple rows → "Delete X rows? This cannot be undone."
  • Bulk edit → "Update X rows?"
  • Drop table → "⚠️ DANGER: Delete entire table?"
  • Import data → "Import X rows? Existing data may be overwritten."

Validation

  • Enforce NOT NULL constraints
  • Check data type compatibility
  • Validate foreign key references exist
  • Check max length for strings
  • Validate JSON syntax
  • Prevent orphan records (check dependencies)

Access Control

  • AAL2 enforcement on page load
  • Re-check AAL2 before each destructive operation
  • Show MFA prompt if session drops to AAL1
  • Log all operations to audit trail

Error Handling

  • Rollback optimistic updates on error
  • Show clear error messages
  • Prevent partial updates (use transactions)
  • Handle foreign key constraint violations gracefully

Data Integrity

  • Warn before editing critical tables (auth, profiles)
  • Prevent editing system columns (id, created_at, etc.)
  • Check for circular foreign key dependencies
  • Validate enum values against allowed list

UX Best Practices

Loading States

// Skeleton loader for table
{isLoading && <TableSkeleton rows={10} cols={8} />}

// Cell-level loading (during save)
{isSaving && <Spinner className="w-4 h-4" />}

// Progress bar for bulk operations
<Progress value={progress} max={total} />

Error Feedback

// Toast notifications
toast.error('Failed to save: Foreign key violation')
toast.success('Successfully updated 5 rows')
toast.warning('Table has unsaved changes')

// Inline validation
<Input 
  error={errors.email?.message}
  className={errors.email && 'border-destructive'}
/>

Keyboard Shortcuts

Shortcut Action
Ctrl+F Focus search
Ctrl+S Save current row
Ctrl+Z Undo last change
Del Delete selected rows
Ctrl+E Toggle edit mode
Escape Cancel edit / Close dialog
Enter Save edit / Submit form
Arrow keys Navigate cells
Tab Move to next cell
Shift+Tab Move to previous cell
Page Up/Down Scroll page
Home/End Jump to first/last row
? Show keyboard shortcuts help

Responsive Design

  • Horizontal scroll for tables on mobile
  • Collapsible filter/column panels
  • Touch-friendly cell editors (larger tap targets)
  • Stack filters vertically on small screens
  • Hide less important columns by default on mobile

Dark Mode Support

/* Table grid */
.table-cell {
  @apply border-border/40;
  @apply hover:bg-accent/50;
}

/* Cell states */
.cell-editing { @apply bg-yellow-500/20 dark:bg-yellow-500/10; }
.cell-saved { @apply bg-green-500/20 dark:bg-green-500/10; }
.cell-error { @apply bg-red-500/20 dark:bg-red-500/10; }

/* Syntax highlighting */
.json-editor {
  @apply bg-background text-foreground;
}

Security Considerations

Authentication & Authorization

  1. AAL2 Enforcement: All database operations require MFA verification
  2. Role-Based Access: Only admin/superuser roles can access
  3. Session Monitoring: Detect if session drops to AAL1 during use
  4. Token Refresh: Handle token expiration gracefully

Data Protection

  1. Audit Logging: Every edit logged with user, timestamp, before/after values
  2. RLS Policies: Enforce row-level security even in admin UI
  3. Input Sanitization: Validate all user input before DB writes
  4. SQL Injection Prevention: Use parameterized queries (Supabase handles this)

Rate Limiting

-- Prevent abuse: max 1000 edits per admin per hour
CREATE POLICY "rate_limit_admin_edits"
ON admin_audit_log
FOR INSERT
WITH CHECK (
  (SELECT COUNT(*) FROM admin_audit_log 
   WHERE admin_user_id = auth.uid() 
   AND created_at > NOW() - INTERVAL '1 hour'
   AND action = 'direct_database_edit') < 1000
);

Dangerous Operations Warning

Display prominent warnings for:

  • Editing auth-related tables (profiles, user_roles)
  • Deleting > 100 rows at once
  • Importing > 1000 rows
  • Editing production environment (if detected)

Testing Strategy

Manual Testing Checklist

  • AAL2 enforcement blocks AAL1 users
  • Non-admin users cannot access page
  • All table categories display correctly
  • Table search/filter works
  • Table editor loads data correctly
  • Inline editing saves successfully
  • Optimistic updates rollback on error
  • All cell editor types work
  • Filtering works for all data types
  • Multi-column sorting works
  • Column visibility toggle works
  • Column reordering works
  • Pagination works correctly
  • Global search highlights matches
  • Keyboard navigation works
  • Bulk edit updates multiple rows
  • Bulk delete confirms and deletes
  • Export CSV/JSON/SQL works
  • Import validates and inserts data
  • Schema viewer displays correctly
  • Real-time updates show notifications
  • Audit trail logs all edits
  • Recent changes panel displays edits
  • Revert functionality works
  • Confirmation dialogs show for destructive actions
  • Read-only mode prevents edits
  • Dark mode styling looks good

Automated Testing (Future)

// Example Playwright test
test('admin can edit table cell', async ({ page }) => {
  await loginAsAdmin(page)
  await page.goto('/admin/database/parks')
  
  // Click cell to edit
  await page.click('[data-cell="name-0"]')
  await page.fill('input', 'New Park Name')
  await page.press('input', 'Enter')
  
  // Wait for save
  await page.waitForSelector('.cell-saved')
  
  // Verify in database
  const park = await getParkByName('New Park Name')
  expect(park).toBeTruthy()
})

Future Enhancements

Phase 2 Features (Post-MVP)

  1. SQL Query Console:

    • Raw SQL query interface
    • Query history
    • Result set export
    • Query templates/snippets
  2. Visual Query Builder:

    • Drag-drop JOIN builder
    • Visual WHERE clause builder
    • Preview results
  3. Scheduled Jobs:

    • Schedule bulk updates
    • Automated cleanup scripts
    • Email reports
  4. Advanced Analytics:

    • Table usage statistics
    • Most edited tables
    • Edit frequency heatmap
    • Admin activity dashboard
  5. Version Control Integration:

    • Git-like diff view
    • Branch/merge for schema changes
    • Rollback to previous states
  6. Collaboration Features:

    • Live presence indicators (who's viewing)
    • Comments on rows
    • Change proposals (before applying)
    • Team chat in context

Troubleshooting

Common Issues

Issue: "AAL2 required" error blocks access

  • Solution: Ensure user has MFA enrolled and verified. Prompt to verify MFA if session is at AAL1.

Issue: Table loads slowly (> 5s)

  • Solution: Add pagination, reduce pageSize, add indexes to frequently filtered columns.

Issue: Foreign key dropdown shows too many options

  • Solution: Add search/autocomplete to dropdown, paginate results, show only recent 100.

Issue: JSON editor loses formatting on save

  • Solution: Ensure JSON is pretty-printed before display, preserve whitespace.

Issue: Real-time updates cause table to jump

  • Solution: Use optimistic updates, throttle refresh notifications, show "refresh available" button instead of auto-refresh.

Issue: Export fails for large tables (> 10k rows)

  • Solution: Stream export instead of loading all in memory, show progress bar, split into chunks.


Change Log

Date Version Changes Author
2025-01-XX 1.0.0 Initial documentation System

Approval & Sign-off

Status: 📋 Planned (awaiting approval)

Reviewed By: Pending

Approved By: Pending

Implementation Start Date: TBD

Expected Completion: TBD (14 days after start)


End of Document