From d5575b58e3210d99d4d9f30aa5d9c40d48891b4c Mon Sep 17 00:00:00 2001 From: Horus AI Date: Thu, 19 Mar 2026 17:38:12 +0100 Subject: [PATCH] SiteMente - AI-Powered Lead Generation Platform Features: - Mission Control dashboard - HP Submissions tracking - AI Agents integration - Lead management CRM - Marketing email templates - Chrome extension support Tech: Next.js, TypeScript, Tailwind CSS, MySQL --- .gitignore | 35 +- app/api/chat/route.ts | 35 ++ app/api/hookd/route.ts | 118 +++++ app/api/hp-submissions/route.ts | 30 ++ app/api/mission-control/hp-leads/route.ts | 74 +++ app/api/research/route.ts | 64 +++ app/components/VoiceWidget.tsx | 187 ++++++++ app/demos/voice/page.tsx | 29 ++ app/extensions/hookd/page.tsx | 86 ++++ app/mission-control/hp-leads/page.tsx | 27 ++ app/mission-control/hp-submissions/page.tsx | 72 +++ app/mission-control/leads/hp-leads/page.tsx | 219 +++++++++ app/mission-control/research/page.tsx | 226 +++++++++ .../MissionControlDashboard.tsx | 59 +++ data/eod_briefs.json | 227 ++++++++- data/execution-logs.json | 18 + data/leads.json | 267 +++++++++++ data/morning_briefs.json | 451 +++++++++++++++++- data/recurring-templates.json | 9 +- data/research.json | 50 ++ .../2026-03-18-vip-standard-email.html | 81 ++++ public/extensions/hookd.zip | Bin 0 -> 8811 bytes public/extensions/hookd/README.md | 78 +++ public/extensions/hookd/background.js | 159 ++++++ public/extensions/hookd/content.css | 28 ++ public/extensions/hookd/content.js | 190 ++++++++ public/extensions/hookd/icons/icon128.png | Bin 0 -> 552 bytes public/extensions/hookd/icons/icon16.png | Bin 0 -> 121 bytes public/extensions/hookd/icons/icon48.png | Bin 0 -> 272 bytes public/extensions/hookd/manifest.json | 11 + public/extensions/hookd/popup.html | 73 +++ public/extensions/hookd/popup.js | 180 +++++++ public/hookd-107.zip | Bin 0 -> 9564 bytes public/hookd.zip | Bin 0 -> 9259 bytes 34 files changed, 3061 insertions(+), 22 deletions(-) create mode 100644 app/api/chat/route.ts create mode 100644 app/api/hookd/route.ts create mode 100644 app/api/hp-submissions/route.ts create mode 100644 app/api/mission-control/hp-leads/route.ts create mode 100644 app/api/research/route.ts create mode 100644 app/components/VoiceWidget.tsx create mode 100644 app/demos/voice/page.tsx create mode 100644 app/extensions/hookd/page.tsx create mode 100644 app/mission-control/hp-leads/page.tsx create mode 100644 app/mission-control/hp-submissions/page.tsx create mode 100644 app/mission-control/leads/hp-leads/page.tsx create mode 100644 app/mission-control/research/page.tsx create mode 100644 data/leads.json create mode 100644 data/research.json create mode 100644 leads/marketing-emails/2026-03-18-vip-standard-email.html create mode 100644 public/extensions/hookd.zip create mode 100644 public/extensions/hookd/README.md create mode 100644 public/extensions/hookd/background.js create mode 100644 public/extensions/hookd/content.css create mode 100644 public/extensions/hookd/content.js create mode 100644 public/extensions/hookd/icons/icon128.png create mode 100644 public/extensions/hookd/icons/icon16.png create mode 100644 public/extensions/hookd/icons/icon48.png create mode 100644 public/extensions/hookd/manifest.json create mode 100644 public/extensions/hookd/popup.html create mode 100644 public/extensions/hookd/popup.js create mode 100644 public/hookd-107.zip create mode 100644 public/hookd.zip diff --git a/.gitignore b/.gitignore index 54d7c2a..aed9203 100644 --- a/.gitignore +++ b/.gitignore @@ -1,31 +1,34 @@ # Dependencies node_modules/ -# Next.js -.next/ -out/ - # Environment .env .env.local -.env.development.local -.env.test.local -.env.production.local +.env.*.local -# Vite +# Logs +logs/ +*.log +npm-debug.log* + +# Build +.next/ +out/ +build/ dist/ -# Debug -npm-debug.log* -yarn-debug.log* -yarn-error.log* +# System +.DS_Store +Thumbs.db # IDE -.idea/ .vscode/ +.idea/ *.swp *.swo -# OS -.DS_Store -Thumbs.db +# Secrets - NEVER COMMIT +*.pem +*.key +credentials.json +secrets.json diff --git a/app/api/chat/route.ts b/app/api/chat/route.ts new file mode 100644 index 0000000..8758425 --- /dev/null +++ b/app/api/chat/route.ts @@ -0,0 +1,35 @@ +import { NextResponse } from 'next/server'; + +export async function POST(request: Request) { + try { + const body = await request.json(); + const { text } = body; + + const prompt = `You are Cleopatra, a professional Spanish-speaking sales agent. Keep responses SHORT (1-2 sentences), friendly, in Spanish. + +User: ${text} + +Response in Spanish:`; + + const res = await fetch("http://127.0.0.1:11434/api/generate", { + method: "POST", + headers: { "Content-Type": "application/json" }, + signal: AbortSignal.timeout(60000), + body: JSON.stringify({ + model: "phi3:mini", + prompt: prompt, + stream: false + }) + }); + + const data = await res.json(); + const reply = (data.response || "¡Hola! Estoy aquí.").substring(0, 200); + + return NextResponse.json({ response: reply }); + } catch (e) { + console.error(e); + return NextResponse.json({ + response: "Tengo problemas para conectar. Intenta de nuevo." + }, { status: 500 }); + } +} diff --git a/app/api/hookd/route.ts b/app/api/hookd/route.ts new file mode 100644 index 0000000..e9a2471 --- /dev/null +++ b/app/api/hookd/route.ts @@ -0,0 +1,118 @@ +import { NextResponse } from 'next/server' +import fs from 'fs' +import path from 'path' + +export const dynamic = 'force-dynamic' + +// Get all Hookd items +export async function GET(request: Request) { + const url = new URL(request.url) + const action = url.searchParams.get('action') || 'list' + + try { + const possiblePaths = [ + path.join(process.cwd(), '..', 'chrome-extensions', 'hookd', 'data.json'), + '/var/www/hostpioneers.com/public_html/contacts.json', + ] + + let data: any[] = [] + + // Try to read from Hookd storage + for (const filePath of possiblePaths) { + if (fs.existsSync(filePath)) { + const content = fs.readFileSync(filePath, 'utf-8') + data = JSON.parse(content) + break + } + } + + if (action === 'stats') { + return NextResponse.json({ + total: data.length, + byCategory: data.reduce((acc: any, item) => { + const cat = item.category || 'Uncategorized' + acc[cat] = (acc[cat] || 0) + 1 + return acc + }, {}), + aiProcessed: data.filter((i: any) => i.aiProcessed).length, + saved: data.filter((i: any) => i.saved).length, + recent: data.slice(0, 10) + }) + } + + return NextResponse.json({ items: data }) + } catch (error) { + console.error('Hookd API error:', error) + return NextResponse.json({ items: [], error: 'Failed to fetch' }, { status: 200 }) + } +} + +// Process items with AI +export async function POST(request: Request) { + try { + const body = await request.json() + const { items, instruction } = body + + // This would call MiniMax API to process items + // For now, return mock AI analysis + + const results = items.map((item: any) => ({ + id: item.id, + category: guessCategory(item), + score: guessScore(item), + tags: guessTags(item), + summary: item.content?.slice(0, 100) + '...' + })) + + return NextResponse.json({ results }) + } catch (error) { + console.error('AI processing error:', error) + return NextResponse.json({ error: 'Processing failed' }, { status: 500 }) + } +} + +function guessCategory(item: any): string { + const content = (item.content || '').toLowerCase() + const link = (item.link || '').toLowerCase() + + if (content.includes('ai') || content.includes('gpt') || content.includes('claude') || + content.includes('openai') || content.includes('anthropic') || + link.includes('github.com') || link.includes('huggingface')) { + return 'AI' + } + if (content.includes('news') || content.includes('breaking') || content.includes('just in')) { + return 'News' + } + if (content.includes('idea') || content.includes('thought') || content.includes('what if')) { + return 'Ideas' + } + if (content.includes('lol') || content.includes('meme') || content.includes('funny') || content.includes('😂')) { + return 'Memes' + } + return 'Other' +} + +function guessScore(item: any): number { + const content = (item.content || '').toLowerCase() + let score = 5 + + if (item.link) score += 1 + if (content.length > 100) score += 1 + if (content.includes('github')) score += 1 + if (content.includes('ai') || content.includes('gpt')) score += 2 + + return Math.min(10, score) +} + +function guessTags(item: any): string[] { + const tags: string[] = [] + const content = (item.content || '').toLowerCase() + + if (content.includes('ai')) tags.push('AI') + if (content.includes('tool')) tags.push('Tools') + if (content.includes('news')) tags.push('News') + if (content.includes('tutorial')) tags.push('Tutorial') + if (content.includes('github')) tags.push('Code') + + return tags +} diff --git a/app/api/hp-submissions/route.ts b/app/api/hp-submissions/route.ts new file mode 100644 index 0000000..90ed684 --- /dev/null +++ b/app/api/hp-submissions/route.ts @@ -0,0 +1,30 @@ +import { NextResponse } from 'next/server' +import fs from 'fs' +import path from 'path' + +export const dynamic = 'force-dynamic' + +export async function GET() { + try { + // Try multiple possible paths + const possiblePaths = [ + path.join(process.cwd(), '..', 'hostpioneers.com', 'public_html', 'contacts.json'), + path.join(process.cwd(), 'public', '..', 'contacts.json'), + '/var/www/hostpioneers.com/public_html/contacts.json', + ] + + let data = '[]' + for (const filePath of possiblePaths) { + if (fs.existsSync(filePath)) { + data = fs.readFileSync(filePath, 'utf-8') + break + } + } + + const submissions = JSON.parse(data || '[]') + return NextResponse.json({ submissions: submissions.reverse() }) + } catch (error) { + console.error('Error:', error) + return NextResponse.json({ submissions: [], error: 'Failed to fetch' }, { status: 200 }) + } +} diff --git a/app/api/mission-control/hp-leads/route.ts b/app/api/mission-control/hp-leads/route.ts new file mode 100644 index 0000000..3ca7150 --- /dev/null +++ b/app/api/mission-control/hp-leads/route.ts @@ -0,0 +1,74 @@ +import { NextResponse } from 'next/server' +import mysql from 'mysql2/promise' + +export const dynamic = 'force-dynamic' + +async function getPool() { + return mysql.createPool({ + host: 'localhost', + user: 'sitemente', + password: 'pa0ndoQRPDYBHzTXX3rbWd6E', + database: 'sitemente', + waitForConnections: true, + connectionLimit: 10 + }) +} + +export async function GET(request: Request) { + const { searchParams } = new URL(request.url) + const segment = searchParams.get('segment') + const dormancy = searchParams.get('dormancy') + const search = searchParams.get('search') + const sortBy = searchParams.get('sortBy') || 'total_spent' + const sortOrder = searchParams.get('sortOrder') || 'DESC' + const page = parseInt(searchParams.get('page') || '1') + const limit = parseInt(searchParams.get('limit') || '100') + const offset = (page - 1) * limit + + // Validate sortBy to prevent SQL injection + const allowedSortColumns = ['firstname', 'lastname', 'email', 'company', 'country', 'total_spent', 'segment', 'dormancy_flag', 'id'] + const safeSortBy = allowedSortColumns.includes(sortBy) ? sortBy : 'total_spent' + const safeSortOrder = sortOrder === 'ASC' ? 'ASC' : 'DESC' + + try { + const pool = await getPool() + + let countQuery = 'SELECT COUNT(*) as total FROM hostpioneers_leads WHERE 1=1' + let query = 'SELECT * FROM hostpioneers_leads WHERE 1=1' + const params: any[] = [] + + if (segment && segment !== 'all') { + countQuery += ' AND segment = ?' + query += ' AND segment = ?' + params.push(segment) + } + if (dormancy && dormancy !== 'all') { + countQuery += ' AND dormancy_flag = ?' + query += ' AND dormancy_flag = ?' + params.push(dormancy) + } + if (search) { + const searchClause = ' AND (firstname LIKE ? OR lastname LIKE ? OR email LIKE ? OR company LIKE ? OR phone LIKE ? OR country LIKE ?)' + countQuery += searchClause + query += searchClause + const searchTerm = `%${search}%` + params.push(searchTerm, searchTerm, searchTerm, searchTerm, searchTerm, searchTerm) + } + + const [countResult] = await pool.query(countQuery, params) + const total = (countResult as any[])[0].total + + query += ` ORDER BY ${safeSortBy} ${safeSortOrder} LIMIT ? OFFSET ?` + const [rows] = await pool.query(query, [...params, limit, offset]) + + await pool.end() + + return NextResponse.json({ + leads: rows, + pagination: { page, limit, total, totalPages: Math.ceil(total / limit) } + }) + } catch (error) { + console.error('Error:', error) + return NextResponse.json({ error: 'Failed to fetch' }, { status: 500 }) + } +} diff --git a/app/api/research/route.ts b/app/api/research/route.ts new file mode 100644 index 0000000..3bbfc4a --- /dev/null +++ b/app/api/research/route.ts @@ -0,0 +1,64 @@ +import { NextResponse } from 'next/server'; +import mysql from 'mysql2/promise'; + +const dbConfig = { + host: process.env.DB_HOST || 'localhost', + user: process.env.DB_USER || 'sitemente', + password: process.env.DB_PASS || 'SiteMente2026!', + database: process.env.DB_NAME || 'sitemente' +}; + +export async function GET() { + let conn; + try { + conn = await mysql.createConnection(dbConfig); + const [rows] = await conn.execute('SELECT * FROM research ORDER BY created_at DESC'); + return NextResponse.json(rows); + } catch (e) { + console.error(e); + return NextResponse.json([]); + } finally { + if (conn) conn.end(); + } +} + +export async function POST(request: Request) { + let conn; + try { + const body = await request.json(); + conn = await mysql.createConnection(dbConfig); + + await conn.execute( + 'INSERT INTO research (name, query, results_count, summary, results, created_at) VALUES (?, ?, ?, ?, ?, NOW())', + [body.name, body.query, body.results_count || 0, body.summary || '', JSON.stringify(body.results || [])] + ); + + const [rows] = await conn.execute('SELECT * FROM research ORDER BY created_at DESC'); + return NextResponse.json(rows); + } catch (e) { + console.error(e); + return NextResponse.json({ error: 'Failed to save research' }, { status: 500 }); + } finally { + if (conn) conn.end(); + } +} + +export async function DELETE(request: Request) { + let conn; + try { + const { searchParams } = new URL(request.url); + const id = searchParams.get('id'); + + if (!id) return NextResponse.json({ error: 'Missing id' }, { status: 400 }); + + conn = await mysql.createConnection(dbConfig); + await conn.execute('DELETE FROM research WHERE id = ?', [id]); + + return NextResponse.json({ success: true }); + } catch (e) { + console.error(e); + return NextResponse.json({ error: 'Failed to delete' }, { status: 500 }); + } finally { + if (conn) conn.end(); + } +} diff --git a/app/components/VoiceWidget.tsx b/app/components/VoiceWidget.tsx new file mode 100644 index 0000000..fbd2a46 --- /dev/null +++ b/app/components/VoiceWidget.tsx @@ -0,0 +1,187 @@ +"use client"; + +import { useState, useEffect, useRef } from "react"; + +export default function VoiceWidget() { + const [isListening, setIsListening] = useState(false); + const [transcript, setTranscript] = useState(""); + const [response, setResponse] = useState(""); + const [isSpeaking, setIsSpeaking] = useState(false); + const [isLoading, setIsLoading] = useState(false); + const recognitionRef = useRef(null); + const synthRef = useRef(null); + + useEffect(() => { + const SpeechRecognition = (window as any).SpeechRecognition || (window as any).webkitSpeechRecognition; + const synth = window.speechSynthesis; + + if (SpeechRecognition) { + recognitionRef.current = new SpeechRecognition(); + recognitionRef.current.continuous = false; + recognitionRef.current.interimResults = true; + recognitionRef.current.lang = "es-ES"; + + recognitionRef.current.onresult = (event: any) => { + const last = event.results.length - 1; + const text = event.results[last][0].transcript; + setTranscript(text); + + if (event.results[last].isFinal) { + handleSend(text); + } + }; + + recognitionRef.current.onend = () => { + setIsListening(false); + }; + } + + if (synth) { + synthRef.current = synth; + } + + return () => { + if (recognitionRef.current) recognitionRef.current.abort(); + }; + }, []); + + const startListening = () => { + if (recognitionRef.current && !isListening) { + setTranscript(""); + recognitionRef.current.start(); + setIsListening(true); + } + }; + + const stopListening = () => { + if (recognitionRef.current && isListening) { + recognitionRef.current.stop(); + setIsListening(false); + } + }; + + const handleSend = async (text: string) => { + if (!text.trim()) return; + + setIsLoading(true); + setResponse("Pensando..."); + + try { + const res = await fetch("/api/chat", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ text }) + }); + + const data = await res.json(); + const reply = (data.response || "¡Hola! Estoy aquí. ¿En qué puedo ayudarte?").substring(0, 200); + + setResponse(reply); + speak(reply); + } catch (e) { + console.error(e); + setResponse("Tengo problemas para conectar. Intenta de nuevo."); + } + + setIsLoading(false); + }; + + const speak = (text: string) => { + if (!synthRef.current) return; + + synthRef.current.cancel(); + + const utterance = new SpeechSynthesisUtterance(text); + utterance.lang = "es-ES"; + utterance.rate = 0.85; + utterance.pitch = 0.9; + + const voices = synthRef.current.getVoices(); + const spanishVoice = voices.find((v: any) => v.lang.includes("es")); + if (spanishVoice) utterance.voice = spanishVoice; + + utterance.onstart = () => setIsSpeaking(true); + utterance.onend = () => setIsSpeaking(false); + utterance.onerror = () => setIsSpeaking(false); + + synthRef.current.speak(utterance); + }; + + return ( +
+
+
👑
+

Cleopatra Voice

+

Asistente de Ventas IA

+
+ +
+
+ {isListening ? "🎤 Escuchando..." : + isSpeaking ? "🔊 Hablando..." : + isLoading ? "⏳ Pensando..." : + "💬 Lista"} +
+
+ +
+ {[...Array(12)].map((_, i) => ( +
+ ))} +
+ + {transcript && ( +
+

Dijiste:

+

{transcript}

+
+ )} + + {response && ( +
+

Cleopatra:

+

{response}

+
+ )} + +
+ + + +
+ +

+ {isListening ? "Habla ahora..." : "Toca el micrófono para empezar"} +

+
+ ); +} diff --git a/app/demos/voice/page.tsx b/app/demos/voice/page.tsx new file mode 100644 index 0000000..23c1fe7 --- /dev/null +++ b/app/demos/voice/page.tsx @@ -0,0 +1,29 @@ +import VoiceWidget from "../../components/VoiceWidget"; + +export default function DemoPage() { + return ( +
+
+
+

+ 🏆 SiteMente AI Demo +

+

+ Meet Cleopatra - Your AI Voice Assistant +

+
+ + + +
+

+ Powered by Ollama (local AI) +

+

+ Voice processing happens locally - no external APIs! +

+
+
+
+ ); +} diff --git a/app/extensions/hookd/page.tsx b/app/extensions/hookd/page.tsx new file mode 100644 index 0000000..e5c67f7 --- /dev/null +++ b/app/extensions/hookd/page.tsx @@ -0,0 +1,86 @@ +export default function HookdPage() { + return ( +
+
+
+
📌
+

Hookd

+

+ Save, organize and AI-sort your X content +

+
+ +
+

Download the Hookd Chrome Extension

+ + ⬇️ Download Hookd.zip + +
+ +

Features

+
+ {[ + { icon: '📥', title: 'DM Organizer', desc: 'Extract and categorize your X DMs' }, + { icon: '🐦', title: 'Feed Saver', desc: 'Checkmark posts to save in bulk' }, + { icon: '🤖', title: 'AI Processing', desc: 'Connect to OpenClaw for auto-categorization' }, + { icon: '👥', title: 'Contact Lists', desc: 'Share to Dev Friends, Meme Buddies & more' }, + ].map((f, i) => ( +
+
{f.icon}
+

{f.title}

+

{f.desc}

+
+ ))} +
+ +

Installation

+
    +
  1. Download the extension (button above)
  2. +
  3. Unzip the downloaded file
  4. +
  5. Open Chrome → chrome://extensions/
  6. +
  7. Enable Developer mode (top right toggle)
  8. +
  9. Click Load unpacked
  10. +
  11. Select the hookd folder (the folder, not individual files)
  12. +
  13. Pin 📌 Hookd to your toolbar!
  14. +
+ +
+

+ Note: Version 1.0.5 - CSP compliant, requires Chrome 88+ +

+
+
+
+ ) +} diff --git a/app/mission-control/hp-leads/page.tsx b/app/mission-control/hp-leads/page.tsx new file mode 100644 index 0000000..53b17dc --- /dev/null +++ b/app/mission-control/hp-leads/page.tsx @@ -0,0 +1,27 @@ +import { NextResponse } from 'next/server' +import mysql from 'mysql2/promise' + +export const dynamic = 'force-dynamic' + +async function getPool() { + return mysql.createPool({ + host: 'localhost', + user: 'sitemente', + password: 'pa0ndoQRPDYBHzTXX3rbWd6E', + database: 'sitemente', + waitForConnections: true, + connectionLimit: 10 + }) +} + +export async function GET() { + try { + const pool = await getPool() + const [rows] = await pool.query('SELECT * FROM hostpioneers_leads ORDER BY total_spent DESC LIMIT 100') + await pool.end() + return NextResponse.json({ leads: rows }) + } catch (error) { + console.error('Error:', error) + return NextResponse.json({ error: 'Failed to fetch' }, { status: 500 }) + } +} diff --git a/app/mission-control/hp-submissions/page.tsx b/app/mission-control/hp-submissions/page.tsx new file mode 100644 index 0000000..0993785 --- /dev/null +++ b/app/mission-control/hp-submissions/page.tsx @@ -0,0 +1,72 @@ +"use client"; + +import { useState, useEffect } from "react"; + +interface Submission { + name: string; + email: string; + whatsapp: string; + message: string; + date: string; +} + +export default function HPSubmissionsPage() { + const [submissions, setSubmissions] = useState([]); + const [loading, setLoading] = useState(true); + + useEffect(() => { + fetch('/api/hp-submissions') + .then(r => r.json()) + .then(data => { + setSubmissions(data.submissions || []); + setLoading(false); + }) + .catch(() => setLoading(false)); + }, []); + + if (loading) { + return ( +
+

HP Submissions

+

Loading...

+
+ ); + } + + return ( +
+

📬 HP Contact Submissions

+ + {submissions.length === 0 ? ( +

No submissions yet

+ ) : ( +
+ {submissions.map((sub, i) => ( +
+
+ {sub.name} + {sub.date} +
+
+ 📧 {sub.email} +
+ {sub.whatsapp && ( +
+ 📱 {sub.whatsapp} +
+ )} +
+ {sub.message} +
+
+ ))} +
+ )} +
+ ); +} diff --git a/app/mission-control/leads/hp-leads/page.tsx b/app/mission-control/leads/hp-leads/page.tsx new file mode 100644 index 0000000..ca99b19 --- /dev/null +++ b/app/mission-control/leads/hp-leads/page.tsx @@ -0,0 +1,219 @@ +'use client' +import { useState, useEffect } from 'react' + +interface Lead { + id: number + firstname: string + lastname: string + company: string + email: string + phone: string + country: string + status: string + total_spent: string + segment: string + dormancy_flag: string + email_status: string +} + +interface Pagination { + page: number + limit: number + total: number + totalPages: number +} + +export default function HPLeadsPage() { + const [leads, setLeads] = useState([]) + const [loading, setLoading] = useState(true) + const [filter, setFilter] = useState('all') + const [page, setPage] = useState(1) + const [pagination, setPagination] = useState({ page: 1, limit: 100, total: 0, totalPages: 0 }) + const [search, setSearch] = useState('') + const [showSearch, setShowSearch] = useState(false) + const [jumpPage, setJumpPage] = useState('') + const [sortBy, setSortBy] = useState('total_spent') + const [sortOrder, setSortOrder] = useState('DESC') + + useEffect(() => { + fetchLeads() + }, [filter, page, sortBy, sortOrder]) + + const fetchLeads = async () => { + setLoading(true) + const segmentParam = filter !== 'all' ? `&segment=${filter}` : '' + const res = await fetch(`/api/mission-control/hp-leads?page=${page}&limit=100${segmentParam}&sortBy=${sortBy}&sortOrder=${sortOrder}`) + const data = await res.json() + setLeads(data.leads || []) + setPagination(data.pagination || { page: 1, limit: 100, total: 0, totalPages: 0 }) + setLoading(false) + } + + const handleSort = (column: string) => { + if (sortBy === column) { + setSortOrder(sortOrder === 'ASC' ? 'DESC' : 'ASC') + } else { + setSortBy(column) + setSortOrder('DESC') + } + } + + const handleJump = (e: React.FormEvent) => { + e.preventDefault() + const pageNum = parseInt(jumpPage) + if (pageNum > 0 && pageNum <= pagination.totalPages) { + setPage(pageNum) + setJumpPage('') + } + } + + const handleSearch = (e: React.FormEvent) => { + e.preventDefault() + setPage(1) + fetchSearch() + } + + const fetchSearch = async () => { + setLoading(true) + const segmentParam = filter !== 'all' ? `&segment=${filter}` : '' + const searchParam = search ? `&search=${encodeURIComponent(search)}` : '' + const res = await fetch(`/api/mission-control/hp-leads?page=1&limit=100${segmentParam}${searchParam}&sortBy=${sortBy}&sortOrder=${sortOrder}`) + const data = await res.json() + setLeads(data.leads || []) + setPagination(data.pagination || { page: 1, limit: 100, total: 0, totalPages: 0 }) + setLoading(false) + } + + const clearSearch = () => { + setSearch('') + setFilter('all') + setPage(1) + fetchLeads() + } + + const SortIcon = ({ column }: { column: string }) => ( + + {sortBy === column && sortOrder === 'ASC' ? '↑' : '↓'} + + ) + + return ( +
+
+
+

HP Leads

+

{pagination.total.toLocaleString()} total records

+
+ +
+ + {showSearch && ( +
+ setSearch(e.target.value)} + placeholder="Search name, email, company, phone..." + style={{ flex: 1, padding: '0.75rem 1rem', borderRadius: '0.5rem', border: '1px solid #334155', background: '#1e293b', color: '#fff', minWidth: '200px' }} + /> + + +
+ )} + +
+ Filter: + {['all', 'VIP', 'Standard', 'Re-engage'].map(f => ( + + ))} +
+ +
+
+ + + + {['firstname', 'lastname', 'company', 'email', 'phone', 'country', 'total_spent', 'segment', 'dormancy_flag'].map(col => ( + + ))} + + + + {loading ? ( + + ) : leads.length === 0 ? ( + + ) : leads.map(lead => ( + + + + + + + + + + + + ))} + +
handleSort(col)} + style={{ padding: '1rem', textAlign: 'left', borderBottom: '1px solid #334155', cursor: 'pointer', userSelect: 'none' }} + > + + {col === 'firstname' ? 'First Name' : col === 'lastname' ? 'Last Name' : col === 'company' ? 'Company' : col === 'email' ? 'Email' : col === 'phone' ? 'Phone' : col === 'country' ? 'Country' : col === 'total_spent' ? 'Spent' : col === 'segment' ? 'Segment' : col === 'dormancy_flag' ? 'Status' : col} + + +
Loading...
No results found
{lead.firstname}{lead.lastname}{lead.company || '-'}{lead.email}{lead.phone || '-'}{lead.country}${Number(lead.total_spent).toFixed(2)} + {lead.segment} + + {lead.dormancy_flag} +
+
+
+ +
+ + +
+
+ Go to: + setJumpPage(e.target.value)} placeholder="#" min={1} max={pagination.totalPages} style={{ width: '60px', padding: '0.5rem', borderRadius: '0.25rem', border: '1px solid #334155', background: '#1e293b', color: '#fff', textAlign: 'center' }} /> + +
+ {page} / {pagination.totalPages} +
+ +
+ {[1, Math.floor(pagination.totalPages/4), Math.floor(pagination.totalPages/2), Math.floor(pagination.totalPages*3/4), pagination.totalPages].filter((p, i, arr) => arr.indexOf(p) === i && p > 0 && p <= pagination.totalPages).map(p => ( + + ))} +
+ + +
+ +

Showing {leads.length} of {pagination.total.toLocaleString()} records

+
+ ) +} diff --git a/app/mission-control/research/page.tsx b/app/mission-control/research/page.tsx new file mode 100644 index 0000000..aee0606 --- /dev/null +++ b/app/mission-control/research/page.tsx @@ -0,0 +1,226 @@ +"use client"; + +import { useState, useEffect } from "react"; + +interface ResearchResult { + title: string; + url: string; + score: number; + content: string; +} + +interface Research { + id: string; + name: string; + query: string; + results_count: number; + created_at: string; + saved: boolean; + summary: string; + results?: ResearchResult[]; +} + +export default function MissionResearchPage() { + const [research, setResearch] = useState([]); + const [loading, setLoading] = useState(true); + const [newResearch, setNewResearch] = useState({ name: "", query: "" }); + const [searching, setSearching] = useState(false); + const [expanded, setExpanded] = useState(null); + + useEffect(() => { + fetchResearch(); + }, []); + + const fetchResearch = async () => { + setLoading(true); + try { + const res = await fetch("/api/research"); + const data = await res.json(); + setResearch(data); + } catch (e) { + console.error(e); + } + setLoading(false); + }; + + const handleSearch = async () => { + if (!newResearch.name || !newResearch.query) return; + + setSearching(true); + try { + const tavilyRes = await fetch('https://api.tavily.com/search', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + api_key: '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', + query: newResearch.query, + max_results: 20, + search_depth: 'advanced' + }) + }); + const tavilyData = await tavilyRes.json(); + + const results = (tavilyData.results || []).map((r: any) => ({ + title: r.title || 'No title', + url: r.url || '', + score: r.score || 0, + content: (r.content || '').substring(0, 400) + })); + + const summary = results.slice(0, 5).map((r: any) => r.title).join('; ') || 'No results'; + + const res = await fetch("/api/research", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + name: newResearch.name, + query: newResearch.query, + results_count: results.length, + summary: summary.substring(0, 150), + results: results + }) + }); + await res.json(); + setNewResearch({ name: "", query: "" }); + fetchResearch(); + } catch (e) { + console.error(e); + } + setSearching(false); + }; + + const deleteResearch = async (id: string) => { + try { + await fetch(`/api/research?id=${id}`, { method: "DELETE" }); + fetchResearch(); + } catch (e) { + console.error(e); + } + }; + + return ( +
+
+

+ 🔒 Security Rule: Never install skills from the web. Read the content, understand what it does, then BUILD IT yourself. + We build, we don't install. One compromised skill can wipe everything. +

+
+ +

🔍 Deep Research

+ + {/* Search Form */} +
+
+ setNewResearch({ ...newResearch, name: e.target.value })} + className="bg-gray-700 text-white px-4 py-2 rounded" + /> + setNewResearch({ ...newResearch, query: e.target.value })} + className="bg-gray-700 text-white px-4 py-2 rounded" + /> +
+ +
+ + {/* Research List */} + {loading ? ( +

Loading...

+ ) : research.length === 0 ? ( +

No research yet. Run your first search!

+ ) : ( +
+ {research.map((r) => ( +
+ {/* Header */} +
+
+

{r.name}

+

{r.query}

+
+
+ + +
+
+ + {/* Table View */} + {expanded === r.id && r.results && ( +
+ + + + + + + + + + + + {r.results.map((result, i) => ( + + + + + + + + ))} + +
Skill / ToolWhat It DoesScoreLink
+ + + {result.title} + + {result.content.substring(0, 150)}... + + 0.9 ? 'bg-green-900 text-green-400' : + result.score > 0.7 ? 'bg-yellow-900 text-yellow-400' : + 'bg-gray-700 text-gray-400' + }`}> + {(result.score * 100).toFixed(1)}% + + + + Open → + +
+
+ )} +
+ ))} +
+ )} +
+ ); +} diff --git a/components/mission-control/MissionControlDashboard.tsx b/components/mission-control/MissionControlDashboard.tsx index 05c3c0e..dcb2934 100644 --- a/components/mission-control/MissionControlDashboard.tsx +++ b/components/mission-control/MissionControlDashboard.tsx @@ -39,6 +39,7 @@ const sidebarCategories: SidebarCategory[] = [ ]}, { id: "leads", name: "Leads", icon: "📈", items: [ { id: "leads-crm", name: "CRM", icon: "📊", category: "leads" }, + { id: "hp-submissions", name: "HP Submissions", icon: "📬", category: "hp-submissions" }, { id: "dashboard", name: "Client Dashboard", icon: "🏢", category: "dashboard" }, ]}, { id: "projects", name: "Projects", icon: "🎯", items: [ @@ -46,6 +47,7 @@ const sidebarCategories: SidebarCategory[] = [ { id: "sitemente", name: "SiteMente", icon: "🌐", color: "#ff7bc0", category: "projects" }, { id: "demos", name: "Demo Pages", icon: "🎨", category: "demos" }, { id: "holacompi", name: "HolaCompi", icon: "🤝", color: "#6366f1", category: "projects" }, + { id: "hookd", name: "Hookd", icon: "📌", color: "#4F46E5", category: "hookd" }, { id: "arabredox", name: "Arabredox", icon: "💚", color: "#22c55e", category: "projects" }, { id: "infrastructure", name: "Infra", icon: "⚙️", color: "#10b981", category: "projects" }, ]}, @@ -77,6 +79,7 @@ const sidebarCategories: SidebarCategory[] = [ { id: "memory", name: "Memory", icon: "🧠", items: [ { id: "logs", name: "Session Logs", icon: "📝", category: "memory" }, { id: "snapshots", name: "Snapshots", icon: "📸", category: "snapshots" }, + { id: "research", name: "Deep Research", icon: "🔍", category: "research" }, ]}, { id: "docs", name: "Docs", icon: "📚", items: [ { id: "docs-index", name: "Documentation", icon: "📚", category: "docs" }, @@ -436,6 +439,16 @@ export default function MissionControlDashboard({ onLogout }: MissionControlDash
)} + {currentItem?.category === "research" && ( +
+
+

🔍 Deep Research

+

AI-powered research using Tavily API

+ 🔍 Open Research +
+
+ )} + {/* BMHQ Automation Panels */} {currentItem?.category === "autorun" && (
@@ -568,6 +581,19 @@ export default function MissionControlDashboard({ onLogout }: MissionControlDash
)} + {currentItem?.category === "hp-submissions" && ( +
+
+
📬
+

HP Submissions

+

HostPioneers contact form submissions

+ +
+
+ )} + {currentItem?.category === "dashboard" && (
@@ -612,6 +638,7 @@ export default function MissionControlDashboard({ onLogout }: MissionControlDash + @@ -655,6 +682,38 @@ export default function MissionControlDashboard({ onLogout }: MissionControlDash
)} + + {currentItem?.category === "hookd" && ( +
+
+
📌
+

Hookd

+

Save, organize and AI-sort your X content

+ +
+
+ 📥 +

DM Organizer

+
+
+ 🐦 +

Feed Saver

+
+
+ 🤖 +

AI Processing

+
+
+ 🏷️ +

Custom Tags

+
+
+
+
+ )}
); diff --git a/data/eod_briefs.json b/data/eod_briefs.json index fe51488..fcf10e7 100644 --- a/data/eod_briefs.json +++ b/data/eod_briefs.json @@ -1 +1,226 @@ -[] +[ + { + "id": "eod-1773864002102", + "date": "2026-03-18", + "completed": [ + "See Mission Control for details" + ], + "progress": {}, + "council": {}, + "tomorrow": [], + "created_at": "2026-03-18T20:00:02.102Z" + }, + { + "id": "eod-1773777601641", + "date": "2026-03-17", + "completed": [ + "See Mission Control for details" + ], + "progress": {}, + "council": {}, + "tomorrow": [], + "created_at": "2026-03-17T20:00:01.641Z" + }, + { + "id": "eod-1773691202704", + "date": "2026-03-16", + "completed": [ + "See Mission Control for details" + ], + "progress": {}, + "council": {}, + "tomorrow": [], + "created_at": "2026-03-16T20:00:02.704Z" + }, + { + "id": "eod-1773604802284", + "date": "2026-03-15", + "completed": [ + "See Mission Control for details" + ], + "progress": {}, + "council": {}, + "tomorrow": [], + "created_at": "2026-03-15T20:00:02.284Z" + }, + { + "id": "eod-1773518402124", + "date": "2026-03-14", + "completed": [ + "See Mission Control for details" + ], + "progress": {}, + "council": {}, + "tomorrow": [], + "created_at": "2026-03-14T20:00:02.124Z" + }, + { + "id": "eod-1773432001747", + "date": "2026-03-13", + "completed": [ + "See Mission Control for details" + ], + "progress": {}, + "council": {}, + "tomorrow": [], + "created_at": "2026-03-13T20:00:01.747Z" + }, + { + "id": "eod-1773345602107", + "date": "2026-03-12", + "completed": [ + "See Mission Control for details" + ], + "progress": {}, + "council": {}, + "tomorrow": [], + "created_at": "2026-03-12T20:00:02.107Z" + }, + { + "id": "eod-1773259202293", + "date": "2026-03-11", + "completed": [ + "See Mission Control for details" + ], + "progress": {}, + "council": {}, + "tomorrow": [], + "created_at": "2026-03-11T20:00:02.293Z" + }, + { + "id": "eod-1773172801469", + "date": "2026-03-10", + "completed": [ + "See Mission Control for details" + ], + "progress": {}, + "council": {}, + "tomorrow": [], + "created_at": "2026-03-10T20:00:01.469Z" + }, + { + "id": "eod-1773086402559", + "date": "2026-03-09", + "completed": [ + "See Mission Control for details" + ], + "progress": {}, + "council": {}, + "tomorrow": [], + "created_at": "2026-03-09T20:00:02.559Z" + }, + { + "id": "eod-1773000002206", + "date": "2026-03-08", + "completed": [ + "See Mission Control for details" + ], + "progress": {}, + "council": {}, + "tomorrow": [], + "created_at": "2026-03-08T20:00:02.206Z" + }, + { + "id": "eod-1772913601820", + "date": "2026-03-07", + "completed": [ + "See Mission Control for details" + ], + "progress": {}, + "council": {}, + "tomorrow": [], + "created_at": "2026-03-07T20:00:01.820Z" + }, + { + "id": "eod-1772827201835", + "date": "2026-03-06", + "completed": [ + "See Mission Control for details" + ], + "progress": {}, + "council": {}, + "tomorrow": [], + "created_at": "2026-03-06T20:00:01.835Z" + }, + { + "id": "eod-1772740802338", + "date": "2026-03-05", + "completed": [ + "See Mission Control for details" + ], + "progress": {}, + "council": {}, + "tomorrow": [], + "created_at": "2026-03-05T20:00:02.338Z" + }, + { + "id": "eod-1772654402583", + "date": "2026-03-04", + "completed": [ + "See Mission Control for details" + ], + "progress": {}, + "council": {}, + "tomorrow": [], + "created_at": "2026-03-04T20:00:02.583Z" + }, + { + "id": "eod-1772568002013", + "date": "2026-03-03", + "completed": [ + "See Mission Control for details" + ], + "progress": {}, + "council": {}, + "tomorrow": [], + "created_at": "2026-03-03T20:00:02.013Z" + }, + { + "id": "eod-1772481602906", + "date": "2026-03-02", + "completed": [ + "See Mission Control for details" + ], + "progress": {}, + "council": {}, + "tomorrow": [], + "created_at": "2026-03-02T20:00:02.906Z" + }, + { + "id": "eod-1772308802958", + "date": "2026-02-28", + "completed": [ + "See Mission Control for details" + ], + "progress": {}, + "council": {}, + "tomorrow": [], + "created_at": "2026-02-28T20:00:02.958Z" + }, + { + "id": "eod-1772228668001", + "date": "2026-02-27", + "completed": [ + "Council to top of MC", + "Agent modal with tasks/chat", + "Voice widget added (disabled - API key issue)", + "Grid layout for agents" + ], + "progress": { + "MC": "80%", + "Voice": "50%", + "Briefs": "90%" + }, + "council": { + "Horus": "Orchestrating all agents", + "Thoth": "Research ready", + "Ptah": "Dev ops ready" + }, + "tomorrow": [ + "Fix MiniMax API key", + "Enable voice widget", + "Test agent automation" + ], + "created_at": "2026-02-27T21:44:28.001Z" + } +] \ No newline at end of file diff --git a/data/execution-logs.json b/data/execution-logs.json index 039f400..8ad0d08 100644 --- a/data/execution-logs.json +++ b/data/execution-logs.json @@ -1,4 +1,22 @@ [ + { + "id": "log-1772229860745", + "templateId": "anubis-outreach-scan", + "agent": "anubis", + "taskTitle": "Weekly Outreach Scan", + "startedAt": "2026-02-27T22:04:20.745Z", + "status": "running", + "output": "Agent executing..." + }, + { + "id": "log-1772229850446", + "templateId": "ptah-infra-monitor", + "agent": "ptah", + "taskTitle": "Infra Health Check", + "startedAt": "2026-02-27T22:04:10.446Z", + "status": "running", + "output": "Agent executing..." + }, { "id": "log-1772215845028", "templateId": "thoth-daily-research", diff --git a/data/leads.json b/data/leads.json new file mode 100644 index 0000000..b32b37d --- /dev/null +++ b/data/leads.json @@ -0,0 +1,267 @@ +{ + "categories": [ + { + "category": "restaurants", + "location": "Málaga/Costa del Sol", + "businesses": [ + {"name": "El Tapeo de Cervantes", "address": "Calle Cister, Málaga", "phone": "", "type": "tapas restaurant"}, + {"name": "Casa Lola", "address": "Calle Granada, Málaga centro", "phone": "", "type": "tapas bar"}, + {"name": "La Farola de Orellana", "address": "Málaga", "phone": "", "type": "tapas bar"}, + {"name": "Arrebato Gastrotaberna", "address": "Málaga", "phone": "", "type": "gastro bar"}, + {"name": "El Lagar de Verum", "address": "Málaga", "phone": "", "type": "tapas bar"}, + {"name": "Casa Aranda", "address": "Centro, Málaga", "phone": "", "type": "restaurant"}, + {"name": "Uvedoble Taberna", "address": "Calle Cister 1, 29015 Málaga", "phone": "", "type": "tapas bar"}, + {"name": "Mesón de Cervantes", "address": "Granada Street 36, 29015 Málaga", "phone": "+34 951 55 00 55", "type": "restaurant"}, + {"name": "Taberna El Mentidero", "address": "Málaga", "phone": "", "type": "tavern"}, + {"name": "La Cabrera", "address": "Málaga", "phone": "", "type": "restaurant"}, + {"name": "Arco Tapas Bar", "address": "Marbella", "phone": "", "type": "tapas bar"}, + {"name": "Bistro Paloma", "address": "Marbella", "phone": "", "type": "bistro"}, + {"name": "El Candil de San Pedro", "address": "San Pedro de Alcántara, Marbella", "phone": "", "type": "restaurant"}, + {"name": "La Bodega", "address": "Marbella", "phone": "", "type": "tapas bar"}, + {"name": "Taberna La Niña del Pisto", "address": "Marbella Old Town", "phone": "", "type": "tavern"}, + {"name": "La Sala", "address": "Marbella", "phone": "", "type": "restaurant/bar"}, + {"name": "Wot You Like", "address": "Torremolinos", "phone": "", "type": "tapas bar"}, + {"name": "Casero La Comida Mediterránea", "address": "Torremolinos", "phone": "", "type": "Mediterranean restaurant"}, + {"name": "The Dizzy Donkey", "address": "Benalmádena", "phone": "", "type": "tapas bar"}, + {"name": "Azure Terraza", "address": "Benalmádena", "phone": "", "type": "restaurant"}, + {"name": "Coast To Coast", "address": "Benalmádena", "phone": "", "type": "restaurant"}, + {"name": "Il Boccaccio", "address": "Benalmádena", "phone": "", "type": "restaurant"}, + {"name": "Taperia La Bodeguita", "address": "Benalmádena", "phone": "", "type": "tapas bar"}, + {"name": "La Cepa", "address": "Benalmádena", "phone": "", "type": "tapas bar"}, + {"name": "Sabores Gourmet", "address": "Estepona", "phone": "", "type": "gourmet restaurant"}, + {"name": "The Boab Tree Restaurante", "address": "Estepona", "phone": "", "type": "restaurant"}, + {"name": "La Carbonara", "address": "Estepona", "phone": "", "type": "Italian restaurant"}, + {"name": "La Bulla Gastrobar", "address": "Estepona", "phone": "", "type": "gastro bar"}, + {"name": "La Casa del Rey", "address": "Estepona", "phone": "", "type": "restaurant"}, + {"name": "La Escollera", "address": "Estepona", "phone": "", "type": "restaurant"}, + {"name": "Tipi Tapa", "address": "Calle Malaga 4, 29640 Fuengirola", "phone": "951 311 630", "type": "tapas restaurant"}, + {"name": "Lotus Steak & Burger House", "address": "Fuengirola", "phone": "", "type": "steakhouse"}, + {"name": "La Mamounia", "address": "Fuengirola", "phone": "", "type": "restaurant"}, + {"name": "Platos Gastrobar", "address": "Fuengirola", "phone": "", "type": "gastro bar"}, + {"name": "O Mamma Mia", "address": "Fuengirola", "phone": "", "type": "Italian restaurant"}, + {"name": "Alberto's Restaurante", "address": "Matalascañas", "phone": "", "type": "restaurant"}, + {"name": "Costaluz Taberna", "address": "Matalascañas", "phone": "", "type": "tavern"}, + {"name": "Restaurante Los Pepes", "address": "Matalascañas", "phone": "", "type": "restaurant"}, + {"name": "Misuto", "address": "Málaga", "phone": "", "type": "restaurant"}, + {"name": "Araboka Centro", "address": "Málaga", "phone": "", "type": "restaurant"}, + {"name": "Arrozeando Pedregalejo", "address": "Pedregalejo, Málaga", "phone": "", "type": "restaurant"}, + {"name": "Las Merchanas", "address": "Calle Meryland, Málaga", "phone": "", "type": "tapas bar"}, + {"name": "La Cosmo", "address": "Málaga", "phone": "", "type": "restaurant"}, + {"name": "Casa Lola Strachan", "address": "Pl. de las Flores 2, 29005 Málaga", "phone": "", "type": "tapas bar"}, + {"name": "Malavida", "address": "Málaga", "phone": "", "type": "restaurant"}, + {"name": "No Piqui", "address": "Málaga", "phone": "", "type": "restaurant"}, + {"name": "San Sabor A Nápoles", "address": "Victoria, Málaga", "phone": "", "type": "Italian restaurant"}, + {"name": "Nintai", "address": "Marbella", "phone": "", "type": "Japanese restaurant"}, + {"name": "Erre & Urrechu", "address": "Marbella", "phone": "", "type": "restaurant"}, + {"name": "La Milla Marbella", "address": "Marbella", "phone": "", "type": "restaurant"}, + {"name": "Leña Marbella", "address": "Puente Romano, Marbella", "phone": "", "type": "restaurant"} + ] + }, + { + "category": "clinics", + "location": "Málaga/Costa del Sol", + "businesses": [ + {"name": "Hospital Dr. Gálvez", "address": "Málaga", "phone": "", "type": "private hospital"}, + {"name": "Vithas Málaga Hospital", "address": "Avenida Pintor Sorolla, Málaga", "phone": "", "type": "private hospital"}, + {"name": "Hospital Vithas Parque San Antonio", "address": "Málaga", "phone": "", "type": "private hospital"}, + {"name": "CHIP Hospital (Centro Hospitalario Integral Privado)", "address": "Málaga", "phone": "", "type": "private hospital"}, + {"name": "Quirónsalud Málaga Hospital", "address": "Málaga", "phone": "", "type": "private hospital"}, + {"name": "HM Málaga Hospital", "address": "Málaga", "phone": "", "type": "private hospital"}, + {"name": "Hospital Regional Universitario de Málaga", "address": "Málaga", "phone": "", "type": "public hospital"}, + {"name": "Hospital Universitario Virgen de la Victoria", "address": "Málaga", "phone": "", "type": "public hospital"}, + {"name": "Vithas Xanit International Hospital", "address": "Málaga/Costa del Sol", "phone": "", "type": "international hospital"}, + {"name": "The Budwig Center", "address": "Málaga", "phone": "", "type": "alternative medicine clinic"}, + {"name": "Perez Bryan Gynaecology Clinic", "address": "Málaga centro", "phone": "", "type": "gynecology clinic"}, + {"name": "Costa Medical Services", "address": "Calle Fernando Camino 10, 29016 Málaga", "phone": "", "type": "medical center"}, + {"name": "Fios Clinic", "address": "Plaza Uncibay 3, 29008 Málaga", "phone": "951 431 794", "type": "physiotherapy clinic"}, + {"name": "Phybone", "address": "29010 Málaga", "phone": "", "type": "physiotherapy clinic"}, + {"name": "Qicenter", "address": "Málaga", "phone": "", "type": "medical center"}, + {"name": "Fisitania Fisioterapia", "address": "Málaga", "phone": "", "type": "physiotherapy clinic"}, + {"name": "Centro Ankarena", "address": "Málaga", "phone": "", "type": "physiotherapy clinic"}, + {"name": "Fisioterapia Águeda Rodríguez", "address": "Málaga", "phone": "", "type": "physiotherapy clinic"}, + {"name": "Fisioterapia 2002", "address": "Málaga", "phone": "", "type": "physiotherapy clinic"}, + {"name": "Auriafisio", "address": "Málaga", "phone": "", "type": "physiotherapy clinic"}, + {"name": "Jorge Espada Fisioterapia", "address": "Marbella", "phone": "", "type": "physiotherapy clinic"}, + {"name": "Solaris Clinic", "address": "Málaga", "phone": "", "type": "rehabilitation center"}, + {"name": "Osteopathy Costa del Sol", "address": "Costa del Sol", "phone": "", "type": "osteopathy/physiotherapy"}, + {"name": "English Physio Málaga", "address": "Málaga", "phone": "", "type": "physiotherapy clinic"}, + {"name": "Physio Wave Marbella", "address": "Marbella", "phone": "", "type": "physiotherapy clinic"}, + {"name": "Corporis Fisioterapia", "address": "Marbella", "phone": "", "type": "physiotherapy clinic"}, + {"name": "In One Clinic", "address": "Marbella", "phone": "", "type": "multispecialty clinic"}, + {"name": "Clinica Premium", "address": "Málaga", "phone": "952 865 856", "type": "medical clinic"}, + {"name": "AMS Centro Médico", "address": "Costa del Sol", "phone": "", "type": "medical center"}, + {"name": "Grupo Dental Clinics Málaga Centro", "address": "Málaga", "phone": "", "type": "dental clinic"}, + {"name": "Grupo Dental Clinics Torremolinos", "address": "Benyamina Avenue 25, Torremolinos", "phone": "", "type": "dental clinic"}, + {"name": "Gross Dentists", "address": "Málaga y Torremolinos", "phone": "", "type": "dental clinic"}, + {"name": "Clínica Omega", "address": "Marbella y Torremolinos", "phone": "952 073 602 / 951 681 508", "type": "dental clinic"}, + {"name": "Marbella Dental Arts", "address": "Marbella", "phone": "", "type": "dental clinic"}, + {"name": "Scandinavian Dental Clinic", "address": "Torremolinos", "phone": "", "type": "dental clinic"}, + {"name": "Clínica Medident", "address": "Torremolinos", "phone": "", "type": "dental clinic"}, + {"name": "Clinica Dental Soriano", "address": "Marbella centro", "phone": "", "type": "dental clinic"}, + {"name": "R&H Dental Marbella", "address": "Marbella", "phone": "", "type": "dental clinic"}, + {"name": "Adeslas Dental Málaga", "address": "Málaga", "phone": "", "type": "dental clinic"}, + {"name": "Adeslas Dental Marbella", "address": "Marbella", "phone": "", "type": "dental clinic"}, + {"name": "Adeslas Dental Fuengirola", "address": "Fuengirola", "phone": "", "type": "dental clinic"}, + {"name": "Adeslas Dental Torremolinos", "address": "Torremolinos", "phone": "", "type": "dental clinic"}, + {"name": "Adeslas Dental Estepona", "address": "Estepona", "phone": "", "type": "dental clinic"}, + {"name": "Crooke Laguna Dental Clinic", "address": "Calle Mediterráneo 1, Marbella", "phone": "", "type": "dental clinic"}, + {"name": "International Dental Marbella", "address": "Marbella", "phone": "", "type": "dental clinic"}, + {"name": "Dentist Marbella", "address": "Marbella", "phone": "", "type": "dental clinic"}, + {"name": "Adeslas Dental Antequera", "address": "Antequera", "phone": "", "type": "dental clinic"}, + {"name": "Adeslas Dental Ronda", "address": "Ronda", "phone": "", "type": "dental clinic"} + ] + }, + { + "category": "real_estate_agencies", + "location": "Málaga/Costa del Sol", + "businesses": [ + {"name": "DM Properties (Diana Morales Properties)", "address": "Marbella", "phone": "", "type": "real estate agency"}, + {"name": "Marbella Estates", "address": "Marbella", "phone": "", "type": "real estate agency"}, + {"name": "Marbella Living", "address": "Marbella", "phone": "", "type": "real estate agency"}, + {"name": "The Agency Marbella", "address": "Marbella", "phone": "", "type": "real estate agency"}, + {"name": "Bromley Estates Marbella", "address": "Marbella", "phone": "", "type": "real estate agency"}, + {"name": "Key Real Estates", "address": "Marbella", "phone": "", "type": "real estate agency"}, + {"name": "CDS Property", "address": "Marbella", "phone": "", "type": "real estate agency"}, + {"name": "Christie's International Real Estate Costa del Sol", "address": "Costa del Sol", "phone": "", "type": "luxury real estate"}, + {"name": "Panorama Marbella", "address": "Marbella", "phone": "", "type": "real estate agency"}, + {"name": "Cima Real Estate", "address": "Marbella", "phone": "", "type": "real estate agency"}, + {"name": "Terra Meridiana", "address": "Estepona", "phone": "", "type": "real estate agency"}, + {"name": "Marbella24 (Ralf Michael)", "address": "Marbella", "phone": "", "type": "real estate agency"}, + {"name": "MPDunne & Hamptons International", "address": "Marbella", "phone": "", "type": "real estate agency"}, + {"name": "Engel & Völkers Marbella", "address": "Marbella", "phone": "", "type": "real estate agency"}, + {"name": "Marco Properties", "address": "Estepona", "phone": "", "type": "real estate agency"}, + {"name": "Orangestate", "address": "Marbella & Fuengirola", "phone": "", "type": "real estate agency"}, + {"name": "Spain Homes", "address": "Costa del Sol", "phone": "", "type": "real estate agency"}, + {"name": "Lucas Fox Estepona", "address": "C/ del Naranjo, Estepona", "phone": "+34 933 562 989", "type": "real estate agency"}, + {"name": "Cabanillas Real Estate", "address": "Estepona", "phone": "", "type": "real estate agency"}, + {"name": "Costa Dream House", "address": "Costa del Sol", "phone": "", "type": "real estate agency"}, + {"name": "VIVA Costa del Sol", "address": "Calahonda", "phone": "", "type": "real estate agency"}, + {"name": "Olsen Property Brokers", "address": "Estepona", "phone": "", "type": "real estate agency"}, + {"name": "BEST HOME", "address": "Costa del Sol", "phone": "", "type": "real estate agency"}, + {"name": "Fuengirola Estates", "address": "Fuengirola", "phone": "", "type": "real estate agency"}, + {"name": "Navasol Real Estate", "address": "Benalmádena Costa", "phone": "", "type": "real estate agency"}, + {"name": "Mojo Estates", "address": "Estepona Old Town", "phone": "", "type": "real estate agency"}, + {"name": "ERA Costa del Sol", "address": "Costa del Sol", "phone": "", "type": "real estate agency"}, + {"name": "Costa del Sol Property", "address": "Costa del Sol", "phone": "", "type": "real estate agency"}, + {"name": "Real Estepona", "address": "Estepona", "phone": "", "type": "real estate portal"}, + {"name": "Spain Homes Costa del Sol", "address": "Costa del Sol", "phone": "", "type": "real estate agency"}, + {"name": "Idealista (Marbella)", "address": "Marbella", "phone": "", "type": "property portal"}, + {"name": "Costa del Sol Real Estate", "address": "Costa del Sol", "phone": "", "type": "real estate agency"}, + {"name": "Luxury Properties Marbella", "address": "Marbella", "phone": "", "type": "luxury real estate"}, + {"name": "Estepona Property Agents", "address": "Estepona", "phone": "", "type": "real estate agency"}, + {"name": "Benalmádena Real Estate", "address": "Benalmádena", "phone": "", "type": "real estate agency"}, + {"name": "Mijas Real Estate", "address": "Mijas", "phone": "", "type": "real estate agency"}, + {"name": "Torremolinos Properties", "address": "Torremolinos", "phone": "", "type": "real estate agency"}, + {"name": "Fuengirola Property", "address": "Fuengirola", "phone": "", "type": "real estate agency"}, + {"name": "Malaga City Properties", "address": "Málaga", "phone": "", "type": "real estate agency"}, + {"name": "Ronda Properties", "address": "Ronda", "phone": "", "type": "real estate agency"}, + {"name": "Benahavis Real Estate", "address": "Benahavís", "phone": "", "type": "real estate agency"}, + {"name": "Nueva Alcuzcurra Real Estate", "address": "Costa del Sol", "phone": "", "type": "real estate agency"} + ] + }, + { + "category": "car_rental", + "location": "Málaga/Costa del Sol", + "businesses": [ + {"name": "Marbesol", "address": "Málaga Airport & Marbella", "phone": "", "type": "car rental"}, + {"name": "Hertz Málaga Airport", "address": "Málaga Airport", "phone": "", "type": "car rental"}, + {"name": "National Car Rental", "address": "Málaga Airport", "phone": "", "type": "car rental"}, + {"name": "Enterprise Rent-A-Car", "address": "Málaga Airport", "phone": "", "type": "car rental"}, + {"name": "Avis Car Rental", "address": "Málaga Airport", "phone": "", "type": "car rental"}, + {"name": "Budget Car Rental", "address": "Málaga Airport", "phone": "", "type": "car rental"}, + {"name": "Alamo Car Rental", "address": "Málaga Airport", "phone": "", "type": "car rental"}, + {"name": "SIXT Costa del Sol", "address": "Costa del Sol", "phone": "", "type": "car rental"}, + {"name": "Europcar Málaga Airport", "address": "Málaga Airport", "phone": "", "type": "car rental"}, + {"name": "Fetajo Rent a Car", "address": "Costa del Sol", "phone": "", "type": "car rental"}, + {"name": "MalagaCar.com", "address": "Pol. Ind. Aeropuerto de Málaga", "phone": "", "type": "car rental"}, + {"name": "Enterprise Marbella", "address": "Marbella", "phone": "", "type": "car rental"}, + {"name": "Yellow Car Marbella", "address": "Marbella & Málaga Airport", "phone": "", "type": "car rental"}, + {"name": "Malaga U Drive", "address": "Benalmádena", "phone": "", "type": "car rental"}, + {"name": "Rahul Rent a Car", "address": "Torremolinos & Benalmádena", "phone": "", "type": "car rental"}, + {"name": "Europcar Torremolinos", "address": "Torremolinos", "phone": "", "type": "car rental"}, + {"name": "Caramba Car", "address": "Torremolinos", "phone": "", "type": "car rental"}, + {"name": "Rent a Car Patry", "address": "Torremolinos", "phone": "", "type": "car rental"}, + {"name": "CAFISA Rent a Car", "address": "Costa del Sol", "phone": "", "type": "car rental"}, + {"name": "Albacars Benalmádena", "address": "Benalmádena", "phone": "", "type": "car rental"}, + {"name": "Localrent Benalmádena", "address": "Benalmádena", "phone": "", "type": "car rental"}, + {"name": "DoYouSpain Benalmádena", "address": "Benalmádena", "phone": "", "type": "car rental"}, + {"name": "Finauto Rent a Car", "address": "Benalmádena", "phone": "", "type": "car rental"}, + {"name": "Miami Car Hire", "address": "Benalmádena", "phone": "", "type": "car rental"}, + {"name": "Montemar Rent a Car", "address": "Benalmádena", "phone": "", "type": "car rental"}, + {"name": "Autos Remo", "address": "Benalmádena", "phone": "", "type": "car rental"}, + {"name": "Good Morning Rent a Car", "address": "Benalmádena", "phone": "", "type": "car rental"}, + {"name": "Clickrent", "address": "Costa del Sol", "phone": "", "type": "car rental"}, + {"name": "DelPaso", "address": "Costa del Sol", "phone": "", "type": "car rental"}, + {"name": "Fox Rent A Car", "address": "Costa del Sol", "phone": "", "type": "car rental"}, + {"name": "Flexways", "address": "Costa del Sol", "phone": "", "type": "car rental"}, + {"name": "Ace Car Rental", "address": "Málaga Airport", "phone": "", "type": "car rental"}, + {"name": "Thrifty Car Rental", "address": "Málaga Airport", "phone": "", "type": "car rental"}, + {"name": "Dollar Car Rental", "address": "Málaga Airport", "phone": "", "type": "car rental"}, + {"name": "Payless Car Rental", "address": "Málaga Airport", "phone": "", "type": "car rental"}, + {"name": "Gold Car Rental", "address": "Málaga", "phone": "", "type": "car rental"}, + {"name": "Record Car Rental", "address": "Málaga", "phone": "", "type": "car rental"}, + {"name": "Firefly Car Rental", "address": "Málaga Airport", "phone": "", "type": "car rental"}, + {"name": "Kimi Car Rental", "address": "Málaga", "phone": "", "type": "car rental"}, + {"name": "Orlando Car Rental", "address": "Costa del Sol", "phone": "", "type": "car rental"}, + {"name": "Beta Car Rental", "address": "Málaga", "phone": "", "type": "car rental"} + ] + }, + { + "category": "hotels", + "location": "Málaga/Costa del Sol", + "businesses": [ + {"name": "Hotel MS Maestranza", "address": "Málaga", "phone": "", "type": "hotel"}, + {"name": "Catalonia Puerta del Mar", "address": "Málaga centro", "phone": "", "type": "hotel"}, + {"name": "Coeo Hernan Ruíz", "address": "Málaga", "phone": "", "type": "hotel"}, + {"name": "Hostal Larios", "address": "Calle Larios, Málaga", "phone": "", "type": "hostal"}, + {"name": "Hotel Boutique Teatro Romano", "address": "Málaga", "phone": "", "type": "boutique hotel"}, + {"name": "LD Convento Cister", "address": "Málaga", "phone": "", "type": "hotel"}, + {"name": "Hotel Carlos V Málaga", "address": "Málaga", "phone": "", "type": "hotel"}, + {"name": "Hotel Vincci Seleccion Posada", "address": "Málaga", "phone": "", "type": "hotel"}, + {"name": "Gran Hotel Miramar GL", "address": "Málaga", "phone": "", "type": "luxury hotel"}, + {"name": "Marbella Club Hotel Golf Resort & Spa", "address": "Marbella", "phone": "", "type": "luxury resort"}, + {"name": "Hotel Don Pepe Gran Meliá", "address": "Marbella", "phone": "", "type": "luxury hotel"}, + {"name": "Hotel El Fuerte Marbella", "address": "Marbella", "phone": "", "type": "hotel"}, + {"name": "ME Marbella", "address": "Marbella", "phone": "", "type": "luxury hotel"}, + {"name": "Amare Beach Hotel Marbella", "address": "Marbella", "phone": "", "type": "beach hotel"}, + {"name": "Palacio Solecio", "address": "Marbella", "phone": "", "type": "boutique hotel"}, + {"name": "Elba Estepona Gran Hotel & Thalasso", "address": "Estepona", "phone": "", "type": "hotel & spa"}, + {"name": "H10 Estepona Palace", "address": "Estepona", "phone": "", "type": "hotel"}, + {"name": "Exe Estepona Thalasso & Spa", "address": "Estepona", "phone": "", "type": "adults only hotel"}, + {"name": "Sol Marbella Estepona Atalaya Park", "address": "Estepona", "phone": "", "type": "hotel"}, + {"name": "Senator Banús", "address": "Estepona", "phone": "", "type": "hotel"}, + {"name": "THE FLAG Hotel Marbella", "address": "Marbella", "phone": "", "type": "hotel"}, + {"name": "Hotel Benalma Costa del Sol", "address": "Benalmádena", "phone": "", "type": "hotel"}, + {"name": "Hotel Costa Azul", "address": "Benalmádena", "phone": "", "type": "hotel"}, + {"name": "Hotel Sunset Beach Club", "address": "Benalmádena", "phone": "", "type": "hotel"}, + {"name": "Hotel Best Siroco", "address": "Benalmádena", "phone": "", "type": "hotel"}, + {"name": "Hotel Elsereins", "address": "Torremolinos", "phone": "", "type": "hotel"}, + {"name": "Hotel Ritual", "address": "Torremolinos", "phone": "", "type": "hotel"}, + {"name": "Hotel Ocean House", "address": "Torremolinos", "phone": "", "type": "hotel"}, + {"name": "Hotel Medplaya Hotel Bali", "address": "Torremolinos", "phone": "", "type": "hotel"}, + {"name": "Hotel Los Pearlos", "address": "Torremolinos", "phone": "", "type": "hotel"}, + {"name": "Meliá Costa del Sol", "address": "Torremolinos", "phone": "", "type": "hotel"}, + {"name": "Sol Principe", "address": "Torremolinos", "phone": "", "type": "hotel"}, + {"name": "Hotel Allegro Granada", "address": "Málaga", "phone": "", "type": "hotel"}, + {"name": "Hotel Villa Ligero", "address": "Málaga", "phone": "", "type": "hotel"}, + {"name": "Hotel Del Pintor", "address": "Málaga", "phone": "", "type": "hotel"}, + {"name": "Hotel Luzzy", "address": "Málaga", "phone": "", "type": "hotel"}, + {"name": "Hotel Málaga Centro", "address": "Málaga", "phone": "", "type": "hotel"}, + {"name": "Hotel Sercotel Costa de la Luz", "address": "Málaga", "phone": "", "type": "hotel"}, + {"name": "Parador de Málaga Gibralfaro", "address": "Málaga", "phone": "", "type": "parador hotel"}, + {"name": "Hotel ILunion Málaga", "address": "Málaga", "phone": "", "type": "hotel"}, + {"name": "Hotel AXO Little Hight", "address": "Málaga", "phone": "", "type": "hotel"}, + {"name": "Hotel Malak", + "address": "Málaga", + "phone": "", + "type": "hotel"}, + {"name": "Hotel Mariposa", "address": "Fuengirola", "phone": "", "type": "hotel"}, + {"name": "Hotel IPV Palace", "address": "Fuengirola", "phone": "", "type": "hotel"}, + {"name": "Hotel Las Farolas", "address": "Fuengirola", "phone": "", "type": "hotel"}, + {"name": "Hotel Florida", "address": "Fuengirola", "phone": "", "type": "hotel"}, + {"name": "Hotel Monarque Torreblanca", "address": "Fuengirola", "phone": "", "type": "hotel"}, + {"name": "Hotel Ondine", "address": "Estepona", "phone": "", "type": "hotel"} + ] + } + ] +} diff --git a/data/morning_briefs.json b/data/morning_briefs.json index fe51488..5bc56e4 100644 --- a/data/morning_briefs.json +++ b/data/morning_briefs.json @@ -1 +1,450 @@ -[] +[ + { + "id": "morning-1773896519933", + "date": "2026-03-19", + "weather": { + "condition": "Sunny", + "temp": 13, + "feels_like": 13, + "wind": "8km/h SW", + "humidity": "76%", + "precipitation": "0.0mm" + }, + "priorities": [], + "leads": [], + "created_at": "2026-03-19T05:01:59.933Z" + }, + { + "id": "morning-1773892802810", + "date": "2026-03-19", + "weather": "Check weather", + "market": { + "BTC": 71246, + "ETH": 2212, + "SOL": 90 + }, + "priorities": [ + "Focus on SiteMente revenue", + "Fix voice widget API", + "Get first client" + ], + "goal": "$3,000/month", + "leads": [], + "created_at": "2026-03-19T04:00:02.810Z" + }, + { + "id": "morning-1773810085974", + "date": "2026-03-18", + "weather": { + "location": "Benalmádena", + "temp": 14, + "condition": "Overcast", + "humidity": 54, + "wind": 9, + "forecast": "Rain expected later today, clearing tomorrow" + }, + "priorities": [], + "leads": [], + "created_at": "2026-03-18T05:01:25.974Z" + }, + { + "id": "morning-1773806402251", + "date": "2026-03-18", + "weather": "Check weather", + "market": { + "BTC": 74515, + "ETH": 2341, + "SOL": 95 + }, + "priorities": [ + "Focus on SiteMente revenue", + "Fix voice widget API", + "Get first client" + ], + "goal": "$3,000/month", + "leads": [], + "created_at": "2026-03-18T04:00:02.251Z" + }, + { + "id": "morning-1773723629261", + "date": "2026-03-17", + "weather": { + "location": "Benalmádena, Spain", + "current": "Clear", + "temp": 11, + "feelsLike": 10, + "humidity": 68, + "wind": "12 km/h NE", + "sunrise": "07:27", + "sunset": "19:28", + "today_high": 16, + "today_low": 11, + "tomorrow": "Rain expected morning, clearing PM - high 15°C" + }, + "priorities": [], + "leads": [], + "created_at": "2026-03-17T05:00:29.261Z" + }, + { + "id": "morning-1773720002267", + "date": "2026-03-17", + "weather": "Check weather", + "market": { + "BTC": 74410, + "ETH": 2317, + "SOL": 93 + }, + "priorities": [ + "Focus on SiteMente revenue", + "Fix voice widget API", + "Get first client" + ], + "goal": "$3,000/month", + "leads": [], + "created_at": "2026-03-17T04:00:02.267Z" + }, + { + "id": "morning-1773637240086", + "date": "2026-03-16", + "weather": { + "location": "Benalmádena, Málaga", + "current": { + "temp": 13, + "condition": "Clear", + "humidity": 66, + "wind": 9 + }, + "today": { + "min": 13, + "max": 15, + "condition": "Sunny" + }, + "tomorrow": { + "min": 12, + "max": 16, + "condition": "Sunny" + } + }, + "priorities": [], + "leads": [], + "created_at": "2026-03-16T05:00:40.086Z" + }, + { + "id": "morning-1773633602782", + "date": "2026-03-16", + "weather": "Check weather", + "market": { + "BTC": 73668, + "ETH": 2237, + "SOL": 93 + }, + "priorities": [ + "Focus on SiteMente revenue", + "Fix voice widget API", + "Get first client" + ], + "goal": "$3,000/month", + "leads": [], + "created_at": "2026-03-16T04:00:02.782Z" + }, + { + "id": "morning-1773550855351", + "date": "2026-03-15", + "weather": { + "current": { + "temp": 4, + "feelsLike": 2, + "condition": "Cloudy", + "humidity": 75 + }, + "today": { + "high": 14, + "low": 6, + "condition": "Partly cloudy, rain early" + }, + "tomorrow": { + "high": 18, + "low": 5, + "condition": "Sunny" + } + }, + "priorities": [], + "leads": [], + "created_at": "2026-03-15T05:00:55.351Z" + }, + { + "id": "morning-1773547202622", + "date": "2026-03-15", + "weather": "Check weather", + "market": { + "BTC": 71328, + "ETH": 2095, + "SOL": 87 + }, + "priorities": [ + "Focus on SiteMente revenue", + "Fix voice widget API", + "Get first client" + ], + "goal": "$3,000/month", + "leads": [], + "created_at": "2026-03-15T04:00:02.622Z" + }, + { + "id": "morning-1773460802169", + "date": "2026-03-14", + "weather": "Check weather", + "market": { + "BTC": 71075, + "ETH": 2101, + "SOL": 88 + }, + "priorities": [ + "Focus on SiteMente revenue", + "Fix voice widget API", + "Get first client" + ], + "goal": "$3,000/month", + "leads": [], + "created_at": "2026-03-14T04:00:02.169Z" + }, + { + "id": "morning-1773374402444", + "date": "2026-03-13", + "weather": "Check weather", + "market": { + "BTC": 71398, + "ETH": 2118, + "SOL": 89 + }, + "priorities": [ + "Focus on SiteMente revenue", + "Fix voice widget API", + "Get first client" + ], + "goal": "$3,000/month", + "leads": [], + "created_at": "2026-03-13T04:00:02.444Z" + }, + { + "id": "morning-1773288001561", + "date": "2026-03-12", + "weather": "Check weather", + "market": { + "BTC": 69443, + "ETH": 2023, + "SOL": 84 + }, + "priorities": [ + "Focus on SiteMente revenue", + "Fix voice widget API", + "Get first client" + ], + "goal": "$3,000/month", + "leads": [], + "created_at": "2026-03-12T04:00:01.561Z" + }, + { + "id": "morning-1773201601829", + "date": "2026-03-11", + "weather": "Check weather", + "market": { + "BTC": 69558, + "ETH": 2020, + "SOL": 85 + }, + "priorities": [ + "Focus on SiteMente revenue", + "Fix voice widget API", + "Get first client" + ], + "goal": "$3,000/month", + "leads": [], + "created_at": "2026-03-11T04:00:01.829Z" + }, + { + "id": "morning-1773115201870", + "date": "2026-03-10", + "weather": "Check weather", + "market": { + "BTC": 69489, + "ETH": 2027, + "SOL": 85 + }, + "priorities": [ + "Focus on SiteMente revenue", + "Fix voice widget API", + "Get first client" + ], + "goal": "$3,000/month", + "leads": [], + "created_at": "2026-03-10T04:00:01.870Z" + }, + { + "id": "morning-1773028802619", + "date": "2026-03-09", + "weather": "Check weather", + "market": { + "BTC": 67179, + "ETH": 1976, + "SOL": 83 + }, + "priorities": [ + "Focus on SiteMente revenue", + "Fix voice widget API", + "Get first client" + ], + "goal": "$3,000/month", + "leads": [], + "created_at": "2026-03-09T04:00:02.620Z" + }, + { + "id": "morning-1772942402314", + "date": "2026-03-08", + "weather": "Check weather", + "market": { + "BTC": 67052, + "ETH": 1948, + "SOL": 82 + }, + "priorities": [ + "Focus on SiteMente revenue", + "Fix voice widget API", + "Get first client" + ], + "goal": "$3,000/month", + "leads": [], + "created_at": "2026-03-08T04:00:02.314Z" + }, + { + "id": "morning-1772856002342", + "date": "2026-03-07", + "weather": "Check weather", + "market": { + "BTC": 68137, + "ETH": 1980, + "SOL": 84 + }, + "priorities": [ + "Focus on SiteMente revenue", + "Fix voice widget API", + "Get first client" + ], + "goal": "$3,000/month", + "leads": [], + "created_at": "2026-03-07T04:00:02.342Z" + }, + { + "id": "morning-1772769602040", + "date": "2026-03-06", + "weather": "Check weather", + "market": { + "BTC": 71054, + "ETH": 2081, + "SOL": 88 + }, + "priorities": [ + "Focus on SiteMente revenue", + "Fix voice widget API", + "Get first client" + ], + "goal": "$3,000/month", + "leads": [], + "created_at": "2026-03-06T04:00:02.040Z" + }, + { + "id": "morning-1772683202243", + "date": "2026-03-05", + "weather": "Check weather", + "market": { + "BTC": 72494, + "ETH": 2120, + "SOL": 90 + }, + "priorities": [ + "Focus on SiteMente revenue", + "Fix voice widget API", + "Get first client" + ], + "goal": "$3,000/month", + "leads": [], + "created_at": "2026-03-05T04:00:02.243Z" + }, + { + "id": "morning-1772596802681", + "date": "2026-03-04", + "weather": "Check weather", + "market": { + "BTC": 67617, + "ETH": 1956, + "SOL": 85 + }, + "priorities": [ + "Focus on SiteMente revenue", + "Fix voice widget API", + "Get first client" + ], + "goal": "$3,000/month", + "leads": [], + "created_at": "2026-03-04T04:00:02.681Z" + }, + { + "id": "morning-1772514120505", + "date": "2026-03-03", + "weather": { + "temp": 9.4, + "condition": "light rain", + "wind": 6.4 + }, + "priorities": [], + "leads": [], + "created_at": "2026-03-03T05:02:00.505Z" + }, + { + "id": "morning-1772510401919", + "date": "2026-03-03", + "weather": "Check weather", + "market": { + "BTC": 68359, + "ETH": 2008, + "SOL": 86 + }, + "priorities": [ + "Focus on SiteMente revenue", + "Fix voice widget API", + "Get first client" + ], + "goal": "$3,000/month", + "leads": [], + "created_at": "2026-03-03T04:00:01.919Z" + }, + { + "id": "morning-1772254807319", + "date": "2026-02-28", + "weather": { + "temp": 12, + "condition": "clear" + }, + "priorities": [], + "leads": [], + "created_at": "2026-02-28T05:00:07.319Z" + }, + { + "id": "morning-1772251202213", + "date": "2026-02-28", + "weather": "Check weather", + "market": { + "BTC": 65787, + "ETH": 1923, + "SOL": 81 + }, + "priorities": [ + "Focus on SiteMente revenue", + "Fix voice widget API", + "Get first client" + ], + "goal": "$3,000/month", + "leads": [], + "created_at": "2026-02-28T04:00:02.213Z" + } +] \ No newline at end of file diff --git a/data/recurring-templates.json b/data/recurring-templates.json index 5da6a10..5954ca0 100644 --- a/data/recurring-templates.json +++ b/data/recurring-templates.json @@ -36,9 +36,9 @@ "priority": "critical" }, "preInstructions": "Report any issues immediately to Horus", - "enabled": false, - "runCount": 2, - "lastRun": "2026-02-27T17:53:03.836Z" + "enabled": true, + "runCount": 3, + "lastRun": "2026-02-27T22:04:10.446Z" }, { "id": "seshat-content-pipeline", @@ -80,7 +80,8 @@ }, "preInstructions": "Focus on Benalmádena and Costa del Sol area", "enabled": false, - "runCount": 0 + "runCount": 1, + "lastRun": "2026-02-27T22:04:20.745Z" }, { "id": "thoth-trading-market", diff --git a/data/research.json b/data/research.json new file mode 100644 index 0000000..853c626 --- /dev/null +++ b/data/research.json @@ -0,0 +1,50 @@ +[ + { + "id": "research-001", + "name": "OpenClaw Top Skills", + "query": "OpenClaw best skills 2024 trending github", + "results_count": 20, + "created_at": "2026-03-02T15:30:00Z", + "saved": true, + "summary": "Top: awesome-openclaw-skills, VoltAgent, BankrBot skills, security skills", + "results": [ + {"title": "awesome-openclaw-skills (913 skills)", "url": "https://github.com/sundial-org/awesome-openclaw-skills", "score": 0.9999, "content": "A curated collection of the top agent skills from OpenClaw, regularly updated with the most popular and useful skills."}, + {"title": "VoltAgent/awesome-openclaw-skills (5400+ skills)", "url": "https://github.com/VoltAgent/awesome-openclaw-skills", "score": 0.9998, "content": "The awesome collection of OpenClaw skills. 5400+ skills filtered and categorized from the official OpenClaw Skills Registry."}, + {"title": "OpenClaw Skills Directory (10,000+ skills)", "url": "https://openclawskills.best/", "score": 0.9989, "content": "OpenClaw Skills Directory - 10,000+ Skills, Updated Daily."}, + {"title": "BankrBot/openclaw-skills", "url": "https://github.com/BankrBot/openclaw-skills", "score": 0.9987, "content": "Bankr Skills equip builders with plug-and-play tools to build more powerful agents."}, + {"title": "Security Skills - ClawSec", "url": "https://github.com/clawsec", "score": 0.9800, "content": "Security skill suite for OpenClaw - drift detection, audits, integrity verification."} + ] + }, + { + "id": "research-002", + "name": "Ollama Free Use Cases", + "query": "Ollama free local AI use cases business productivity", + "results_count": 20, + "created_at": "2026-03-02T15:31:00Z", + "saved": true, + "summary": "Business automation, customer support, coding assistants, data analysis", + "results": [ + {"title": "Revolutionizing Small Business with Ollama", "url": "https://www.raiaai.com/blogs/revolutionizing-small-business-operations-with-ollama-and-ai-agents", "score": 0.9999, "content": "How small businesses use Ollama for automation, customer service, and productivity."}, + {"title": "Complete Guide to Local AI Models", "url": "https://www.thundercompute.com/blog/what-is-ollama-run-ai-models-locally", "score": 0.9999, "content": "Run Llama, Mistral, Codestral locally - zero API costs."}, + {"title": "Build Your Free Personal AI Agent", "url": "https://atalupadhyay.wordpress.com/2026/03/01/build-your-own-free-personal-ai-agent-using-qwen-3-5-ollama/", "score": 0.9997, "content": "Build AI agents locally with Ollama - no cloud fees."}, + {"title": "Your Machine Your AI - Local Productivity Stack", "url": "https://blog.dataengineerthings.org/your-machine-your-ai-the-ultimate-local-productivity-stack-with-ollama-7a118f271479", "score": 0.9998, "content": "The ultimate local productivity stack with Ollama."}, + {"title": "Ollama Advanced Use Cases", "url": "https://www.cohorte.co/blog/ollama-advanced-use-cases-and-integrations", "score": 0.9998, "content": "Advanced use cases and integrations for Ollama."} + ] + }, + { + "id": "research-003", + "name": "AI Agent Tools Alternatives", + "query": "AI agent tools integration API Tavily alternative web search automation 2024", + "results_count": 20, + "created_at": "2026-03-02T15:32:00Z", + "saved": true, + "summary": "Tavily, Brave, Exa, Serper, Firecrawl, Apify, Scrapeless", + "results": [ + {"title": "Beyond Tavily - Complete Guide to AI Search APIs", "url": "https://websearchapi.ai/blog/tavily-alternatives", "score": 0.6714, "content": "Complete comparison of AI search APIs for agents."}, + {"title": "Best Tavily Alternatives: Scrapeless", "url": "https://www.scrapeless.com/en/wiki/tavily-alternatives", "score": 0.6514, "content": "Web scraping and data extraction alternatives."}, + {"title": "7 Free Web Search APIs for AI Agents", "url": "https://www.kdnuggets.com/7-free-web-search-apis-for-ai-agents", "score": 0.5863, "content": "Free options for AI agent web search."}, + {"title": "Best APIs for Autonomous Agents 2025", "url": "https://fast.io/resources/best-apis-autonomous-agents/", "score": 0.5217, "content": "Top tool recommendations for autonomous agents."}, + {"title": "Firecrawl - AI Data Extraction", "url": "https://www.firecrawl.dev/blog/tavily-alternatives", "score": 0.3870, "content": "Alternative for web scraping and content extraction."} + ] + } +] diff --git a/leads/marketing-emails/2026-03-18-vip-standard-email.html b/leads/marketing-emails/2026-03-18-vip-standard-email.html new file mode 100644 index 0000000..6e149ac --- /dev/null +++ b/leads/marketing-emails/2026-03-18-vip-standard-email.html @@ -0,0 +1,81 @@ + + + +

Your competitors are using AI. Here's proof.

+ +

Hi {{ contact.FIRSTNAME }},

+ +

It's been a while since we last reached out. We've been busy building AI agents that are transforming how local businesses handle calls, leads, and websites.

+ +

Here's what businesses like yours and your competitors are doing online right now:

+ +
    +
  • ✅ AI agents answering every call - 24/7, never miss a lead
  • +
  • ✅ Smart chatbots qualifying website visitors - instant responses
  • +
  • ✅ Competitor monitoring - know what rivals are doing before they do it
  • +
  • ✅ Automated lead follow-up - never let a prospect go cold
  • +
+ +

We're not sharing this to alarm you.

+ +

We're sharing it because this is now the baseline. This is what average looks like in 2026.

+ +

Our offer:

+ +
    +
  • Single Agent: $1,997 setup + $797/month
  • +
  • Multi-Agent: $4,997 setup + $1,797/month
  • +
  • Founding Client Bonus: Reputation Manager ($297/mo value) FREE
  • +
+ +

60-Day Guarantee: If you don't see results, next month is free.

+ +

Founding Client Pricing closes April 30, 2026.

+ + + + + + +
+ + See Pricing + +
+ +

If you're curious what this would look like for your business, just hit reply and tell us what you do—we'll recommend the best setup in plain English.

+ +

The HostPioneers Team

+ +

💡 Need a new website? We build sites with AI agents included.

+ +
+





+ + + + +
+ + + + +
+ This email was sent to {{ contact.EMAIL }}
+ unsubscribe from this list +      + update subscription preferences +

+
+
+ + +
+ + diff --git a/public/extensions/hookd.zip b/public/extensions/hookd.zip new file mode 100644 index 0000000000000000000000000000000000000000..2aa1754c1013d659f68ab2ba78b5333476534cd3 GIT binary patch literal 8811 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 Z;Q!=5xTmoF**!%91V82O36h^r{{s_IOuhgB literal 0 HcmV?d00001 diff --git a/public/extensions/hookd/README.md b/public/extensions/hookd/README.md new file mode 100644 index 0000000..0c7f0ce --- /dev/null +++ b/public/extensions/hookd/README.md @@ -0,0 +1,78 @@ +# Hookd - Chrome Extension + +Save, organize and AI-sort your X (Twitter) content. + +## Features + +- 📥 **DM Organizer** - Extract and categorize DMs +- 🐦 **Feed Saver** - Checkmark posts to save in bulk +- 🏷️ **Custom Categories** - AI, News, Ideas, Memes, Other +- 🤖 **AI Processing Ready** - Connect to OpenClaw for AI analysis +- 👥 **Contact Lists** - Quick share to friends/family lists +- 💾 **Export** - Save your data anytime + +## Installation + +1. Open Chrome and go to `chrome://extensions/` +2. Enable "Developer mode" (toggle in top right) +3. Click "Load unpacked" +4. Select the `hookd` folder +5. Click the extension icon and pin it to toolbar + +## OpenClaw Integration + +To enable AI processing: + +1. Install OpenClaw Browser Relay extension +2. Configure connection in extension settings (⚙️ tab) +3. AI will automatically: + - Read saved content + - Suggest categories + - Score by relevance + - Delete spam + +## Categories + +- 💡 **AI** - AI tools, news, developments +- 📰 **News** - Breaking news, trends +- 💡 **Ideas** - Inspiration, ideas, concepts +- 😂 **Memes** - Fun content +- 📁 **Other** - Uncategorized + +## Usage + +### Saving DMs +1. Open X DMs +2. Hover over a message +3. Click 📌 to save + +### Saving Feed Posts +1. Open X home/timeline +2. Hover over any tweet +3. Click the ○ checkbox on left +4. Select multiple tweets +5. Click "Save to Hookd" in floating bar + +### Viewing Saved +1. Click Hookd extension icon +2. Go to 💾 Saved tab + +### AI Processing +1. Go to 🤖 AI tab +2. Click "Process All with AI" +3. AI will analyze and categorize + +## Files + +- `manifest.json` - Extension config +- `popup.html/js` - Popup UI +- `background.js` - Service worker +- `content.js/css` - X.com page interaction + +## TODO + +- [ ] Connect to OpenClaw Browser Relay +- [ ] Add X API integration (alternative to scraping) +- [ ] FB/IG support +- [ ] Cloud sync +- [ ] Mobile companion diff --git a/public/extensions/hookd/background.js b/public/extensions/hookd/background.js new file mode 100644 index 0000000..9f1e98a --- /dev/null +++ b/public/extensions/hookd/background.js @@ -0,0 +1,159 @@ +// Service Worker - Hookd Extension + +const STORAGE_KEY = 'hookd_items'; +const SETTINGS_KEY = 'hookd_settings'; +const LISTS_KEY = 'hookd_lists'; + +const defaultSettings = { + categories: ['AI', 'News', 'Ideas', 'Memes', 'Other'], + userHandle: 'HaithamEKhalifa', + relayEnabled: true +}; + +const defaultLists = [ + { id: 'dev', name: 'Dev Friends', emoji: '👨‍💻', contacts: [] }, + { id: 'memes', name: 'Meme Buddies', emoji: '😂', contacts: [] }, + { id: 'business', name: 'Business', emoji: '💼', contacts: [] }, + { id: 'family', name: 'Family', emoji: '👨‍👩‍👧', contacts: [] } +]; + +// Initialize +chrome.runtime.onInstalled.addListener(() => { + console.log('Hookd installed'); + chrome.storage.local.set({ + [STORAGE_KEY]: [], + [SETTINGS_KEY]: defaultSettings, + [LISTS_KEY]: defaultLists + }); +}); + +// Handle messages +chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { + // Get items + if (message.type === 'GET_ITEMS') { + chrome.storage.local.get([STORAGE_KEY], (r) => sendResponse(r[STORAGE_KEY] || [])); + return true; + } + + // Get lists + if (message.type === 'GET_LISTS') { + chrome.storage.local.get([LISTS_KEY], (r) => sendResponse(r[LISTS_KEY] || defaultLists)); + return true; + } + + // Add contact to list + if (message.type === 'ADD_CONTACT') { + chrome.storage.local.get([LISTS_KEY], (r) => { + const lists = r[LISTS_KEY] || defaultLists; + const list = lists.find(l => l.id === message.listId); + if (list && !list.contacts.find(c => c.username === message.contact.username)) { + list.contacts.push(message.contact); + chrome.storage.local.set({ [LISTS_KEY]: lists }); + } + sendResponse({ success: true }); + }); + return true; + } + + // Remove contact from list + if (message.type === 'REMOVE_CONTACT') { + chrome.storage.local.get([LISTS_KEY], (r) => { + const lists = r[LISTS_KEY] || defaultLists; + const list = lists.find(l => l.id === message.listId); + if (list) { + list.contacts = list.contacts.filter(c => c.username !== message.username); + chrome.storage.local.set({ [LISTS_KEY]: lists }); + } + sendResponse({ success: true }); + }); + return true; + } + + // Create new list + if (message.type === 'CREATE_LIST') { + chrome.storage.local.get([LISTS_KEY], (r) => { + const lists = r[LISTS_KEY] || defaultLists; + lists.push({ + id: Date.now().toString(), + name: message.name, + emoji: message.emoji || '📌', + contacts: [] + }); + chrome.storage.local.set({ [LISTS_KEY]: lists }); + sendResponse({ success: true }); + }); + return true; + } + + // Delete list + if (message.type === 'DELETE_LIST') { + chrome.storage.local.get([LISTS_KEY], (r) => { + let lists = r[LISTS_KEY] || defaultLists; + lists = lists.filter(l => l.id !== message.listId); + chrome.storage.local.set({ [LISTS_KEY]: lists }); + sendResponse({ success: true }); + }); + return true; + } + + // Share item to list + if (message.type === 'SHARE_TO_LIST') { + chrome.storage.local.get([STORAGE_KEY], (r) => { + const items = r[STORAGE_KEY] || []; + // Mark item as shared + const item = items.find(i => i.id === message.itemId); + if (item) { + item.sharedTo = item.sharedTo || []; + item.sharedTo.push(message.listId); + chrome.storage.local.set({ [STORAGE_KEY]: items }); + } + sendResponse({ success: true }); + }); + return true; + } + + // Save item + if (message.type === 'SAVE_ITEM') { + chrome.storage.local.get([STORAGE_KEY], (r) => { + const items = r[STORAGE_KEY] || []; + const newItem = { + id: Date.now(), + ...message.data, + type: message.data.source || 'dm', + saved: false, + aiProcessed: false, + sharedTo: [], + time: new Date().toISOString() + }; + items.unshift(newItem); + chrome.storage.local.set({ [STORAGE_KEY]: items }); + sendResponse({ success: true, item: newItem }); + }); + return true; + } + + // Delete items + if (message.type === 'DELETE_ITEMS') { + chrome.storage.local.get([STORAGE_KEY], (r) => { + const items = r[STORAGE_KEY] || []; + const remaining = items.filter(i => !message.ids.includes(i.id)); + chrome.storage.local.set({ [STORAGE_KEY]: remaining }); + sendResponse({ success: true, deleted: message.ids.length }); + }); + return true; + } + + // Update item + if (message.type === 'UPDATE_ITEM') { + chrome.storage.local.get([STORAGE_KEY], (r) => { + const items = r[STORAGE_KEY] || []; + const idx = items.findIndex(i => i.id === message.data.id); + if (idx !== -1) { + items[idx] = { ...items[idx], ...message.data }; + chrome.storage.local.set({ [STORAGE_KEY]: items }); + } + sendResponse({ success: true }); + }); + return true; + } +}); diff --git a/public/extensions/hookd/content.css b/public/extensions/hookd/content.css new file mode 100644 index 0000000..32a5b38 --- /dev/null +++ b/public/extensions/hookd/content.css @@ -0,0 +1,28 @@ +/* Content script styles for X.com */ + +/* Hover effect on tweets */ +[data-testid="tweet"]:hover .hookd-checkbox { + display: flex !important; +} + +/* Save button hover */ +.hookd-save-btn:hover { + opacity: 1 !important; + transform: scale(1.1); +} + +/* Notification animation */ +@keyframes hookd-slide-in { + from { + transform: translateY(100%); + opacity: 0; + } + to { + transform: translateY(0); + opacity: 1; + } +} + +#hookd-floating-bar { + animation: hookd-slide-in 0.3s ease-out; +} diff --git a/public/extensions/hookd/content.js b/public/extensions/hookd/content.js new file mode 100644 index 0000000..e23c138 --- /dev/null +++ b/public/extensions/hookd/content.js @@ -0,0 +1,190 @@ +// Content Script - runs on X.com +// Adds "Save to Hookd" option to tweet/post menu + +console.log('Hookd content script loaded'); + +// Listen for messages from background +chrome.runtime.onMessage.addListener((msg, sender, response) => { + if (msg.type === 'GET_SAVED') { + response([]); + } +}); + +// Add "Save to Hookd" to X's menu +function addHookdOption() { + // Find all "more" buttons (the ... button on tweets/posts) + document.querySelectorAll('[data-testid="tweet"] [role="button"], [data-testid="tweet"] [aria-label*="more"], [data-testid="tweet"] [aria-label*="More"]').forEach(menuBtn => { + // Check if we already added our option + if (menuBtn.closest('[data-testid="tweet"]')?.querySelector('.hookd-menu-option')) continue; + + const tweet = menuBtn.closest('[data-testid="tweet"]'); + if (!tweet) return; + + // Create save button + const saveBtn = document.createElement('div'); + saveBtn.className = 'hookd-menu-option'; + saveBtn.style.cssText = ` + padding: 12px 16px; + cursor: pointer; + display: flex; + align-items: center; + gap: 12px; + font-size: 15px; + color: #fff; + `; + saveBtn.innerHTML = '📌Save to Hookd'; + + saveBtn.addEventListener('click', (e) => { + e.stopPropagation(); + saveTweet(tweet); + }); + + // Find the dropdown menu and append + const dropdown = menuBtn.closest('[role="menu"]') || menuBtn.closest('div[aria-labelledby]'); + if (dropdown && !dropdown.querySelector('.hookd-menu-option')) { + dropdown.appendChild(saveBtn); + } + }); + + // Also try to find DM more options + document.querySelectorAll('[data-testid="DMMessage"] [role="button"]').forEach(btn => { + if (btn.textContent.includes('more') || btn.getAttribute('aria-label')?.includes('more')) { + const dm = btn.closest('[data-testid="DMMessage"]'); + if (dm && !dm.querySelector('.hookd-menu-option')) { + const saveBtn = document.createElement('div'); + saveBtn.className = 'hookd-menu-option'; + saveBtn.style.cssText = ` + padding: 8px 12px; + cursor: pointer; + display: flex; + align-items: center; + gap: 8px; + font-size: 13px; + `; + saveBtn.innerHTML = '📌 Save to Hookd'; + + saveBtn.addEventListener('click', (e) => { + e.stopPropagation(); + saveDM(dm); + }); + + // Insert after the clickable area + btn.parentElement?.appendChild(saveBtn); + } + } + }); +} + +// Save tweet +function saveTweet(tweetElement) { + try { + // Extract author + const authorEl = tweetElement.querySelector('[data-testid="User-Name"] span a[role="link"]') || + tweetElement.querySelector('a[tabindex="-1"]'); + const author = authorEl?.textContent?.trim()?.replace('@', '') || 'unknown'; + + // Extract content + const contentEl = tweetElement.querySelector('[data-testid="tweetText"]'); + const content = contentEl?.textContent?.trim() || ''; + + // Extract link + const linkEl = tweetElement.querySelector('a[href*="/status/"]'); + const link = linkEl?.href || ''; + + const item = { + id: Date.now(), + author: author, + content: content.substring(0, 500), + link: link, + source: 'tweet', + time: new Date().toISOString() + }; + + // Save to storage via background + chrome.runtime.sendMessage({ + type: 'SAVE_ITEM', + data: item + }, (response) => { + if (response?.success) { + showNotification('Saved to Hookd!'); + } else { + showNotification('Failed to save'); + } + }); + } catch (err) { + console.error('Hookd save error:', err); + showNotification('Error saving'); + } +} + +// Save DM +function saveDM(dmElement) { + try { + const contentEl = dmElement.querySelector('[data-testid="dmMessageContent"]'); + const content = contentEl?.textContent?.trim() || ''; + + const item = { + id: Date.now(), + author: 'DM', + content: content.substring(0, 500), + source: 'dm', + time: new Date().toISOString() + }; + + chrome.runtime.sendMessage({ + type: 'SAVE_ITEM', + data: item + }, (response) => { + if (response?.success) { + showNotification('Saved to Hookd!'); + } else { + showNotification('Failed to save'); + } + }); + } catch (err) { + console.error('Hookd save error:', err); + } +} + +// Show notification +function showNotification(message) { + const notif = document.createElement('div'); + notif.style.cssText = ` + position: fixed; + bottom: 20px; + right: 20px; + background: #4F46E5; + color: white; + padding: 12px 20px; + border-radius: 8px; + font-family: Arial, sans-serif; + font-size: 14px; + z-index: 99999; + box-shadow: 0 4px 12px rgba(0,0,0,0.3); + `; + notif.textContent = message; + document.body.appendChild(notif); + + setTimeout(() => { + notif.style.opacity = '0'; + notif.style.transition = 'opacity 0.3s'; + setTimeout(() => notif.remove(), 300); + }, 2000); +} + +// Watch for new elements +const observer = new MutationObserver(() => { + addHookdOption(); +}); + +// Start observing +observer.observe(document.body, { + childList: true, + subtree: true +}); + +// Initial scan +setTimeout(addHookdOption, 1000); +setTimeout(addHookdOption, 3000); + +console.log('Hookd content script initialized'); diff --git a/public/extensions/hookd/icons/icon128.png b/public/extensions/hookd/icons/icon128.png new file mode 100644 index 0000000000000000000000000000000000000000..5f39d053310570ffb54e06e58348aa36e95fa7f5 GIT binary patch literal 552 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H1|$#LC7xzrV07_xaSW+oe0ysnFM|RPv%~$r z)7!Xt&ct%JY`D(4fd!}#4wyfzWrcGfj0gHm73gAXm~Nm8?PkbhJfMfJR|qJCp;U%x z2P?yFG^GvB4BHtJ#L+}8SPHlq${5i^+p*dua6os|uI&u}7*)8qKk~32wFHHlr>mdK II;Vst0NzPJy#N3J literal 0 HcmV?d00001 diff --git a/public/extensions/hookd/icons/icon16.png b/public/extensions/hookd/icons/icon16.png new file mode 100644 index 0000000000000000000000000000000000000000..37fe95250da627bbd765a2143f57d7d41733e112 GIT binary patch literal 121 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`_MR?|Ar}70DG3Qb&O0zB$XqqZ zkveeV#EAnyPEtzBACMrB21_DnFjuNfih*G(H_K=3@N3OLvlu*G{an^LB{Ts54UHy? literal 0 HcmV?d00001 diff --git a/public/extensions/hookd/icons/icon48.png b/public/extensions/hookd/icons/icon48.png new file mode 100644 index 0000000000000000000000000000000000000000..ab6173f70cf369e46e8c659a1a423785135977a3 GIT binary patch literal 272 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA1|-9oezpUt2c9mDAsNnZXKv&@WFX@DUp#@u z%ps9^-T}TG2ki%JW)6(4ya5gA4vcKl3mDWTwj5y7aNuX2@OHwx8+~(cOnQ5xQ0Df} z$u~ZFZxj8%_k%gZihT#0%`pq}hVTQ*4{kDl;Q7&2n8&cb!JYqr@dNE+I~n&d$=NcT wZ&)w!ob3Zw#TNz}=6y+Xqz`C6xJXsS7GIg`81wa#9DsiGboFyt=akR{0L`9f9smFU literal 0 HcmV?d00001 diff --git a/public/extensions/hookd/manifest.json b/public/extensions/hookd/manifest.json new file mode 100644 index 0000000..fc0209a --- /dev/null +++ b/public/extensions/hookd/manifest.json @@ -0,0 +1,11 @@ +{ + "manifest_version": 3, + "name": "Hookd", + "version": "1.1.0", + "description": "Save, organize and AI-sort your X content", + "permissions": ["storage"], + "host_permissions": ["https://x.com/*", "https://twitter.com/*"], + "action": { + "default_popup": "popup.html" + } +} diff --git a/public/extensions/hookd/popup.html b/public/extensions/hookd/popup.html new file mode 100644 index 0000000..36c23b4 --- /dev/null +++ b/public/extensions/hookd/popup.html @@ -0,0 +1,73 @@ + + + + +Hookd + + + +

Hookd

+ +
+
INBOX
+
LISTS
+
SET
+
+ +
+
+

INBOX (0)

+
+
No items.
+ +
+ +
+

LISTS

+
+
+ + +
+
+ +
+

SETTINGS

+
+ + +
+
+ +
+
+
+ + + + diff --git a/public/extensions/hookd/popup.js b/public/extensions/hookd/popup.js new file mode 100644 index 0000000..acabd92 --- /dev/null +++ b/public/extensions/hookd/popup.js @@ -0,0 +1,180 @@ +// State +var items = []; +var lists = [ + { id: '1', name: 'Dev Friends', contacts: [] }, + { id: '2', name: 'Meme Buddies', contacts: [] }, + { id: '3', name: 'Business', contacts: [] } +]; + +// Load data +function loadData() { + chrome.storage.local.get(['hookd_items', 'hookd_lists'], function(result) { + items = result.hookd_items || []; + lists = result.hookd_lists || [ + { id: '1', name: 'Dev Friends', contacts: [] }, + { id: '2', name: 'Meme Buddies', contacts: [] }, + { id: '3', name: 'Business', contacts: [] } + ]; + renderAll(); + }); +} + +// Save items +function saveItems() { + chrome.storage.local.set({ hookd_items: items }); +} + +// Render all +function renderAll() { + renderInbox(); + renderLists(); + loadHandle(); +} + +// Tab switching +function switchTab(tabId) { + var contents = document.querySelectorAll('.tab-content'); + for (var i = 0; i < contents.length; i++) { + contents[i].classList.remove('active'); + } + var tabs = document.querySelectorAll('.tab'); + for (var i = 0; i < tabs.length; i++) { + tabs[i].classList.remove('active'); + } + document.getElementById(tabId).classList.add('active'); + document.getElementById('tab-' + tabId).classList.add('active'); +} + +// Render inbox +function renderInbox() { + document.getElementById('inbox-count').textContent = '(' + items.length + ')'; + var list = document.getElementById('inbox-list'); + var empty = document.getElementById('inbox-empty'); + + if (items.length === 0) { + list.innerHTML = ''; + empty.style.display = 'block'; + } else { + empty.style.display = 'none'; + var html = ''; + for (var i = 0; i < items.length; i++) { + html += '
' + + '
@' + (items[i].author || 'unknown') + '
' + + '
' + ((items[i].content || 'No content').substring(0, 60)) + '
' + + '' + + '
'; + } + list.innerHTML = html; + + // Add delete handlers + var deleteBtns = list.querySelectorAll('.delete-btn'); + for (var i = 0; i < deleteBtns.length; i++) { + deleteBtns[i].addEventListener('click', function() { + var idx = parseInt(this.getAttribute('data-index')); + items.splice(idx, 1); + saveItems(); + renderInbox(); + }); + } + } +} + +// Test add item +function testAddItem() { + items.unshift({ + id: Date.now(), + author: 'TestUser', + content: 'Test item saved!', + time: new Date().toISOString() + }); + saveItems(); + renderInbox(); +} + +// Render lists +function renderLists() { + var container = document.getElementById('lists-container'); + var html = ''; + for (var i = 0; i < lists.length; i++) { + html += '
' + + '' + lists[i].name + ' (' + lists[i].contacts.length + ')' + + '' + + '
'; + } + container.innerHTML = html; + + // Add delete handlers + var deleteBtns = container.querySelectorAll('.delete-btn'); + for (var i = 0; i < deleteBtns.length; i++) { + deleteBtns[i].addEventListener('click', function() { + var idx = parseInt(this.getAttribute('data-list-index')); + lists.splice(idx, 1); + chrome.storage.local.set({ hookd_lists: lists }); + renderLists(); + }); + } +} + +// Create list +function createList() { + var input = document.getElementById('new-list-name'); + var name = input.value.trim(); + if (!name) return; + + lists.push({ + id: String(Date.now()), + name: name, + contacts: [] + }); + chrome.storage.local.set({ hookd_lists: lists }); + input.value = ''; + renderLists(); +} + +// Load handle +function loadHandle() { + chrome.storage.local.get(['hookd_handle'], function(result) { + if (result.hookd_handle) { + document.getElementById('setting-handle').value = result.hookd_handle; + } + }); +} + +// Save handle +function saveHandle() { + var handle = document.getElementById('setting-handle').value.trim(); + if (!handle) return; + chrome.storage.local.set({ hookd_handle: handle }); + alert('Saved: ' + handle); +} + +// Clear all +function clearAll() { + if (!confirm('Delete ALL data?')) return; + items = []; + lists = [ + { id: '1', name: 'Dev Friends', contacts: [] }, + { id: '2', name: 'Meme Buddies', contacts: [] }, + { id: '3', name: 'Business', contacts: [] } + ]; + chrome.storage.local.set({ hookd_items: [], hookd_lists: lists }); + renderAll(); + alert('All data cleared'); +} + +// Setup +document.addEventListener('DOMContentLoaded', function() { + // Tab clicks + document.getElementById('tab-inbox').addEventListener('click', function() { switchTab('inbox'); }); + document.getElementById('tab-lists').addEventListener('click', function() { switchTab('lists'); }); + document.getElementById('tab-settings').addEventListener('click', function() { switchTab('settings'); }); + + // Buttons + document.getElementById('btn-test-add').addEventListener('click', testAddItem); + document.getElementById('btn-create-list').addEventListener('click', createList); + document.getElementById('btn-save-handle').addEventListener('click', saveHandle); + document.getElementById('btn-clear-all').addEventListener('click', clearAll); + + // Load data + loadData(); +}); diff --git a/public/hookd-107.zip b/public/hookd-107.zip new file mode 100644 index 0000000000000000000000000000000000000000..16e5cff51cd5e667ce44d6c12ec223ab1bcef8b8 GIT binary patch literal 9564 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 h(fyUCK?3--1pl}A!98x