chore: add Claude Code configuration and sensor updates

This commit is contained in:
fullsizemalt 2026-01-07 22:29:50 -08:00
parent 3847d2cf26
commit 4094654a5b
4 changed files with 235 additions and 28 deletions

172
.claude/claude.md Normal file
View file

@ -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.

View file

@ -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

View file

@ -12,28 +12,30 @@ export class KasaController {
/** /**
* Discover the Kasa device on local network. * Discover the Kasa device on local network.
* Prioritizes EP10 model as requested. * Filters for device named "Veridian test plug" or similar.
*/ */
async discover(): Promise<string | null> { async discover(): Promise<string | null> {
// Prevent spamming discovery // Prevent spamming discovery
if (Date.now() - this.lastDiscovery < 10000 && !this.deviceIp) { if (Date.now() - this.lastDiscovery < 10000 && this.deviceIp) {
return null; return this.deviceIp;
} }
this.lastDiscovery = Date.now(); this.lastDiscovery = Date.now();
return new Promise((resolve) => { return new Promise((resolve) => {
console.log('🔌 Kasa: Starting discovery...'); console.log('🔌 Kasa: Starting discovery (looking for "Veridian" device)...');
let found = false; let found = false;
this.client.startDiscovery({ this.client.startDiscovery({
discoveryInterval: 500, discoveryInterval: 500,
discoveryTimeout: 2000 discoveryTimeout: 3000
}).on('device-new', (device) => { }).on('device-new', (device) => {
const alias = device.alias?.toLowerCase() || '';
const model = device.model; 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 // Filter: Only accept devices with "veridian" in the name
if (model.toLowerCase().includes('ep10')) { if (alias.includes('veridian')) {
console.log(`✅ Kasa: Matched target device "${device.alias}"`);
this.deviceIp = device.host; this.deviceIp = device.host;
found = true; found = true;
this.client.stopDiscovery(); this.client.stopDiscovery();
@ -41,15 +43,15 @@ export class KasaController {
} }
}); });
// Timeout after 3s // Timeout after 4s
setTimeout(() => { setTimeout(() => {
this.client.stopDiscovery(); this.client.stopDiscovery();
if (!found) { if (!found) {
if (this.deviceIp) console.log(`🔌 Kasa: Using previously known IP: ${this.deviceIp}`); 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); resolve(this.deviceIp);
} }
}, 3000); }, 4000);
}); });
} }

View file

@ -60,12 +60,13 @@ export class SensorPushClient {
}); });
if (!authRes.ok) { 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) { 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 = { this.tokens = {
accessToken: 'MOCK_TOKEN', accessToken: 'SKIP_MODE',
authorization: 'MOCK_AUTH', authorization: 'SKIP_AUTH',
expiresAt: Date.now() + 24 * 60 * 60 * 1000 // 24h expiresAt: Date.now() + 24 * 60 * 60 * 1000 // 24h
}; };
return; return;
@ -106,19 +107,9 @@ export class SensorPushClient {
async getSamples(minutes: number = 5): Promise<SensorReading[]> { async getSamples(minutes: number = 5): Promise<SensorReading[]> {
await this.ensureAuthenticated(); await this.ensureAuthenticated();
// Check for Demo Mode // Check for Skip Mode - no sensor polling, Pulse is primary
if (this.tokens?.accessToken === 'MOCK_TOKEN') { if (this.tokens?.accessToken === 'SKIP_MODE') {
const now = Date.now(); return []; // No SensorPush data - Pulse handles sensors
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()
}];
} }
// First get list of sensors // First get list of sensors