From 4094654a5b380eff9ca6a2c29aec692ae1263bf9 Mon Sep 17 00:00:00 2001 From: fullsizemalt <106900403+fullsizemalt@users.noreply.github.com> Date: Wed, 7 Jan 2026 22:29:50 -0800 Subject: [PATCH] chore: add Claude Code configuration and sensor updates --- .claude/claude.md | 172 ++++++++++++++++++++++++++++++++ .claude/commands/deploy-edge.md | 42 ++++++++ src/kasa.ts | 24 +++-- src/sensorpush.ts | 25 ++--- 4 files changed, 235 insertions(+), 28 deletions(-) create mode 100644 .claude/claude.md create mode 100644 .claude/commands/deploy-edge.md diff --git a/.claude/claude.md b/.claude/claude.md new file mode 100644 index 0000000..4cc3760 --- /dev/null +++ b/.claude/claude.md @@ -0,0 +1,172 @@ +# Project Overview + +**Name**: Veridian Edge Agent +**Type**: Edge computing service (sensor data sync) +**Primary Languages**: TypeScript (Bun runtime) +**Status**: v1.0.0, Production + +## High-Level Description + +Veridian Edge is a SensorPush integration agent that runs on Raspberry Pi or Ubuntu. It polls the SensorPush Cloud API for environmental readings and syncs them to the Veridian backend API. + +**Architecture:** +``` +SensorPush Cloud API → Veridian Edge Agent → Veridian Backend API + ↓ + SQLite (offline buffer) +``` + +**External Systems:** +- SensorPush Cloud API (OAuth 2.0, automatic token refresh) +- Veridian Backend API (HTTP/JSON webhooks) +- Local SQLite database (offline resilience) +- Systemd service management on Linux + +## Multi-Repo Architecture + +This repo works in conjunction with **veridian** as a single codebase: +- **veridian-edge** (this repo): Edge agent that polls SensorPush and syncs to backend +- **veridian**: Main platform backend that receives data from edge agent + +When making changes, consider: +- API contract: Edge agent must send data in format expected by Veridian backend +- Authentication: API key authentication with Veridian backend +- Error handling: Network failures must be buffered to SQLite + +## Layout + +``` +veridian-edge/ +├── src/ # Source code +│ ├── index.ts # Main entry point +│ ├── sensorpush.ts # SensorPush API client +│ ├── sync.ts # Sync logic to Veridian backend +│ └── db.ts # SQLite database operations +├── scripts/ # Installation and maintenance scripts +│ └── install.sh # Systemd service installer +├── config.json # Configuration (after installation) +└── package.json # Bun dependencies +``` + +## Conventions + +### Language/Style +- **TypeScript strict mode** enabled +- **Bun runtime** (not Node.js) +- **Hono framework** for HTTP endpoints +- **Better-sqlite3** for local database +- **Systemd service** for process management + +### Package Manager +- **Use `bun` exclusively** (this is a Bun project) +- Commands: `bun install`, `bun run`, `bun test`, `bun add` + +### Error Handling +- Graceful degradation: Network failures buffer to SQLite +- Retry logic with exponential backoff for API calls +- Clear error messages in logs +- Never expose credentials in logs + +### Logging +- Structured logging to systemd journal +- Log levels: error, warn, info, debug +- Include timestamp, severity, and context + +### Config +- Configuration file: `/opt/veridian-edge/config.json` +- Sensitive data: email, password, API keys +- Use `config.demo.json` or `config.example.json` as templates + +## Patterns & Playbooks + +### How to Add New Sensor Types +1. Update SensorPush API client to handle new sensor data +2. Add database schema migration if needed +3. Update sync logic to include new data type in API payload +4. Add tests for new sensor type +5. Update configuration schema + +### How to Modify API Payload Format +**WARNING**: This affects the Veridian backend integration. +1. Check Veridian backend API contract in `veridian/` repo +2. Update this repo's sync logic +3. Coordinate changes with Veridian backend deployment +4. Test with actual Veridian backend before deploying +5. Consider backward compatibility during transition + +### How to Run Locally (Development) +```bash +bun install +cp config.demo.json config.json +# Edit config.json with your credentials +bun run dev +``` + +### How to Run in Production (Raspberry Pi/Ubuntu) +```bash +# Run installer (sets up systemd service) +sudo ./scripts/install.sh + +# Service management +sudo systemctl start veridian-edge +sudo systemctl status veridian-edge +sudo systemctl stop veridian-edge +journalctl -u veridian-edge -f +``` + +### Important Configuration Fields +- `facilityId`: UUID of facility in Veridian backend +- `backendUrl`: Veridian backend API URL +- `backendApiKey`: API key for Veridian authentication +- `sensorpush.email`: SensorPush account email +- `sensorpush.password`: SensorPush account password +- `sensorMappings`: Array mapping sensor IDs to room IDs + +## PR & Learning Workflow + +- When a PR introduces a new pattern or fixes a subtle issue: + 1. Summarize the lesson in 1-2 bullets + 2. Append under "Patterns & Playbooks" above + 3. Consider updating README.md if user-facing + +## Testing Strategy + +- **Unit tests**: Bun test for pure functions +- **Integration tests**: Mock SensorPush API and Veridian backend +- **VPS verification**: Deploy to edge device and verify: + - Service is running: `systemctl status veridian-edge` + - Logs show successful sync: `journalctl -u veridian-edge -f` + - Data appears in Veridian backend + +## Deployment Considerations + +- **Target devices**: Raspberry Pi 4 (2GB+ RAM) or Ubuntu 22.04+ +- **Installer script**: `scripts/install.sh` handles systemd setup +- **Service runs as**: Dedicated user (created by installer) +- **Offline resilience**: SQLite buffer survives network outages +- **Health monitoring**: HTTP endpoint `http://localhost:3030/health` +- **Prometheus metrics**: Available at `http://localhost:3030/metrics` + +## Integration with Veridian Backend + +**Webhook/POST endpoint format** (must match Veridian backend expectations): +``` +POST /api/v1/sensor-readings +Authorization: Bearer {backendApiKey} +Content-Type: application/json + +{ + "facilityId": "uuid", + "readings": [ + { + "sensorId": "string", + "roomId": "uuid", + "timestamp": "ISO8601", + "temperature": number, + "humidity": number + } + ] +} +``` + +Before changing this format, ALWAYS check the Veridian backend API specification. diff --git a/.claude/commands/deploy-edge.md b/.claude/commands/deploy-edge.md new file mode 100644 index 0000000..3adc1f6 --- /dev/null +++ b/.claude/commands/deploy-edge.md @@ -0,0 +1,42 @@ +You are the edge device deployment assistant for Veridian Edge. + +## Edge Device Information +- Target: Raspberry Pi or Ubuntu device +- Service: veridian-edge (systemd) +- Default deployment path: /opt/veridian-edge + +## Deployment Process + +1. **Pre-deployment checks**: + - `git status` to check for uncommitted changes + - `git log -1 --oneline` to show what will be deployed + - Run tests: `bun test` + +2. **Deploy to edge device** (replace EDGE_IP with actual device IP): + ```bash + # Copy files to edge device + scp -r ./* admin@EDGE_IP:/tmp/veridian-edge/ + + # SSH to device and deploy + ssh admin@EDGE_IP << 'EOF' + cd /opt/veridian-edge + systemctl stop veridian-edge + cp -r /tmp/veridian-edge/* . + bun install --production + systemctl start veridian-edge + EOF + ``` + +3. **Post-deployment verification**: + - Check service status: `ssh admin@EDGE_IP 'systemctl status veridian-edge'` + - View logs: `ssh admin@EDGE_IP 'journalctl -u veridian-edge -n 50'` + - Health check: `ssh admin@EDGE_IP 'curl http://localhost:3030/health'` + +## Output + +Report: +- What was deployed (commit hash, message) +- Service status (running/not running) +- Any errors or warnings in logs +- Sync status (is it sending data to Veridian backend?) +- Suggested manual verification steps diff --git a/src/kasa.ts b/src/kasa.ts index 47bd23c..8e7a3db 100644 --- a/src/kasa.ts +++ b/src/kasa.ts @@ -12,28 +12,30 @@ export class KasaController { /** * Discover the Kasa device on local network. - * Prioritizes EP10 model as requested. + * Filters for device named "Veridian test plug" or similar. */ async discover(): Promise { // Prevent spamming discovery - if (Date.now() - this.lastDiscovery < 10000 && !this.deviceIp) { - return null; + if (Date.now() - this.lastDiscovery < 10000 && this.deviceIp) { + return this.deviceIp; } this.lastDiscovery = Date.now(); return new Promise((resolve) => { - console.log('🔌 Kasa: Starting discovery...'); + console.log('🔌 Kasa: Starting discovery (looking for "Veridian" device)...'); let found = false; this.client.startDiscovery({ discoveryInterval: 500, - discoveryTimeout: 2000 + discoveryTimeout: 3000 }).on('device-new', (device) => { + const alias = device.alias?.toLowerCase() || ''; const model = device.model; - console.log(`🔌 Kasa: Found device ${device.alias} (${model}) at ${device.host}`); + console.log(`🔌 Kasa: Found device "${device.alias}" (${model}) at ${device.host}`); - // If we found an EP10, grab it immediately - if (model.toLowerCase().includes('ep10')) { + // Filter: Only accept devices with "veridian" in the name + if (alias.includes('veridian')) { + console.log(`✅ Kasa: Matched target device "${device.alias}"`); this.deviceIp = device.host; found = true; this.client.stopDiscovery(); @@ -41,15 +43,15 @@ export class KasaController { } }); - // Timeout after 3s + // Timeout after 4s setTimeout(() => { this.client.stopDiscovery(); if (!found) { if (this.deviceIp) console.log(`🔌 Kasa: Using previously known IP: ${this.deviceIp}`); - else console.warn('🔌 Kasa: Discovery timed out, no device found'); + else console.warn('🔌 Kasa: No device named "Veridian" found'); resolve(this.deviceIp); } - }, 3000); + }, 4000); }); } diff --git a/src/sensorpush.ts b/src/sensorpush.ts index c2ee847..e902117 100644 --- a/src/sensorpush.ts +++ b/src/sensorpush.ts @@ -60,12 +60,13 @@ export class SensorPushClient { }); if (!authRes.ok) { - // DEGRADE GRACEFULLY: If auth fails with 400/403 (likely invalid/missing creds), switch to Mock Mode + // DEGRADE GRACEFULLY: If auth fails, switch to SKIP MODE (no sensor polling) + // Edge Agent will still run for heartbeats and Kasa control if (authRes.status === 400 || authRes.status === 403 || authRes.status === 401) { - console.warn('⚠️ SensorPush auth failed. Switching to DEMO MODE (Mock Data Generator).'); + console.warn('⚠️ SensorPush auth failed. SKIPPING sensor polling (Pulse sensors are primary).'); this.tokens = { - accessToken: 'MOCK_TOKEN', - authorization: 'MOCK_AUTH', + accessToken: 'SKIP_MODE', + authorization: 'SKIP_AUTH', expiresAt: Date.now() + 24 * 60 * 60 * 1000 // 24h }; return; @@ -106,19 +107,9 @@ export class SensorPushClient { async getSamples(minutes: number = 5): Promise { await this.ensureAuthenticated(); - // Check for Demo Mode - if (this.tokens?.accessToken === 'MOCK_TOKEN') { - const now = Date.now(); - const sineWave = Math.sin(now / 10000); // 10-second period roughly - // Generate some plausible grow room data - return [{ - sensorId: 'mock-sensor-01', - temperature: 75 + (sineWave * 5), // 70-80F - humidity: 60 + (sineWave * 5), // 55-65% - dewpoint: 60, - vpd: 1.2 + (sineWave * 0.2), // 1.0-1.4 kPa - observed: new Date().toISOString() - }]; + // Check for Skip Mode - no sensor polling, Pulse is primary + if (this.tokens?.accessToken === 'SKIP_MODE') { + return []; // No SensorPush data - Pulse handles sensors } // First get list of sensors