feat(farm-bridge): replace 3-job aggregator with single clean Dropbox→Claude→WhatsApp bridge

Strips out AG-Refine API, TomTom, Express server, and node-cron scheduling.
Single bridge.js (~100 lines) reads FeedLync/DairyComp files from Dropbox,
generates a Haiku digest, sends to WhatsApp, and saves back to /Farm/Summaries.
One daily Render free cron job instead of three.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KBD2dN2KEjzz3UQFa9hEpu
This commit is contained in:
Claude 2026-06-28 21:32:20 +00:00
parent 7837571535
commit 0858dbc8f4
No known key found for this signature in database
4 changed files with 175 additions and 0 deletions

2
farm-bridge/.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
.env
node_modules/

133
farm-bridge/bridge.js Normal file
View file

@ -0,0 +1,133 @@
/**
* Farm Bridge Dropbox Claude WhatsApp
* Reads FeedLync and DairyComp files from Dropbox, generates a daily digest
* via Claude Haiku, sends to WhatsApp, and saves back to Dropbox.
*
* Run: node bridge.js
* Env: copy .env.example .env and fill in credentials
*/
import 'dotenv/config';
import Anthropic from '@anthropic-ai/sdk';
import { Dropbox } from 'dropbox';
const FEEDLYNC = process.env.DROPBOX_FEEDLYNC_PATH ?? '/Farm/FeedLync';
const DAIRYCOMP = process.env.DROPBOX_DAIRYCOMP_PATH ?? '/Farm/DairyComp';
const OUTBOX = process.env.DROPBOX_OUTBOX_PATH ?? '/Farm/Summaries';
const dbx = new Dropbox({
clientId: process.env.DROPBOX_APP_KEY,
clientSecret: process.env.DROPBOX_APP_SECRET,
refreshToken: process.env.DROPBOX_REFRESH_TOKEN,
});
const ai = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });
// ── Dropbox helpers ────────────────────────────────────────────────────────
async function readFolder(folder, maxFiles = 5) {
let list;
try {
list = await dbx.filesListFolder({ path: folder });
} catch (e) {
console.warn(`⚠ Could not read ${folder}: ${e.message}`);
return '';
}
const files = list.result.entries
.filter(e => e['.tag'] === 'file')
.sort((a, b) => new Date(b.client_modified) - new Date(a.client_modified))
.slice(0, maxFiles);
if (!files.length) return '';
const parts = await Promise.allSettled(
files.map(async f => {
const dl = await dbx.filesDownload({ path: f.path_lower });
return `--- ${f.name} ---\n${dl.result.fileBinary.toString('utf8')}`;
})
);
return parts
.filter(r => r.status === 'fulfilled')
.map(r => r.value)
.join('\n\n');
}
// ── WhatsApp helper ────────────────────────────────────────────────────────
async function sendWhatsApp(text) {
const token = process.env.WHATSAPP_ACCESS_TOKEN;
const phoneId = process.env.WHATSAPP_PHONE_ID;
const recipient = process.env.WHATSAPP_RECIPIENT;
if (!token || !phoneId || !recipient) {
console.log('WhatsApp not configured — skipping send.');
return;
}
const res = await fetch(`https://graph.facebook.com/v19.0/${phoneId}/messages`, {
method: 'POST',
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
body: JSON.stringify({
messaging_product: 'whatsapp',
to: recipient,
type: 'text',
text: { body: text.slice(0, 4096) },
}),
});
if (!res.ok) console.error('WhatsApp error:', await res.text());
else console.log('✅ WhatsApp sent');
}
// ── Main ───────────────────────────────────────────────────────────────────
async function run() {
console.log(`\n── Farm Bridge ${new Date().toISOString()} ─────────────────────`);
const [feedlync, dairycomp] = await Promise.all([
readFolder(FEEDLYNC),
readFolder(DAIRYCOMP),
]);
const sections = [
feedlync && `FeedLync TMR loads:\n${feedlync}`,
dairycomp && `DairyComp herd log:\n${dairycomp}`,
].filter(Boolean);
if (!sections.length) {
console.log('No files found in Dropbox. Check DROPBOX_FEEDLYNC_PATH / DROPBOX_DAIRYCOMP_PATH.');
return;
}
console.log(`Processing ${sections.length} source(s)…`);
const msg = await ai.messages.create({
model: 'claude-haiku-4-5-20251001',
max_tokens: 1024,
system: `You are a farm AI analyst for Drumgoon Dairy.
Analyse FeedLync TMR load records and DairyComp herd logs and produce a clear daily digest.
Rules:
- Be specific: cite actual numbers, feedplan names, operators, error percentages, costs.
- Flag load errors: >5% note, >10% flag, >20% urgent.
- Lead with what matters most. Farmers are busy keep it tight.
FeedLync columns: Start Time, Finish Time, Feedplan/Ingredient, Operator, Device, Planned Quantity, Quantity (mixed), Unloaded Quantity, Error (%), Cost`,
messages: [{ role: 'user', content: sections.join('\n\n---\n\n') }],
});
const summary = msg.content[0].text;
console.log('\n' + summary + '\n');
const date = new Date().toISOString().slice(0, 10);
const outPath = `${OUTBOX}/digest_${date}.txt`;
const [, uploadResult] = await Promise.allSettled([
sendWhatsApp(summary),
dbx.filesUpload({ path: outPath, contents: summary, mode: { '.tag': 'overwrite' } }),
]);
if (uploadResult.status === 'fulfilled') console.log(`✅ Saved → ${outPath}`);
else console.error('Dropbox upload failed:', uploadResult.reason?.message);
}
run().catch(e => { console.error('Fatal:', e.message); process.exit(1); });

10
farm-bridge/package.json Normal file
View file

@ -0,0 +1,10 @@
{
"name": "farm-bridge",
"version": "1.0.0",
"type": "module",
"dependencies": {
"@anthropic-ai/sdk": "^0.52.0",
"dotenv": "^16.4.5",
"dropbox": "^10.34.0"
}
}

30
farm-bridge/render.yaml Normal file
View file

@ -0,0 +1,30 @@
services:
- type: cron
name: farm-bridge
runtime: node
rootDir: farm-bridge
buildCommand: npm install
startCommand: node bridge.js
schedule: "0 6 * * *"
plan: free
envVars:
- key: ANTHROPIC_API_KEY
sync: false
- key: DROPBOX_APP_KEY
sync: false
- key: DROPBOX_APP_SECRET
sync: false
- key: DROPBOX_REFRESH_TOKEN
sync: false
- key: DROPBOX_FEEDLYNC_PATH
value: /Farm/FeedLync
- key: DROPBOX_DAIRYCOMP_PATH
value: /Farm/DairyComp
- key: DROPBOX_OUTBOX_PATH
value: /Farm/Summaries
- key: WHATSAPP_ACCESS_TOKEN
sync: false
- key: WHATSAPP_PHONE_ID
sync: false
- key: WHATSAPP_RECIPIENT
sync: false