Skill: gangtao/timeplus-app-builder (version 1.0.0)

--- name: timeplus-app-builder description: Build real-time Timeplus data processing and analysis applications. Creates pure frontend HTML/JavaScript apps that connect directly to Timeplus Proton via @timeplus/proton-javascript-driver (UMD), visualize live streaming data with @timeplus/vistral (UMD), and follow the Timeplus UI style guide. No npm build or bundler required — output is a single self-contained HTML file. license: Apache-2.0 compatibility: pulsebot>=0.1.0 allowed-tools: shell, workspace, file_ops --- # Timeplus App Builder Use this skill whenever the user asks to build a data processing application, pipeline visualizer, real-time dashboard, streaming analytics app, or any frontend tool that queries or visualizes data from Timeplus Proton. ## Overview You will produce a **single self-contained HTML file** that: 1. Queries Timeplus Proton directly via `@timeplus/proton-javascript-driver` loaded from unpkg CDN 2. Visualizes streaming data using `@timeplus/vistral` loaded from unpkg CDN 3. Follows the **Timeplus App Style Guide** (dark theme, brand colors, clean layout) 4. Requires **no npm install, no build step** — open the HTML file directly in a browser > **Key architecture:** Both `@timeplus/proton-javascript-driver` and `@timeplus/vistral` ship UMD builds. Import them via `<script src="https://unpkg.com/...">` tags. The Proton driver connects directly to the agent proxy running at `localhost:8001`. --- ## Step-by-Step Workflow ### Step 1 — Clarify Requirements Before writing any code, confirm: - What stream(s) or table(s) to query (name, schema if known) - What kind of visualization: time series, bar/column, table, single value, or multi-panel - Whether the query is **streaming** (`SELECT ... FROM stream_name`) or **historical** (`SELECT ... FROM table(stream_name)`) - Any filters, aggregations, or window functions needed If the user doesn't know the schema, suggest running `DESCRIBE stream_name` or `SHOW STREAMS` first. --- ### Step 2 — Use the HTML Template All Timeplus apps follow this single-file HTML template. Read `references/STYLE_GUIDE.md` for style rules and `references/VISTRAL_API.md` for chart configuration options. ```html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>My Timeplus App</title> <!-- 1. React (required by Vistral) --> <script src="https://unpkg.com/react@18/umd/react.production.min.js"></script> <script src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script> <!-- 2. Vistral peer dependencies — load BEFORE vistral --> <script src="https://unpkg.com/lodash@4/lodash.min.js"></script> <script src="https://unpkg.com/ramda@0.29/dist/ramda.min.js"></script> <script src="https://unpkg.com/@antv/g2@5/dist/g2.min.js"></script> <script src="https://unpkg.com/@antv/s2@2/dist/s2.min.js"></script> <script> // Prevent jsx-runtime from crashing on process.env access window.process = window.process || { env: { NODE_ENV: "production" } }; // Some builds also expect global window.global = window.global || window; </script> <!-- 3. Vistral UMD — exposes window.Vistral --> <script src="https://unpkg.com/@timeplus/vistral/dist/index.umd.min.js"></script> <!-- 4. Proton JavaScript Driver UMD — exposes window.ProtonDriver --> <script src="https://unpkg.com/@timeplus/proton-javascript-driver/dist/index.umd.js"></script> <style> /* Timeplus design tokens */ :root { --tp-bg-primary: #0f1117; --tp-bg-secondary: #1a1d27; --tp-bg-card: #1e2235; --tp-bg-hover: #252a3a; --tp-accent-primary: #7c6af7; --tp-accent-secondary: #4fc3f7; --tp-accent-success: #4caf82; --tp-accent-warning: #f7a84f; --tp-accent-danger: #f76f6f; --tp-text-primary: #e8eaf6; --tp-text-secondary: #9ea3b8; --tp-text-muted: #5c6380; --tp-border: #2e3450; --tp-font-mono: 'JetBrains Mono', 'Fira Code', monospace; --tp-font-sans: 'Inter', system-ui, sans-serif; --tp-radius-lg: 12px; --tp-shadow: 0 2px 12px rgba(0,0,0,0.4); } * { box-sizing: border-box; margin: 0; padding: 0; } body { background: var(--tp-bg-primary); color: var(--tp-text-primary); font-family: var(--tp-font-sans); font-size: 14px; line-height: 1.5; } .tp-app { display: flex; flex-direction: column; height: 100vh; overflow: hidden; } .tp-header { display: flex; align-items: center; gap: 12px; padding: 0 24px; height: 56px; background: var(--tp-bg-secondary); border-bottom: 1px solid var(--tp-border); flex-shrink: 0; } .tp-header-title { font-size: 16px; font-weight: 600; } .tp-header-badge { font-size: 11px; padding: 2px 8px; background: var(--tp-accent-primary); color: white; border-radius: 99px; font-weight: 500; } .tp-status { display: flex; align-items: center; gap: 6px; font-size: 12px; color: var(--tp-text-muted); margin-left: auto; } .tp-status-dot { width: 7px; height: 7px; border-radius: 50%; background: var(--tp-text-muted); } .tp-status-dot.connected { background: var(--tp-accent-success); } .tp-status-dot.error { background: var(--tp-accent-danger); } .tp-main { flex: 1; overflow: auto; padding: 20px 24px; display: grid; gap: 16px; } .tp-card { background: var(--tp-bg-card); border: 1px solid var(--tp-border); border-radius: var(--tp-radius-lg); padding: 16px; box-shadow: var(--tp-shadow); } .tp-card-title { font-size: 13px; font-weight: 600; color: var(--tp-text-secondary); text-transform: uppercase; letter-spacing: 0.04em; margin-bottom: 12px; } .tp-error { background: rgba(247,111,111,0.1); border: 1px solid var(--tp-accent-danger); border-radius: 8px; padding: 12px 16px; color: var(--tp-accent-danger); font-size: 13px; display: none; } .chart-container { min-height: 300px; } </style> </head> <body> <div class="tp-app"> <header class="tp-header"> <span class="tp-header-title">My Stream App</span> <span class="tp-header-badge">LIVE</span> <span class="tp-status"> <span class="tp-status-dot" id="status-dot"></span> <span id="status-text">Connecting…</span> </span> </header> <main class="tp-main"> <div class="tp-error" id="error-panel"></div> <div class="tp-card"> <div class="tp-card-title">Live Stream</div> <div class="chart-container" id="chart"></div> </div> </main> </div> <script> // ---- Library globals ---- // proton-javascript-driver UMD exposes window.ProtonDriver const { ProtonClient } = window.ProtonDriver; // vistral UMD exposes window.Vistral; React/ReactDOM are also on window const { Vistral, React, ReactDOM } = window; const { StreamChart } = Vistral; // ---- Configuration ---- const SQL = 'SELECT * FROM my_stream'; // TODO: replace with your SQL // Connect to the Proton proxy running on the agent at localhost:8001 const client = new ProtonClient({ host: 'localhost', port: 8001 }); // ---- State ---- let columns = []; let rows = []; let chartRoot = null; // ---- Helpers ---- function setStatus(state, message) { document.getElementById('status-dot').className = 'tp-status-dot ' + (state || ''); document.getElementById('status-text').textContent = message; } function showError(msg) { const panel = document.getElementById('error-panel'); panel.style.display = msg ? 'block' : 'none'; panel.textContent = msg || ''; if (msg) setStatus('error', 'Error'); } function inferColumns(row) { return Object.entries(row).map(([name, value]) => ({ name, type: typeof value === 'number' ? 'float64' : String(value).match(/^\d{4}-\d{2}-\d{2}/) ? 'datetime64' : 'string', })); } // ---- Render chart ---- function renderChart() { if (columns.length === 0) return; if (!chartRoot) chartRoot = ReactDOM.createRoot(document.getElementById('chart')); // TODO: adjust config for your data — see references/VISTRAL_API.md const config = { chartType: 'line', xAxis: columns[0].name, yAxis: columns[1]?.name ?? columns[0].name, temporal: { mode: 'axis', field: columns[0].name, range: 60 }, }; chartRoot.render( React.createElement(StreamChart, { config, data: { columns, data: rows, isStreaming: true }, theme: 'dark', }) ); } // ---- Main query loop ---- async function runQuery() { try { setStatus('', 'Connecting…'); const { rows: stream, abort } = await client.query(SQL); setStatus('connected', 'Connected'); // Expose abort so it can be called externally if needed window.stopQuery = abort; for await (const row of stream) { if (columns.length === 0) columns = inferColumns(row); rows = [...rows.slice(-999), row]; // keep last 1000 rows renderChart(); } setStatus('', 'Stream ended'); } catch (err) { showError(err.message); } } runQuery(); </script> </body> </html> ``` --- ### Step 3 — Proton Connection Read `references/PROTON_DRIVER.md` for the full driver API. The driver connects directly to the Proton proxy at `localhost:8001`. UMD global: `window.ProtonDriver`. ```javascript const { ProtonClient } = window.ProtonDriver; const client = new ProtonClient({ host: 'localhost', port: 8001 }); const { rows, abort } = await client.query('SELECT * FROM my_stream'); for await (const row of rows) { // row is a plain JavaScript object: { col1: val1, col2: val2, ... } console.log(row); } // To cancel a running streaming query: abort(); ``` --- ### Step 4 — Visualization with Vistral Read `references/VISTRAL_API.md` for the complete component reference. Vistral UMD global: `window.Vistral`. Key components: `StreamChart`, `SingleValueChart`, `DataTable`. All charts are rendered via React (`React.createElement`). Always pass `theme="dark"`. **Quick chart config reference:** | Chart type | `chartType` | Best for | temporal `mode` | |-----------|-------------|----------|-----------------| | Time series | `'line'` or `'area'` | Scrolling metrics over time | `'axis'` | | Horizontal bars | `'bar'` | Ranked categories, latest snapshot | `'frame'` | | Vertical bars | `'column'` | Categorical comparison | `'frame'` | | KPI tile | `'singleValue'` | One big number | `'frame'` | | Live table | `'table'` | Raw rows / mutable state | `'key'` | | Geo points | `'geo'` | Location data | `'key'` | --- ### Step 5 — Apply Timeplus Style Guide Read `references/STYLE_GUIDE.md` for full rules. Core rules always applied: - Dark background `#0f1117` for page, `#1e2235` for cards - Purple `#7c6af7` for primary accents - Inter font for UI, monospace for data values - Cards with `border-radius: 12px` and border `#2e3450` - Header 56px with LIVE badge and connection status indicator --- ### Step 6 — Deliver the App 1. Write the complete single-file HTML, filling in the correct SQL and chart config 2. use workspace_create_app to create the app 3. Present the file — it opens directly in a browser with no server or build step needed For PulseBot deployment: ```python workspace_create_app( session_id=session_id, task_name="My Stream App", html=open("my-app.html").read() ) ``` --- ## SQL Patterns Load the `timeplus-sql-guide` skill for all SQL questions: - Streaming vs historical queries - Window functions (tumble, hop, session) - Aggregations, joins, filters - Schema introspection (`DESCRIBE`, `SHOW STREAMS`) --- ## Checklist Before Delivering - [ ] Single HTML file — no separate JS/CSS files, no package.json, no build step - [ ] Script load order: React → ReactDOM → lodash → ramda → @antv/g2 → @antv/s2 → vistral → proton-javascript-driver - [ ] `window.ProtonDriver` destructured for `ProtonClient` - [ ] `window.Vistral` destructured for `StreamChart` (and other components as needed) - [ ] `ProtonClient` configured with `{ host: 'localhost', port: 8001 }` - [ ] SQL uses correct Proton streaming syntax - [ ] `columns` inferred from first received row using `inferColumns()` - [ ] Chart `config.temporal` uses correct `mode` for the query type - [ ] Status indicator updates on connect, stream end, and error - [ ] `abort()` stored on `window.stopQuery` or similar - [ ] CSS uses Timeplus design tokens (CSS variables) from the style template

path: references/VISTRAL_API.md
content: # Vistral API Reference

## Package & CDN

npm: `@timeplus/vistral`  
GitHub: https://github.com/timeplus-io/vistral  
Playground: https://timeplus-io.github.io/vistral/

**UMD import (use this in HTML apps — no npm required):**

Load dependencies first, then Vistral:
```html
<!-- React (required peer dependency) -->
<script src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>

<!-- Vistral peer dependencies — must load BEFORE vistral -->
<script src="https://unpkg.com/lodash@4/lodash.min.js"></script>
<script src="https://unpkg.com/ramda@0.29/dist/ramda.min.js"></script>
<script src="https://unpkg.com/@antv/g2@5/dist/g2.min.js"></script>
<script src="https://unpkg.com/@antv/s2@2/dist/s2.min.js"></script>

<script>
    // Prevent jsx-runtime from crashing on process.env access
    window.process = window.process || {
      env: { NODE_ENV: "production" }
    };

    // Some builds also expect global
    window.global = window.global || window;
  </script>

<!-- Vistral UMD — exposes window.Vistral -->
<script src="https://unpkg.com/@timeplus/vistral/dist/index.umd.min.js"></script>
```

Access globals:
```javascript
const { Vistral, React, ReactDOM } = window;
const { StreamChart, SingleValueChart, DataTable, VistralChart } = Vistral;
```

---

## Core Concepts

### Temporal Binding

Vistral has three modes controlling how streaming data maps to the visible frame:

| Mode | Description | Use case |
|------|-------------|----------|
| `'axis'` | X-axis scrolls with time, oldest drops off left | Live time-series trends |
| `'frame'` | Only rows at the latest timestamp are shown | Real-time snapshots, leaderboards |
| `'key'` | Latest value per unique key, updated in place | Mutable counters, live dashboards |

Configure via the `temporal` object in `config`:
```javascript
temporal: {
  mode: 'axis',       // 'axis' | 'frame' | 'key'
  field: 'timestamp', // column name for time or key
  range: 60,          // (axis mode only) window size in seconds/minutes — depends on data
}
```

### Data Format

All chart components accept a `data` prop:
```javascript
{
  columns: [
    { name: 'timestamp', type: 'datetime64' },
    { name: 'value', type: 'float64' },
    { name: 'category', type: 'string' },
  ],
  data: [
    // Array-of-objects format (preferred for Proton driver output):
    { timestamp: '2024-01-01T10:00:00Z', value: 42.5, category: 'A' },
    // OR array-of-arrays format:
    // ['2024-01-01T10:00:00Z', 42.5, 'A'],
  ],
  isStreaming: true,  // set true for live data
}
```

---

## StreamChart (Unified API — use for most cases)

```javascript
React.createElement(StreamChart, {
  config,      // StreamChartConfig — see chart types below
  data,        // StreamDataSource
  theme: 'dark',  // 'dark' | 'light' — always use 'dark' for Timeplus apps
  height: 300,    // optional
  width: '100%',  // optional
})
```

### Line Chart

```javascript
config = {
  chartType: 'line',          // or 'area' for filled
  xAxis: 'timestamp',         // datetime column
  yAxis: 'value',             // numeric column (or array for multi-series)
  color: 'category',          // optional: split into series by this column
  lineStyle: 'curve',         // 'curve' | 'straight'
  points: true,               // show data points
  legend: true,
  gridlines: true,
  temporal: {
    mode: 'axis',
    field: 'timestamp',
    range: 5,                 // show last 5 minutes
  },
  fractionDigits: 2,          // decimal places for Y axis
  colors: ['#7c6af7', '#4fc3f7'],  // optional custom palette
}
```

### Area Chart

```javascript
config = {
  chartType: 'area',
  xAxis: 'timestamp',
  yAxis: 'value',
  color: 'category',          // creates stacked areas if multi-series
  temporal: { mode: 'axis', field: 'timestamp', range: 5 },
}
```

### Bar Chart (Horizontal)

```javascript
config = {
  chartType: 'bar',           // horizontal bars
  xAxis: 'category',          // categorical column
  yAxis: 'count',             // numeric column
  groupType: 'stack',         // 'stack' | 'dodge'
  dataLabel: true,            // show value labels on bars
  temporal: { mode: 'frame', field: '_tp_time' },
}
```

### Column Chart (Vertical)

```javascript
config = {
  chartType: 'column',        // vertical bars
  xAxis: 'month',
  yAxis: 'sales',
  color: 'region',
  groupType: 'dodge',
  gridlines: true,
  temporal: { mode: 'frame', field: '_tp_time' },
}
```

### Single Value (KPI)

```javascript
config = {
  chartType: 'singleValue',
  yAxis: 'activeUsers',       // column whose latest value to display
  fontSize: 72,               // font size for the number
  color: 'green',             // text color
  fractionDigits: 0,
  sparkline: true,            // show mini sparkline
  delta: true,                // show up/down change indicator
  unit: { position: 'left', value: '$' },  // optional prefix/suffix
  temporal: { mode: 'frame', field: '_tp_time' },
}
```

### Data Table

```javascript
config = {
  chartType: 'table',
  tableStyles: {
    // Optional per-column display config:
    timestamp: { name: 'Time', width: 200 },
    value: {
      name: 'Value',
      miniChart: 'sparkline',
      color: {
        type: 'condition',
        conditions: [
          { operator: 'gt', value: 100, color: '#22C55E' },
          { operator: 'lt', value: 50, color: '#EF4444' },
        ],
      },
    },
  },
  temporal: { mode: 'key', field: 'id' },  // deduplicate rows by 'id'
}
```

### Geo Chart

```javascript
config = {
  chartType: 'geo',
  latitude: 'lat',
  longitude: 'lng',
  color: 'category',          // color points by category
  size: { key: 'value', min: 4, max: 20 },  // size points by value column
  zoom: 3,
  center: [40.7128, -74.006], // [lat, lng]
  showZoomControl: true,
  pointOpacity: 0.8,
  temporal: { mode: 'key', field: ['region', 'vehicle_id'] },  // composite key
}
```

---

## Individual Components

### SingleValueChart

```javascript
React.createElement(SingleValueChart, {
  config: {
    label: 'Events per second',
    unit: 'eps',
    precision: 1,
    trend: true,
    sparkline: true,
  },
  data: currentValue,      // number or string
  history: historyArray,   // optional: array of numbers for sparkline
  theme: 'dark',
})
```

### DataTable

```javascript
React.createElement(DataTable, {
  config: {
    maxRows: 100,
    showTimestamp: true,
    columns: ['col1', 'col2'],  // optional: show only these columns
    highlightNew: true,
  },
  data: { columns, data: rows },
  theme: 'dark',
})
```

---

## VistralChart (Low-Level Grammar API)

For complex visualizations beyond what `StreamChart` covers:

```javascript
const spec = {
  type: 'layer',
  marks: [
    {
      type: 'line',
      encode: {
        x: { field: 'ts', type: 'temporal' },
        y: { field: 'value', type: 'quantitative' },
        color: { field: 'service', type: 'nominal' },
      },
    },
  ],
  streaming: {
    mode: 'axis',
    timeField: 'ts',
    windowSeconds: 120,
  },
};

React.createElement(VistralChart, {
  spec,
  source: { columns, data: rows, isStreaming: true },
  theme: 'dark',
})
```

---

## useStreamingData Hook

Manages a bounded streaming buffer in state. **Note:** In HTML UMD apps, this hook requires React functional component context. For plain script usage, manage state manually (e.g. `rows = [...rows.slice(-999), newRow]`).

```javascript
// In a React functional component only:
const { data, append, clear, replace } = Vistral.useStreamingData([], 1000);

append(newRows);    // add new rows
replace(snapshot);  // replace entire dataset (for frame-bound)
clear();            // clear all data
```

---

## Color Palettes

```javascript
const { multiColorPalettes, singleColorPalettes, findPaletteByLabel } = Vistral;

// Multi-color: 'Dawn', 'Morning', 'Midnight', 'Ocean', 'Sunset'
const palette = findPaletteByLabel('Midnight');  // recommended for Timeplus apps
// palette.values = ['#7c6af7', '#4fc3f7', ...]

// Apply to chart:
config.colors = palette.values;
```

Recommended palette for Timeplus apps: **Midnight** (matches the dark purple brand).

---

## Rendering Pattern (UMD)

In a plain HTML app, always render via `React.createElement` — no JSX:

```javascript
const root = ReactDOM.createRoot(document.getElementById('chart'));

function render() {
  root.render(
    React.createElement(StreamChart, {
      config: { chartType: 'line', xAxis: 'ts', yAxis: 'val',
                temporal: { mode: 'axis', field: 'ts', range: 60 } },
      data: { columns, data: rows, isStreaming: true },
      theme: 'dark',
    })
  );
}
```

Call `render()` each time `rows` changes to push updates to the chart.

---

## Temporal Binding Cheatsheet

| Mode | `config.temporal` | Best Proton SQL pattern |
|------|-------------------|------------------------|
| `'axis'` | `{ mode: 'axis', field: 'ts', range: 60 }` | `SELECT ts, val FROM stream` |
| `'frame'` | `{ mode: 'frame', field: '_tp_time' }` | `LATEST_BY` or window aggregation |
| `'key'` | `{ mode: 'key', field: 'device_id' }` | `SELECT device_id, latest(val) GROUP BY device_id` |

---

## Themes

Always use `theme="dark"` for Timeplus-branded applications. The dark theme uses:
- Background: `#1e2235`
- Axis/grid: muted purple-gray
- Default palette: Midnight


path: references/PROTON_DRIVER.md
content: # Proton JavaScript Driver Reference

## Package & CDN

npm: `@timeplus/proton-javascript-driver`  
GitHub: https://github.com/timeplus-io/proton-javascript-driver

**UMD import (use this in HTML apps — no npm required):**
```html
<script src="https://unpkg.com/@timeplus/proton-javascript-driver/dist/index.umd.js"></script>
```

This exposes `window.ProtonDriver`. Destructure it:
```javascript
const { ProtonClient } = window.ProtonDriver;
```

---

## Overview

`@timeplus/proton-javascript-driver` is a streaming REST client for Timeplus Proton. It connects to Proton's HTTP port and returns rows as an async iterable — one JavaScript object per row, streamed as data arrives.

In the agent context, a proxy to Proton is already running at `localhost:8001`. Always use this host and port — do not connect to Proton's raw port directly.

---

## Creating a Client

```javascript
const client = new ProtonClient({
  host: 'localhost',  // agent proxy host
  port: 8001,         // agent proxy port
  // username: 'default',  // optional
  // password: '',         // optional
  // timeout: 30000,       // optional connection timeout ms
});
```

### Configuration options

```
ProtonConfig {
  host?:     string   // default: 'localhost'
  port?:     number   // default: 3218  — always override to 8001 for agent use
  username?: string
  password?: string
  timeout?:  number   // connection timeout in milliseconds
}
```

---

## Running a Query

```javascript
const { rows, abort } = await client.query('SELECT * FROM my_stream');
```

Returns:
- `rows` — `AsyncIterableIterator<T>` — yields one object per result row
- `abort()` — function to cancel the query immediately

### Consuming rows

```javascript
for await (const row of rows) {
  // row is a plain object: { col1: val1, col2: val2, ... }
  console.log(row);
}
```

### With an AbortSignal

```javascript
const { rows, abort } = await client.query(sql, { signal: myAbortSignal });
```

### Cancellation

```javascript
const { rows, abort } = await client.query('SELECT * FROM my_stream');

// Cancel after 10 seconds
setTimeout(abort, 10000);

for await (const row of rows) {
  // loop exits cleanly when abort() is called
}
```

---

## Streaming vs Historical Queries

Both query types use the same `client.query()` API:

| Query type | SQL pattern | Behaviour |
|-----------|-------------|-----------|
| **Streaming** | `SELECT ... FROM stream_name` | Runs forever, emits rows as they arrive |
| **Historical** | `SELECT ... FROM table(stream_name)` | Returns past data then terminates |
| **Hybrid** | `SELECT ... FROM stream_name WHERE _tp_time > now() - 5m` | Backfills then continues streaming |

---

## Row Format

Each row is a plain JavaScript object keyed by column name:

```javascript
// Example row from sensor_stream
{ event_time: '2024-01-15T10:23:45.123Z', device_id: 'sensor-42', value: 98.7 }
```

Column type mapping from Proton:

| Proton type | JavaScript representation |
|-------------|--------------------------|
| `int*`, `uint*`, `float*` | `number` |
| `string`, `fixed_string` | `string` |
| `datetime`, `datetime64` | `string` (ISO 8601) |
| `array(T)` | `Array` |
| `nullable(T)` | value or `null` |

---

## Inferring Column Definitions for Vistral

Vistral's `StreamChart` needs a `columns` array with `name` and `type`. Infer these from the first received row:

```javascript
function inferColumns(row) {
  return Object.entries(row).map(([name, value]) => ({
    name,
    type: typeof value === 'number' ? 'float64'
        : String(value).match(/^\d{4}-\d{2}-\d{2}/) ? 'datetime64'
        : 'string',
  }));
}

let columns = [];
for await (const row of rows) {
  if (columns.length === 0) columns = inferColumns(row);
  // ... process row
}
```

---

## Low-Level: `ndjsonStreamParser`

For custom streaming needs, the driver also exports a low-level NDJSON parser:

```javascript
const { ndjsonStreamParser } = window.ProtonDriver;

const response = await fetch('http://localhost:8001/query', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ query: sql }),
});

const reader = response.body.getReader();
for await (const row of ndjsonStreamParser(reader)) {
  console.log(row);
}
```

---

## Error Handling

```javascript
try {
  const { rows, abort } = await client.query(sql);
  for await (const row of rows) {
    // process row
  }
} catch (err) {
  // Connection errors, SQL syntax errors, etc.
  console.error('Query failed:', err.message);
}
```

Common errors:

| Error | Likely cause |
|-------|-------------|
| `Failed to connect` | Proton proxy not running at localhost:8001 |
| `Bad SQL` / `Unknown stream` | SQL syntax error or stream doesn't exist |
| `AbortError` | Query was cancelled via `abort()` |

---

## Full Example (in HTML)

```html
<script src="https://unpkg.com/@timeplus/proton-javascript-driver/dist/index.umd.js"></script>
<script>
  const { ProtonClient } = window.ProtonDriver;

  const client = new ProtonClient({ host: 'localhost', port: 8001 });

  async function streamData() {
    try {
      const { rows, abort } = await client.query(
        'SELECT device_id, value, _tp_time FROM sensor_stream'
      );

      // Store abort so we can cancel later
      window.stopQuery = abort;

      for await (const row of rows) {
        console.log(row.device_id, row.value, row._tp_time);
      }
    } catch (err) {
      console.error(err.message);
    }
  }

  streamData();
</script>
```


path: references/STYLE_GUIDE.md
content: # Timeplus Application Style Guide

Source: https://gist.github.com/gangtao/5307e8bbb84384804c7fb9480a515925

---

## Design Principles

1. **Dark-first** — All Timeplus apps use a dark theme. Never ship a light-mode app without an explicit request.
2. **Data density** — Maximize information per pixel. Avoid decorative whitespace.
3. **Live awareness** — Always show whether the app is connected and streaming.
4. **Monospace for data** — All numeric values, SQL, and raw data use monospace font.
5. **Subtle motion** — Streaming updates should feel fluid, not jarring.

---

## Color Palette

### Background layers (darkest → lightest)
```
--tp-bg-primary:    #0f1117   /* Page background */
--tp-bg-secondary:  #1a1d27   /* Header, sidebar, nav */
--tp-bg-card:       #1e2235   /* Cards, panels */
--tp-bg-hover:      #252a3a   /* Interactive hover states */
--tp-bg-overlay:    #2e3450   /* Modals, dropdowns */
```

### Brand / Accent colors
```
--tp-accent-primary:    #7c6af7   /* Timeplus purple — primary actions, links */
--tp-accent-secondary:  #4fc3f7   /* Cyan — data, streaming indicators */
--tp-accent-success:    #4caf82   /* Green — OK / connected */
--tp-accent-warning:    #f7a84f   /* Orange — warnings */
--tp-accent-danger:     #f76f6f   /* Red — errors, alerts */
```

### Text
```
--tp-text-primary:   #e8eaf6   /* Main text */
--tp-text-secondary: #9ea3b8   /* Labels, subtitles */
--tp-text-muted:     #5c6380   /* Disabled, placeholders */
```

### Borders
```
--tp-border:       #2e3450   /* Default border */
--tp-border-hover: #4a5280   /* Hover border */
```

---

## Typography

```
Font stack (sans):  'Inter', system-ui, -apple-system, sans-serif
Font stack (mono):  'JetBrains Mono', 'Fira Code', 'Consolas', monospace

Base size:    14px
Line height:  1.5

Heading lg:   20px, weight 600
Heading md:   16px, weight 600
Heading sm:   13px, weight 600, uppercase, letter-spacing 0.04em
Body:         14px, weight 400
Small:        12px
Tiny:         11px
```

---

## Spacing

```
4px   — tight (icon gap)
8px   — inner padding (badge, chip)
12px  — small gap (card internals)
16px  — card padding, section gap
20px  — main content padding
24px  — page-level horizontal gutter
32px  — large section separation
```

---

## Borders & Radius

```
--tp-radius-sm:  4px   /* Buttons, inputs, chips */
--tp-radius-md:  8px   /* Tooltips, dropdowns */
--tp-radius-lg:  12px  /* Cards, panels */
--tp-radius-xl:  16px  /* Full panels */

--tp-shadow:     0 2px 12px rgba(0, 0, 0, 0.4)
--tp-shadow-lg:  0 8px 32px rgba(0, 0, 0, 0.6)
```

---

## Layout Patterns

### App Shell
```
+------------------------------------------+
| HEADER  56px  [Logo] [Title] [Badge]      |
+------------------------------------------+
| MAIN CONTENT (flex/grid, padding 20/24)  |
|  +--------+  +-----------+  +----------+ |
|  | Card   |  | Card      |  | Card     | |
|  |        |  |           |  |          | |
|  +--------+  +-----------+  +----------+ |
+------------------------------------------+
```

- Header: `height: 56px`, `background: var(--tp-bg-secondary)`, bottom border
- Main: `padding: 20px 24px`, `gap: 16px`, CSS grid or flex wrap
- Cards: `background: var(--tp-bg-card)`, border, `border-radius: var(--tp-radius-lg)`

### Multi-panel Dashboard
Use CSS Grid with named areas:
```css
.tp-main {
  display: grid;
  grid-template-columns: 1fr 1fr 1fr;
  grid-template-rows: 200px 1fr;
  gap: 16px;
}
```

---

## Components

### Card
```html
<div class="tp-card">
  <div class="tp-card-title">THROUGHPUT</div>
  <!-- content -->
</div>
```

### Status Badge (live indicator)
```html
<!-- Connected -->
<span class="tp-status">
  <span class="tp-status-dot connected"></span>
  LIVE
</span>

<!-- Error -->
<span class="tp-status">
  <span class="tp-status-dot error"></span>
  DISCONNECTED
</span>
```

### "LIVE" badge in header
```html
<span class="tp-header-badge">LIVE</span>
```
Style: `background: #7c6af7`, `color: white`, `border-radius: 99px`, `padding: 2px 8px`, `font-size: 11px`

### Error panel
```html
<div class="tp-error">
  Connection failed: HTTP 404 — stream not found
</div>
```

---

## Vistral Integration

Chart containers should have:
```css
.chart-container {
  background: var(--tp-bg-card);
  border: 1px solid var(--tp-border);
  border-radius: var(--tp-radius-lg);
  padding: 16px;
  min-height: 250px;
}
```

Always pass `theme="dark"` to Vistral components.

Use the **Midnight** palette for brand consistency (`findPaletteByLabel('Midnight')`).

---

## Do / Don't

| DO | DON'T |
|----|-------|
| Dark background `#0f1117` | White or light backgrounds |
| Purple `#7c6af7` for primary actions | Bright red or green for primary |
| Monospace for numbers and data | Proportional font for values |
| Show live/disconnected status | Hide connection state |
| CSS variables for all colors | Hard-coded hex values |
| Dense information layout | Excessive padding / decorative space |
| `border-radius: 12px` for cards | Sharp corners or over-rounded pills |
| `font-weight: 600` for labels | Bold body text |


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: clean (LOW)