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

+
+ 📬 View 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:

+ + + +

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:

+ + + +

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 0000000..2aa1754 Binary files /dev/null and b/public/extensions/hookd.zip differ 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 0000000..5f39d05 Binary files /dev/null and b/public/extensions/hookd/icons/icon128.png differ diff --git a/public/extensions/hookd/icons/icon16.png b/public/extensions/hookd/icons/icon16.png new file mode 100644 index 0000000..37fe952 Binary files /dev/null and b/public/extensions/hookd/icons/icon16.png differ diff --git a/public/extensions/hookd/icons/icon48.png b/public/extensions/hookd/icons/icon48.png new file mode 100644 index 0000000..ab6173f Binary files /dev/null and b/public/extensions/hookd/icons/icon48.png differ 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 0000000..16e5cff Binary files /dev/null and b/public/hookd-107.zip differ diff --git a/public/hookd.zip b/public/hookd.zip new file mode 100644 index 0000000..6cd404f Binary files /dev/null and b/public/hookd.zip differ