custom-view

star 6

Build a custom dashboard view from a natural language request — query, API, React component, build, deploy

wnstnb By wnstnb schedule Updated 4/8/2026

name: custom-view description: Build a custom dashboard view from a natural language request — query, API, React component, build, deploy trigger: manual

Custom View

Build a custom dashboard tab on demand. The user asks a question about their finances in natural language, and this skill creates a new query function, API endpoint, and React component, then builds and deploys the updated dashboard.

When to activate

  • The user says "show me...", "add a tab for...", "I want to see...", "build me a view of..."
  • Any request for a custom financial visualization not covered by the existing 6 tabs
  • The user says "custom view", "new dashboard tab", or "add to dashboard"
  • Also handles removal: "remove the ... tab", "delete custom view ..."

Prerequisites

  • dashboard/dist/ must exist (run cd dashboard && npm run build if not)
  • Dashboard server must be running on port 3847
  • Database at data/foliome.db must have data (run a sync first)

Removal Flow

If the user asks to remove a custom tab (e.g., "remove the restaurant spending tab"):

  1. Read dashboard/src/App.tsx and find the TABS entry with a custom- prefixed ID matching the request.
  2. If not found, tell the user which custom tabs exist and ask which one to remove.
  3. Remove the tab entry from the TABS array and the content switch case in App.tsx.
  4. Delete the component file dashboard/src/tabs/Custom_*.tsx.
  5. Remove the API route from dashboard-server.js.
  6. Remove the query function from dashboard-queries.js.
  7. Run cd dashboard && npm run build.
  8. Restart the dashboard server.
  9. Confirm removal to the user.

Build Flow

Step 1: Understand the request

Parse the natural language request. Determine what data is needed using the schema reference in docs/dashboard-customization.md.

Key tables and patterns:

  • transactions — date, description, amount, category, user_category, account_id, institution
  • balances — account_id, account_type, balance, synced_at (use latest-per-account pattern)
  • holdings — symbol, name, quantity, price, market_value
  • investment_transactions — date, symbol, type, amount, quantity, price
  • Spending queries: WHERE category NOT IN ('Transfer', 'Income') AND amount < 0
  • Category with overrides: COALESCE(user_category, category)

Step 2: Check tab limits and generate slug

  1. Read dashboard/src/App.tsx. Count existing custom tabs (IDs starting with custom-).
  2. If 8 custom tabs already exist, ask the user which one to replace. Remove it first (see Removal Flow).
  3. Generate a tab slug: lowercase, alphanumeric + hyphens only, max 30 chars, derived from key terms in the request. Prefix with custom- (e.g., custom-restaurant-monthly).
  4. Verify no conflict with existing tab IDs in the TABS array. If conflict, append -2, -3, etc.

Step 3: Write the query function

Add a new function to scripts/dashboard-queries.js following the existing pattern:

function getCustomRestaurantMonthly(dbPath) {
  const db = openDb(dbPath);
  // ... query logic ...
  db.close();
  return { /* results */ };
}

Rules:

  • Use openDb(dbPath) and db.close().
  • Use .prepare() with parameterized queries for any user-supplied values.
  • Named params in the JS object must NOT have $ prefix (e.g., params.from not params.$from). The SQL uses $from but better-sqlite3 expects from in the params object.
  • Add the function to the module.exports at the bottom of the file.

Verify the query works before proceeding:

node -e "const q = require('./scripts/dashboard-queries.js'); const r = q.getCustomRestaurantMonthly(); console.log(JSON.stringify(r).slice(0, 200));"

If it errors, fix the query and test again.

Step 4: Add the API route

In scripts/dashboard-server.js:

  1. Add the import to the require('./dashboard-queries.js') destructuring at the top.
  2. Add a new route in the API routes section (inside the if (parsed.pathname.startsWith('/api/')) block), before the 404 fallback:
if (parsed.pathname === '/api/custom-restaurant-monthly') {
  sendJson(getCustomRestaurantMonthly());
  return;
}

Step 5: Restart the dashboard server

New API routes require a server restart. The server is a Node.js process on port 3847.

# Kill existing server and restart
kill $(lsof -ti:3847) 2>/dev/null
node scripts/dashboard-server.js &
sleep 2
curl -s http://localhost:3847/health

Wait for the health check to return "ok" before proceeding.

Step 6: Create the React component

Write dashboard/src/tabs/Custom_{PascalSlug}.tsx:

import { useEffect, useState } from 'react';
import { fetchWithAuth } from '@/lib/api';
import { EmptyState } from '@/components/shared/EmptyState';

interface CustomData { /* match the query return shape */ }

export function Custom{PascalSlug}() {
  const [data, setData] = useState<CustomData | null>(null);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    fetchWithAuth<CustomData>('/api/custom-{slug}')
      .then(setData)
      .catch(e => setError(e.message));
  }, []);

  if (error) return <EmptyState message={error} />;
  if (!data) return <div className="py-12 text-center t-caption text-[var(--text-muted)]">Loading...</div>;

  return (
    <div className="animate-fade-in">
      {/* Render the data using Tailwind + CSS variables */}
      {/* Use existing shared components: EmptyState, KPICard, TransactionRow, etc. */}
      {/* Use fmtAccounting, fmtShort, fmtPercent from @/lib/format */}
      {/* Charts: import from recharts (PieChart, AreaChart, BarChart, LineChart) */}
    </div>
  );
}

Design guidelines:

  • Use raw Tailwind + CSS variables for layout/styling
  • Import shared components from @/components/shared/ as needed
  • Import formatting utilities from @/lib/format
  • For charts, import from recharts (already a dependency)
  • Follow the card pattern: <div className="rounded-xl border border-[var(--border)] bg-[var(--bg-card)] p-4 mb-3">
  • Use typography classes: t-hero, t-value, t-body, t-caption, t-micro
  • Color classes: text-[var(--positive)], text-[var(--negative)], text-[var(--warning)], text-[var(--brand)]
  • Do NOT import new shadcn dependencies

Step 7: Register the tab in App.tsx

  1. Add the import at the top of App.tsx:

    import { CustomRestaurantMonthly } from '@/tabs/Custom_RestaurantMonthly';
    
  2. Add to the TABS array:

    { id: 'custom-restaurant-monthly', label: 'Restaurants' },
    
  3. Add the content switch case in the tab content section:

    {activeTab === 'custom-restaurant-monthly' && <CustomRestaurantMonthly />}
    

Step 8: Backup dist/

cp -r dashboard/dist dashboard/dist-backup 2>/dev/null || true

Step 9: Build

cd dashboard && npm run build

If the build fails:

  1. Read the error message.
  2. Fix the issue in the component, query, or App.tsx.
  3. Retry npm run build.
  4. Maximum 3 attempts. If all fail, proceed to Step 12 (rollback).

Step 10: Smoke test

curl -s http://localhost:3847/api/custom-restaurant-monthly

This will return 401 (no auth token), which confirms the route exists. A 404 means the server wasn't restarted or the route wasn't added correctly.

For a full data test, use a dev token if available, or verify the build output:

ls dashboard/dist/index.html && echo "Build output exists"

Step 11: Report success

Tell the user the new tab is ready. If running as a Telegram agent, present the dashboard as a Mini App button so the user can tap to see the new tab immediately:

node scripts/telegram-notify.js --dashboard "<chatId>" "Built your 'Restaurant Spending' tab. Tap to see it." "<tunnel-url>"

Use the chatId from the inbound Telegram message and the active cloudflared tunnel URL. Do NOT send the dashboard URL as a plain text link — it won't work without Mini App initData.

Step 12: Rollback on failure

If all build retries are exhausted:

  1. Revert changes to dashboard-queries.js (remove the new function and export).
  2. Revert changes to dashboard-server.js (remove the API route and import).
  3. Delete the component file dashboard/src/tabs/Custom_*.tsx.
  4. Revert changes to App.tsx (remove tab entry and switch case).
  5. Restore dashboard/dist-backup to dashboard/dist.
  6. Restart the dashboard server.
  7. Tell the user: "I couldn't build that view. Here's what went wrong: [error]. Try rephrasing your request or ask for something simpler."

Key Principle

The customization doc at docs/dashboard-customization.md is the instruction manual. Read it before building. It has the SQL patterns, the React component patterns, the chart patterns, and the design tokens. Follow it.

Install via CLI
npx skills add https://github.com/wnstnb/foliome --skill custom-view
Repository Details
star Stars 6
call_split Forks 2
navigation Branch main
article Path SKILL.md
More from Creator