Compare commits
53 Commits
0d5205ac94
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| d4c8760007 | |||
| b8fc96ea0a | |||
| 966441e6aa | |||
| 43d66fd295 | |||
| 5afda4e953 | |||
| c6d3e2516e | |||
| 3ac4e9bb4a | |||
| eb2f6802fd | |||
| 8bb70a9af0 | |||
| 8755db3305 | |||
| 2fa1a5eb3a | |||
| 75fc2ccc3b | |||
| e44d99c7e7 | |||
| dedec93e23 | |||
| 9a120ab0d6 | |||
| 6079039f21 | |||
| 2cf4dcbdae | |||
| e9dc4a6a0d | |||
| f488a103c8 | |||
| df5eeeb00c | |||
| b581601064 | |||
| 77bc63de24 | |||
| a86684f6d1 | |||
| b82c04ce51 | |||
| df20b2fb51 | |||
| 77aaa9989e | |||
| c5a862f7ba | |||
| 3bf0060439 | |||
| a5b3b9d36f | |||
| 440cc084d3 | |||
| f9a263dd8c | |||
| fdc1a1a215 | |||
| 10ac10a92a | |||
| a9ea122ac5 | |||
| f865d549d1 | |||
| 3215694f5c | |||
| 8594393664 | |||
| b0dde481cf | |||
| 5172bfe62c | |||
| d518ad2ae4 | |||
| 62c4aee24c | |||
| d92c72a6cd | |||
| 3005e53d6e | |||
| afb5a2ac20 | |||
| 3afcf77a16 | |||
| e542bbd44a | |||
| a626defcfd | |||
| b2cb2a1a8e | |||
| 3549e8757e | |||
| 2b2baf6851 | |||
| e649373118 | |||
| f3cb8ca848 | |||
| acec840d91 |
@@ -1,50 +0,0 @@
|
|||||||
# NGO Website — ODOO4projects
|
|
||||||
|
|
||||||
**Project:** NGO marketing homepage and blog
|
|
||||||
**URL:** [ngo.odoo4projects.com](https://ngo.odoo4projects.com)
|
|
||||||
**Stack:** Static HTML · Tailwind CSS · Vanilla JS
|
|
||||||
|
|
||||||
---
|
|
||||||
## BLOG POSTS
|
|
||||||
Blog posts live in `blog.json` — do not create separate backup files.
|
|
||||||
To add a blog post customize this command
|
|
||||||
jq '.arrays[0] = [$NEW_JSON] + .arrays[0]' blog.json > tmp.json && mv tmp.json blog.json
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Repository Rules
|
|
||||||
- Do not touch this readme
|
|
||||||
- Keep the seperation of Site and the blogposts in blog.json
|
|
||||||
Example of JSON structure for blog.json
|
|
||||||
{
|
|
||||||
"area": "Transparency",
|
|
||||||
"title": "Every Peso on the Page: Ana's Ledger Story",
|
|
||||||
"teaser": "<p>Ana replaced five notebooks with a four-stop impact ledger and now answers donor questions in seconds.</p>",
|
|
||||||
"content": "<h3>Every peso needs a map</h3>\n<p>Ana used to juggle five notebooks and still felt blind when a donor asked where their coin landed.</p>\n<h3>Pressure points</h3>\n<p without late nights.</p>\n",
|
|
||||||
"date": "2026-03-22"
|
|
||||||
},
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Page Structure
|
|
||||||
|
|
||||||
| # | Block | Purpose |
|
|
||||||
|---|-------|---------|
|
|
||||||
| 1 | **Hero** | Headline, subheadline, free-trial CTA, key stats |
|
|
||||||
| 2 | **Hot NGO Topics** | Three focused topic cards (transparency, communications, fundraising) |
|
|
||||||
| 3 | **Blog** | Latest articles loaded dynamically from `blog.json` |
|
|
||||||
| 4 | **Sign Up — Free Trial** | Email signup form, 4-week free trial, no credit card |
|
|
||||||
| 5 | **Have a Meeting** | 30-minute consultation booking via Google Calendar |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
|
|
||||||
## Languages
|
|
||||||
|
|
||||||
| Code | Language |
|
|
||||||
|------|----------|
|
|
||||||
| `en` | English (default) |
|
|
||||||
| `es` | Español |
|
|
||||||
| `de` | Deutsch |
|
|
||||||
|
|
||||||
All UI strings are managed via the inline `translations` object in `index.html`.
|
|
||||||
File diff suppressed because one or more lines are too long
@@ -1,16 +0,0 @@
|
|||||||
[
|
|
||||||
{
|
|
||||||
"area": "Automatisierung",
|
|
||||||
"date": "2026-03-25",
|
|
||||||
"title": "NGO-Verwaltung um 20–40 % reduzieren: 7 Workflows, die du diesen Monat automatisieren kannst",
|
|
||||||
"teaser": "<p>Maria verbrachte ihre Abende mit Papierkram statt mit den Familien, denen sie helfen wollte. Mit Odoo und n8n reduzierte ihr Team den Verwaltungsaufwand in unter vier Wochen um ein Drittel.</p>",
|
|
||||||
"content": "<h3>Die Admin-Falle, die jede NGO kennt</h3><p>Verstreute Spenderdaten, wiederholte Kopier-Einfüge-Schritte und langsame Bestätigungen untergraben das Vertrauen und kosten wertvolle Energie. Ein ERP bündelt Kontakte, Spenden, Rechnungen und Projektaufgaben an einem zentralen Ort — niemand muss dieselben Daten mehr in fünf verschiedene Tools eintippen.</p><h3>Odoo als Kern, n8n als Verbindung</h3><p>Nutze Odoo für Kontakte, Spenden, Projekte und Dokumente. Erstelle dann n8n-Workflows, um automatische Spendenquittungen zu versenden, Kontaktdatensätze bei Formulareinreichungen zu aktualisieren, Folgeaufgaben zu terminieren und Zusammenfassungsberichte per Slack oder E-Mail zu verteilen.</p><h3>7 Workflows für diesen Monat</h3><ul><li>Sofortige Dankesnachricht an Spender bei jeder Zuwendung</li><li>Fördermittel-Compliance-Erinnerungen im 30-Tage-Rhythmus</li><li>Onboarding-Checkliste für Freiwillige automatisch bei der Anmeldung</li><li>Monatlicher Impact-Bericht automatisch erstellt und versendet</li><li>Dubletten-Erkennung bei jedem Datenimport</li><li>Rechnungsgenehmigung nach Projekt und Budgetposition geroutet</li><li>Ausgabenerfassung aus dem Feld mit Foto-Belegerfassung</li></ul><h3>Was du erwarten kannst</h3><p>Typische NGO-Implementierungen zeigen innerhalb weniger Wochen 20–40 % weniger Verwaltungsstunden. Marias Team hörte mit den nächtlichen Admin-Sprints auf und begann jeden Morgen mit einem einzigen Dashboard — mehr Zeit für Lehrercoaching, Gemeindebesuche und die Planung wirkungsvoller Programme.</p>"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"area": "Spenderkommunikation",
|
|
||||||
"date": "2026-03-18",
|
|
||||||
"title": "Wie Maya das Spender-Chaos in klare Verbindungen verwandelte",
|
|
||||||
"teaser": "<p>Maya verwandelte verstreute Spendernachrichten in einen reibungslosen, vertrauenswürdigen Ablauf, der die Unterstützung wachsen lässt und das gesamte Team auf Kurs hält.</p>",
|
|
||||||
"content": "<h3>Von verstreuten E-Mails zur einheitlichen Ansicht</h3><p>Maya jonglierte früher mit Tabellen, E-Mail-Threads und Haftnotizen, um Spenderinteraktionen zu verfolgen. Nachrichten gingen verloren, Dankesworte kamen verspätet an und das Vertrauen bröckelte. Sie wusste, dass sich etwas ändern musste.</p><h3>Den Spender-Journey strukturieren</h3><p>Sie kartierte jeden Kontaktpunkt: Erstkontakt, Spende, Nachfassaktionen, Erneuerungen und Sonderkampagnen. Jede Interaktion erhielt eine Karte in Odoo mit Spendername, Datum, bevorzugtem Kanal und Teamnotizen — für alle auf einen Blick sichtbar.</p><h3>Automatisierte Berührungspunkte</h3><p>n8n übernimmt die Routine: Spender werden innerhalb von Minuten nach einer Zuwendung gedankt, Zusagen werden bestätigt und das Team wird vor fälligen Erneuerungen benachrichtigt. Nichts fällt durchs Raster, und der persönliche Ton bleibt erhalten — dank Vorlagen, die auf jedes Spendersegment zugeschnitten sind.</p><h3>Transparenz auf allen Ebenen</h3><p>Spender können Updates zu den Projekten sehen, die sie unterstützt haben. Status-Labels in Kampagnen zeigen zugewiesene Mittel, Impact-Geschichten und bevorstehende Veranstaltungen. Mitarbeiter hinterlassen für Kollegen sichtbare Notizen — doppelte Kontaktaufnahmen und unangenehme Überschneidungen gehören der Vergangenheit an.</p><h3>Visuelle Echtzeit-Dashboards</h3><p>Ein Live-Dashboard zeigt Spenderaktivitäten, bevorstehende Erneuerungen und die Kampagnengesundheit auf einen Blick. Farbcodes setzen Prioritäten: Grün für bestätigte Gaben, Gelb für ausstehende Zusagen, Rot für dringende Nachfassaktionen, die menschliche Aufmerksamkeit erfordern.</p><h3>Kommunikationsrhythmus</h3><p>Wöchentliche Spender-Check-ins halten das Team auf Kurs. Monatliche Newsletter heben Erfolge, Spender-Impact und kommende Möglichkeiten hervor. Sonderkampagnen lösen personalisierte Micro-E-Mails aus, die auf Spenderlevel und Verlauf jeder Person abgestimmt sind.</p><ul><li>Montag: neue Spender und ausstehende Nachfassaktionen prüfen.</li><li>Mittwoch: Kampagnenfortschritt bestätigen und Spendernotizen aktualisieren.</li><li>Freitag: Dankesmails und Impact-Updates versenden.</li></ul><h3>Kennzahlen & Wirkung</h3><p>Die Spenderbindung verbesserte sich um 25 %. Reaktionszeiten sanken von Tagen auf Stunden. Mitarbeiter berichten von weniger Stress, weil Kommunikation berechenbar, sichtbar und nicht mehr vom Gedächtnis einer einzelnen Person abhängig ist.</p><h3>Dein Rollout-Plan</h3><ul><li>Alle Spenderdaten in Odoo-Karten importieren und Verantwortliche zuweisen.</li><li>n8n-Alerts für Gaben, Nachfassaktionen und bevorstehende Erneuerungen einrichten.</li><li>Dashboards mit aktiven Spendern, Zusagen und Kampagnenstatus erstellen.</li><li>Dankesmails und vierteljährliche Impact-Updates automatisieren.</li></ul><p>Vier Schritte ersetzen wochenlange verstreute E-Mails und verpasste Möglichkeiten — und jeder Spender beginnt sich wirklich wahrgenommen zu fühlen.</p>"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
[
|
|
||||||
{
|
|
||||||
"area": "Automatización",
|
|
||||||
"date": "2026-03-25",
|
|
||||||
"title": "Reduce el trabajo administrativo de tu ONG un 20–40 %: 7 flujos que puedes automatizar este mes",
|
|
||||||
"teaser": "<p>María pasaba sus noches sepultada en papeleo en lugar de ayudar a las familias que había venido a servir. Con Odoo y n8n, su equipo redujo el tiempo administrativo en un tercio en menos de cuatro semanas.</p>",
|
|
||||||
"content": "<h3>La trampa administrativa que toda ONG conoce</h3><p>Datos dispersos de donantes, pasos repetitivos de copiar y pegar y acuses de recibo lentos erosionan la confianza y agotan la energía del equipo. Un ERP centraliza contactos, donaciones, facturas y tareas de proyecto en un único lugar, de modo que nadie tenga que reintroducir los mismos datos en cinco herramientas distintas.</p><h3>Odoo como núcleo, n8n como conector</h3><p>Usa Odoo para Contactos, Donaciones, Proyectos y Documentos. Después crea flujos en n8n para enviar recibos automáticos a donantes, actualizar registros cuando se envían formularios, programar tareas de seguimiento y distribuir informes de resumen por Slack o correo electrónico.</p><h3>7 flujos para automatizar este mes</h3><ul><li>Correo de agradecimiento instantáneo al donante en cada aportación</li><li>Recordatorios de cumplimiento de subvenciones cada 30 días</li><li>Lista de incorporación de voluntarios activada al registrarse</li><li>Informe de impacto mensual generado y enviado automáticamente</li><li>Detección de contactos duplicados en cada importación</li><li>Enrutamiento de aprobaciones de facturas por proyecto y línea presupuestaria</li><li>Envío de gastos desde campo con captura de recibo fotográfico</li></ul><h3>Resultados esperados</h3><p>Las implementaciones habituales en ONG muestran un 20–40 % menos de horas administrativas en pocas semanas. El equipo de María dejó de hacer maratones nocturnos de administración y empezó cada mañana con un solo panel de control, liberando tiempo para capacitar a docentes, visitar comunidades y planificar programas de impacto.</p>"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"area": "Comunicación con Donantes",
|
|
||||||
"date": "2026-03-18",
|
|
||||||
"title": "Cómo Maya convirtió el caos de donantes en conexiones claras",
|
|
||||||
"teaser": "<p>Maya transformó los mensajes dispersos de donantes en un flujo fluido y confiable que hace crecer el apoyo y mantiene a todo el equipo alineado.</p>",
|
|
||||||
"content": "<h3>De correos dispersos a una visión única</h3><p>Maya solía manejar hojas de cálculo, hilos de correo y notas adhesivas para rastrear las interacciones con donantes. Se perdían mensajes, los agradecimientos llegaban tarde y la confianza comenzaba a tambalearse. Sabía que algo tenía que cambiar.</p><h3>Organizando el recorrido del donante</h3><p>Mapeó cada punto de contacto: contacto inicial, donación, seguimientos, renovaciones y campañas especiales. Cada interacción tuvo su tarjeta en Odoo con el nombre del donante, la fecha, el canal preferido y las notas del equipo, visible para todos de un vistazo.</p><h3>Puntos de contacto automatizados</h3><p>n8n automatiza la rutina: agradece a los donantes a los pocos minutos de recibir una aportación, confirma los compromisos y avisa al equipo antes de que venzan las renovaciones. Nadie se queda sin respuesta y el tono personal se mantiene intacto gracias a plantillas diseñadas para cada segmento de donantes.</p><h3>Transparencia en todos los niveles</h3><p>Los donantes pueden ver actualizaciones de los proyectos que apoyaron. Las etiquetas de estado en las campañas muestran los fondos asignados, historias de impacto y próximos eventos. El personal deja notas visibles para sus colegas, eliminando contactos duplicados y superposiciones incómodas.</p><h3>Paneles visuales en tiempo real</h3><p>Un panel en vivo muestra la actividad de donantes, las próximas renovaciones y la salud de la campaña de un solo vistazo. Los códigos de color marcan la prioridad: verde para donaciones confirmadas, amarillo para compromisos pendientes, rojo para seguimientos urgentes que requieren atención humana.</p><h3>Ritmo de comunicación</h3><p>Las revisiones semanales de donantes mantienen al equipo alineado. Los boletines mensuales destacan logros, impacto de los donantes y próximas oportunidades. Las campañas especiales activan microemails personalizados según el nivel de donación y el historial de cada persona.</p><ul><li>Lunes: revisar nuevos donantes y seguimientos pendientes.</li><li>Miércoles: confirmar el avance de las campañas y actualizar las notas de donantes.</li><li>Viernes: enviar correos de agradecimiento y actualizaciones de impacto.</li></ul><h3>Métricas e impacto</h3><p>La retención de donantes mejoró un 25 %. Los tiempos de respuesta bajaron de días a horas. El personal reporta menos estrés porque la comunicación es predecible, visible y ya no depende de la memoria de una sola persona.</p><h3>Tu plan de implementación</h3><ul><li>Importar todos los registros de donantes a Odoo y asignar responsables.</li><li>Configurar alertas en n8n para donaciones, seguimientos y renovaciones.</li><li>Crear paneles con donantes activos, compromisos y estado de campaña.</li><li>Automatizar correos de agradecimiento y actualizaciones de impacto trimestrales.</li></ul><p>Cuatro pasos reemplazan semanas de correos dispersos y oportunidades perdidas — y cada donante empieza a sentirse genuinamente valorado.</p>"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
[
|
|
||||||
{
|
|
||||||
"area": "Automation",
|
|
||||||
"date": "2026-03-25",
|
|
||||||
"title": "Reduce NGO Admin by 20–40%: 7 Workflows You Can Automate This Month",
|
|
||||||
"teaser": "<p>Maria spent her evenings buried in paperwork instead of serving the families she came to help. With Odoo and n8n, her team cut admin time by a third in under four weeks.</p>",
|
|
||||||
"content": "<h3>The admin trap every NGO knows</h3><p>Scattered donor data, repeated copy-paste steps, and slow acknowledgements erode trust and drain energy. An ERP gives you one central place for contacts, donations, invoices, and project tasks — so people stop re-entering the same data into five different tools.</p><h3>Odoo as the hub, n8n as the glue</h3><p>Use Odoo for Contacts, Donations, Projects, and Documents. Then build n8n workflows to trigger automatic donor receipts, update contact records when forms are submitted, schedule follow-up tasks, and push summary reports to Slack or email.</p><h3>7 workflows to automate this month</h3><ul><li>Instant donor thank-you email on every gift</li><li>Grant compliance reminders on a rolling 30-day schedule</li><li>Volunteer onboarding checklist triggered on sign-up</li><li>Monthly impact report compiled and sent automatically</li><li>Duplicate-contact detection on every import</li><li>Invoice approval routing by project and budget line</li><li>Field-office expense submission with photo receipt capture</li></ul><h3>What to expect</h3><p>Common NGO implementations show 20–40% fewer administrative hours within weeks. Maria's team stopped doing nightly admin sprints and started each morning with a single dashboard — freeing her to coach teachers, visit communities, and plan programmes.</p>"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"area": "Donor Communication",
|
|
||||||
"date": "2026-03-18",
|
|
||||||
"title": "How Maya Turned Donor Chaos into Clear Connections",
|
|
||||||
"teaser": "<p>Maya transformed scattered donor messages into a smooth, trustworthy flow that grows support and keeps every team member aligned.</p>",
|
|
||||||
"content": "<h3>From scattered emails to a single view</h3><p>Maya used to juggle spreadsheets, inbox threads, and sticky notes trying to track donor interactions. Messages were missed, thank-yous arrived late, and trust wavered. She knew something had to change.</p><h3>Organizing the donor journey</h3><p>She mapped every donor touchpoint: initial contact, donation, follow-ups, renewals, and special campaigns. Each interaction got a card in Odoo showing the donor name, date, preferred channel, and notes — visible to the whole team at a glance.</p><h3>Automated touchpoints</h3><p>n8n automates the routine: thank donors within minutes of a gift, confirm pledges, and ping staff before renewals fall due. No one misses a beat, and the personal tone stays intact because templates are built around each donor segment.</p><h3>Transparency everywhere</h3><p>Donors can see updates on the projects they supported. Status labels on campaigns show funds allocated, impact stories, and upcoming events. Staff leave notes visible to colleagues so duplicate outreach and awkward overlaps never happen.</p><h3>Visual dashboards</h3><p>A live dashboard shows donor activity, upcoming renewals, and campaign health at a glance. Colour codes set the priority: green for confirmed gifts, yellow for pending pledges, red for urgent follow-ups that need a human touch.</p><h3>Communication rhythm</h3><p>Weekly donor check-ins keep the team aligned. Monthly newsletters highlight wins, donor impact, and upcoming opportunities. Special campaigns trigger personalised micro-emails based on giving level and history.</p><ul><li>Monday: review new donors and pending follow-ups.</li><li>Wednesday: confirm campaign progress and update donor notes.</li><li>Friday: send thank-you emails and impact updates.</li></ul><h3>Metrics & impact</h3><p>Donor retention improved by 25%. Response times dropped from days to hours. Staff report feeling less stressed because communication is predictable, visible, and no longer dependent on any one person's memory.</p><h3>Your rollout plan</h3><ul><li>Import all donor records into Odoo cards and assign owners.</li><li>Set n8n alerts for gifts, follow-ups, and upcoming renewals.</li><li>Build dashboards showing active donors, pledges, and campaign status.</li><li>Automate thank-you emails and quarterly impact updates.</li></ul><p>Four steps replace weeks of scattered emails and missed opportunities — and every donor starts to feel genuinely seen.</p>"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
-677
@@ -1,677 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<title>Dashboard</title>
|
|
||||||
<style>
|
|
||||||
body {
|
|
||||||
margin: 0;
|
|
||||||
width: 1920px;
|
|
||||||
height: 1200px;
|
|
||||||
font-family: Arial, sans-serif;
|
|
||||||
background: #2f3e4e;
|
|
||||||
color: #e6edf3;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dashboard {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
padding: 20px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 2fr 1fr 1fr;
|
|
||||||
grid-template-rows: 1fr 1fr;
|
|
||||||
gap: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.box {
|
|
||||||
background: #3b4c5e;
|
|
||||||
border-radius: 14px;
|
|
||||||
padding: 18px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
box-shadow: 0 6px 14px rgba(0, 0, 0, 0.25);
|
|
||||||
position: relative;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.title {
|
|
||||||
font-size: 20px;
|
|
||||||
font-weight: bold;
|
|
||||||
margin-bottom: 12px;
|
|
||||||
opacity: 0.9;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-light {
|
|
||||||
position: absolute;
|
|
||||||
top: 18px;
|
|
||||||
right: 18px;
|
|
||||||
width: 14px;
|
|
||||||
height: 14px;
|
|
||||||
border-radius: 50%;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.green {
|
|
||||||
background: #3ad29f;
|
|
||||||
}
|
|
||||||
.yellow {
|
|
||||||
background: #f5c542;
|
|
||||||
}
|
|
||||||
.red {
|
|
||||||
background: #e05d5d;
|
|
||||||
}
|
|
||||||
|
|
||||||
.green-text {
|
|
||||||
color: #3ad29f;
|
|
||||||
}
|
|
||||||
.yellow-text {
|
|
||||||
color: #f5c542;
|
|
||||||
}
|
|
||||||
.red-text {
|
|
||||||
color: #e05d5d;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* SERVER GRID */
|
|
||||||
.servers {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(3, 1fr);
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.server {
|
|
||||||
background: #44586d;
|
|
||||||
border-radius: 10px;
|
|
||||||
padding: 12px;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.server-name {
|
|
||||||
font-size: 15px;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
opacity: 0.85;
|
|
||||||
font-weight: bold;
|
|
||||||
padding-right: 22px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.server .status-light {
|
|
||||||
top: 12px;
|
|
||||||
right: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.metrics {
|
|
||||||
font-size: 12px;
|
|
||||||
line-height: 1.7;
|
|
||||||
opacity: 0.85;
|
|
||||||
}
|
|
||||||
|
|
||||||
.metric-row {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* MEETINGS */
|
|
||||||
.meetings-scroll {
|
|
||||||
overflow-y: auto;
|
|
||||||
max-height: calc(100% - 45px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.meeting {
|
|
||||||
background: #44586d;
|
|
||||||
border-radius: 10px;
|
|
||||||
padding: 10px 12px;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
border: 2px solid transparent;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.meeting.soon {
|
|
||||||
border: 2px solid #e05d5d;
|
|
||||||
box-shadow: 0 0 8px rgba(224, 93, 93, 0.4);
|
|
||||||
}
|
|
||||||
|
|
||||||
.meeting-header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.meeting-name {
|
|
||||||
font-size: 13px;
|
|
||||||
font-weight: bold;
|
|
||||||
opacity: 0.95;
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.meeting-badges {
|
|
||||||
display: flex;
|
|
||||||
gap: 4px;
|
|
||||||
margin-left: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.badge {
|
|
||||||
font-size: 9px;
|
|
||||||
padding: 2px 5px;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-weight: bold;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.badge-odoo {
|
|
||||||
background: #714b67;
|
|
||||||
color: #e6edf3;
|
|
||||||
}
|
|
||||||
.badge-brandize {
|
|
||||||
background: #1a6fa0;
|
|
||||||
color: #e6edf3;
|
|
||||||
}
|
|
||||||
|
|
||||||
.time {
|
|
||||||
font-size: 11px;
|
|
||||||
opacity: 0.65;
|
|
||||||
margin-top: 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* TODOS */
|
|
||||||
.todos-scroll {
|
|
||||||
overflow-y: auto;
|
|
||||||
max-height: calc(100% - 45px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.todo {
|
|
||||||
background: #44586d;
|
|
||||||
border-radius: 10px;
|
|
||||||
padding: 10px 12px;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
font-size: 13px;
|
|
||||||
opacity: 0.9;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.todo.done-todo {
|
|
||||||
opacity: 0.5;
|
|
||||||
text-decoration: line-through;
|
|
||||||
}
|
|
||||||
|
|
||||||
.todo-check {
|
|
||||||
width: 14px;
|
|
||||||
height: 14px;
|
|
||||||
border-radius: 3px;
|
|
||||||
border: 2px solid #8aa0b5;
|
|
||||||
flex-shrink: 0;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.todo-check.checked {
|
|
||||||
background: #3ad29f;
|
|
||||||
border-color: #3ad29f;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* LATEST SIGNUPS */
|
|
||||||
.signups-scroll {
|
|
||||||
overflow-y: auto;
|
|
||||||
max-height: calc(100% - 45px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.signup-item {
|
|
||||||
background: #44586d;
|
|
||||||
border-radius: 10px;
|
|
||||||
padding: 10px 12px;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.signup-email {
|
|
||||||
font-size: 13px;
|
|
||||||
font-weight: bold;
|
|
||||||
margin-bottom: 4px;
|
|
||||||
opacity: 0.95;
|
|
||||||
}
|
|
||||||
|
|
||||||
.signup-meta {
|
|
||||||
display: flex;
|
|
||||||
gap: 8px;
|
|
||||||
opacity: 0.7;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.signup-tag {
|
|
||||||
background: #2f3e4e;
|
|
||||||
border-radius: 4px;
|
|
||||||
padding: 1px 6px;
|
|
||||||
font-size: 10px;
|
|
||||||
color: #3ad29f;
|
|
||||||
}
|
|
||||||
|
|
||||||
.signup-server {
|
|
||||||
font-size: 11px;
|
|
||||||
opacity: 0.7;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* CURRENT TESTS */
|
|
||||||
.test-box {
|
|
||||||
background: #44586d;
|
|
||||||
border-radius: 10px;
|
|
||||||
padding: 12px;
|
|
||||||
font-size: 12px;
|
|
||||||
line-height: 1.8;
|
|
||||||
}
|
|
||||||
|
|
||||||
.test-row {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
|
|
||||||
padding: 3px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.test-label {
|
|
||||||
opacity: 0.6;
|
|
||||||
font-size: 11px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.test-value {
|
|
||||||
font-size: 11px;
|
|
||||||
text-align: right;
|
|
||||||
max-width: 65%;
|
|
||||||
word-break: break-all;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-badge {
|
|
||||||
display: inline-block;
|
|
||||||
padding: 1px 7px;
|
|
||||||
border-radius: 5px;
|
|
||||||
font-size: 10px;
|
|
||||||
font-weight: bold;
|
|
||||||
background: #3ad29f33;
|
|
||||||
color: #3ad29f;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-badge.dev {
|
|
||||||
background: #f5c54233;
|
|
||||||
color: #f5c542;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* LOADING / ERROR */
|
|
||||||
.loading {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
height: 100%;
|
|
||||||
font-size: 18px;
|
|
||||||
opacity: 0.5;
|
|
||||||
letter-spacing: 1px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.error-msg {
|
|
||||||
color: #e05d5d;
|
|
||||||
font-size: 13px;
|
|
||||||
opacity: 0.85;
|
|
||||||
padding: 12px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="dashboard" id="dashboard">
|
|
||||||
<div class="loading" style="grid-column: 1/-1; grid-row: 1/-1">
|
|
||||||
Loading dashboard…
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
const WEBHOOK_URL =
|
|
||||||
"https://002-001-5dd6e535-4d1c-46bc-9bd9-42ad4bc5f082.odoo4projects.com/webhook/52781c84-9f12-4a61-96b2-9e1e3933148d";
|
|
||||||
const REFRESH_INTERVAL_MS = 60 * 1000; // refresh every 60 seconds
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Helpers
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
function mbToGb(mb) {
|
|
||||||
return (mb / 1024).toFixed(1);
|
|
||||||
}
|
|
||||||
function pct(free, total) {
|
|
||||||
return total > 0 ? (free / total) * 100 : 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
function metricColor(freePct) {
|
|
||||||
if (freePct < 10) return "red";
|
|
||||||
if (freePct < 20) return "yellow";
|
|
||||||
return "green";
|
|
||||||
}
|
|
||||||
|
|
||||||
function worstColor(colors) {
|
|
||||||
if (colors.includes("red")) return "red";
|
|
||||||
if (colors.includes("yellow")) return "yellow";
|
|
||||||
return "green";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse the wall-clock date/time directly from an ISO-8601 string that
|
|
||||||
// already carries an offset (e.g. "2026-03-31T10:00:00-03:00").
|
|
||||||
// We never let the browser re-interpret it into its own timezone.
|
|
||||||
function parseISOParts(dateTimeStr) {
|
|
||||||
// Matches: YYYY-MM-DDTHH:MM:SS(±HH:MM or Z)
|
|
||||||
const m = dateTimeStr.match(
|
|
||||||
/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})(?::\d{2})?/,
|
|
||||||
);
|
|
||||||
if (!m) return null;
|
|
||||||
return {
|
|
||||||
year: parseInt(m[1], 10),
|
|
||||||
month: parseInt(m[2], 10),
|
|
||||||
day: parseInt(m[3], 10),
|
|
||||||
hour: parseInt(m[4], 10),
|
|
||||||
minute: parseInt(m[5], 10),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatLocalTime(dateTimeStr) {
|
|
||||||
const p = parseISOParts(dateTimeStr);
|
|
||||||
if (!p) return "";
|
|
||||||
const h = String(p.hour).padStart(2, "0");
|
|
||||||
const m = String(p.minute).padStart(2, "0");
|
|
||||||
return `${h}:${m}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatLocalDate(dateTimeStr) {
|
|
||||||
const p = parseISOParts(dateTimeStr);
|
|
||||||
if (!p) return "";
|
|
||||||
// Build a UTC date just for the locale label so day/month names are correct
|
|
||||||
const d = new Date(Date.UTC(p.year, p.month - 1, p.day));
|
|
||||||
return d.toLocaleDateString([], {
|
|
||||||
weekday: "short",
|
|
||||||
month: "short",
|
|
||||||
day: "numeric",
|
|
||||||
timeZone: "UTC",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function isToday(dateTimeStr) {
|
|
||||||
const now = new Date();
|
|
||||||
const p = parseISOParts(dateTimeStr);
|
|
||||||
if (!p) return false;
|
|
||||||
return (
|
|
||||||
p.year === now.getFullYear() &&
|
|
||||||
p.month === now.getMonth() + 1 &&
|
|
||||||
p.day === now.getDate()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function isSoonOrOngoing(startStr, endStr) {
|
|
||||||
const now = new Date();
|
|
||||||
const start = new Date(startStr);
|
|
||||||
const end = new Date(endStr);
|
|
||||||
const diffMin = (start - now) / 60000;
|
|
||||||
if (now >= start && now <= end) return true;
|
|
||||||
if (diffMin >= 0 && diffMin <= 30) return true;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Server cards
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
function buildServerCard(s) {
|
|
||||||
const ramPct = pct(s.free_ram, s.available_ram);
|
|
||||||
const hddPct = pct(s.free_hdd, s.available_hdd);
|
|
||||||
const ramColor = metricColor(ramPct);
|
|
||||||
const hddColor = metricColor(hddPct);
|
|
||||||
const lightColor = worstColor([ramColor, hddColor]);
|
|
||||||
|
|
||||||
const displayNames = {
|
|
||||||
saopaulo: "São Paulo",
|
|
||||||
manchester: "Manchester",
|
|
||||||
mumbai: "Mumbai",
|
|
||||||
sydney: "Sydney",
|
|
||||||
meppel: "Meppel",
|
|
||||||
boston: "Boston",
|
|
||||||
};
|
|
||||||
const displayName =
|
|
||||||
displayNames[s.Server] ||
|
|
||||||
s.Server.charAt(0).toUpperCase() + s.Server.slice(1);
|
|
||||||
|
|
||||||
return {
|
|
||||||
lightColor,
|
|
||||||
html: `
|
|
||||||
<div class="server">
|
|
||||||
<div class="server-name">${displayName}</div>
|
|
||||||
<div class="status-light ${lightColor}"></div>
|
|
||||||
<div class="metrics">
|
|
||||||
<div class="metric-row">
|
|
||||||
<span>RAM:</span>
|
|
||||||
<span class="${ramColor}-text">${ramPct.toFixed(0)}% free (${mbToGb(s.free_ram)} GB)</span>
|
|
||||||
</div>
|
|
||||||
<div class="metric-row">
|
|
||||||
<span>HDD:</span>
|
|
||||||
<span class="${hddColor}-text">${hddPct.toFixed(0)}% free (${mbToGb(s.free_hdd)} GB)</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Meetings
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
function parseMeetings(calEvents, calName) {
|
|
||||||
const seen = new Set();
|
|
||||||
const result = [];
|
|
||||||
for (const e of calEvents) {
|
|
||||||
const ev = e.json || e;
|
|
||||||
if (seen.has(ev.id)) continue;
|
|
||||||
seen.add(ev.id);
|
|
||||||
result.push({ ...ev, _calName: calName });
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildMeetingCard(m) {
|
|
||||||
const startStr =
|
|
||||||
(m.start && (m.start.dateTime || m.start.date)) || "";
|
|
||||||
const endStr = (m.end && (m.end.dateTime || m.end.date)) || "";
|
|
||||||
const soon =
|
|
||||||
startStr && endStr
|
|
||||||
? isSoonOrOngoing(startStr, endStr)
|
|
||||||
: false;
|
|
||||||
const today = startStr ? isToday(startStr) : false;
|
|
||||||
|
|
||||||
const badges = (m._cals || [])
|
|
||||||
.map((c) => {
|
|
||||||
if (c === "ODOO4projects")
|
|
||||||
return `<span class="badge badge-odoo">ODOO4</span>`;
|
|
||||||
if (c === "Brandize")
|
|
||||||
return `<span class="badge badge-brandize">Brandize</span>`;
|
|
||||||
return "";
|
|
||||||
})
|
|
||||||
.join("");
|
|
||||||
|
|
||||||
const dateLabel =
|
|
||||||
today || !startStr
|
|
||||||
? ""
|
|
||||||
: `<span style="font-size:10px;opacity:0.55;margin-right:4px;">${formatLocalDate(startStr)}</span>`;
|
|
||||||
|
|
||||||
const timeLabel =
|
|
||||||
startStr && endStr
|
|
||||||
? `${formatLocalTime(startStr)} – ${formatLocalTime(endStr)}`
|
|
||||||
: "";
|
|
||||||
|
|
||||||
return `
|
|
||||||
<div class="meeting${soon ? " soon" : ""}">
|
|
||||||
<div class="meeting-header">
|
|
||||||
<div class="meeting-name">${m.summary || "Meeting"}</div>
|
|
||||||
<div class="meeting-badges">${badges}</div>
|
|
||||||
</div>
|
|
||||||
<div class="time">${dateLabel}${timeLabel}</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Todos
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
function buildTodoItem(t) {
|
|
||||||
const done = !!t.done;
|
|
||||||
const checkInner = done
|
|
||||||
? `<svg width="10" height="10" viewBox="0 0 10 10"><polyline points="1.5,5 4,7.5 8.5,2.5" stroke="#fff" stroke-width="1.8" fill="none" stroke-linecap="round"/></svg>`
|
|
||||||
: "";
|
|
||||||
return `
|
|
||||||
<div class="todo${done ? " done-todo" : ""}">
|
|
||||||
<div class="todo-check${done ? " checked" : ""}">${checkInner}</div>
|
|
||||||
<span>${t.Todotodo || ""}</span>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Latest Signup item
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
function buildSignupItem(ct) {
|
|
||||||
if (!ct) return "";
|
|
||||||
const tagBadges = (ct.tags || "")
|
|
||||||
.split(",")
|
|
||||||
.map((t) => t.trim())
|
|
||||||
.filter(Boolean)
|
|
||||||
.map((t) => `<span class="signup-tag">${t}</span>`)
|
|
||||||
.join("");
|
|
||||||
return `
|
|
||||||
<div class="signup-item">
|
|
||||||
<div class="signup-email">${ct.email || ""}</div>
|
|
||||||
<div class="signup-meta">
|
|
||||||
<span class="signup-server">🖥 ${ct.server || ""}</span>
|
|
||||||
${tagBadges}
|
|
||||||
<span style="opacity:0.5;font-size:10px;">${ct.status || ""}</span>
|
|
||||||
</div>
|
|
||||||
<div style="margin-top:4px;opacity:0.55;font-size:10px;">
|
|
||||||
${ct.Domains ? ct.Domains : ""}${ct.expires ? " · expires " + ct.expires : ""}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildSignupItems(raw) {
|
|
||||||
if (!raw) return { html: "", count: 0 };
|
|
||||||
const items = Array.isArray(raw) ? raw : [raw];
|
|
||||||
const html = items.map(buildSignupItem).join("");
|
|
||||||
return { html, count: items.length };
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Render
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
function render(data) {
|
|
||||||
const serverStatusRaw = data["Server Status"] || [];
|
|
||||||
const calOdoo = data["Calender ODOO4projects"] || [];
|
|
||||||
const calBrandize = data["Calender Brandize"] || [];
|
|
||||||
const todos = data["Todos"] || [];
|
|
||||||
const currentTestsRaw = data["Current Tests"] || null;
|
|
||||||
|
|
||||||
// Deduplicate servers by Server key
|
|
||||||
const serverMap = {};
|
|
||||||
for (const s of serverStatusRaw) {
|
|
||||||
if (!serverMap[s.Server]) serverMap[s.Server] = s;
|
|
||||||
}
|
|
||||||
const servers = Object.values(serverMap);
|
|
||||||
|
|
||||||
// Server cards
|
|
||||||
const serverCards = servers.map(buildServerCard);
|
|
||||||
const overallColor = worstColor(
|
|
||||||
serverCards.map((c) => c.lightColor),
|
|
||||||
);
|
|
||||||
const serverCardsHtml = serverCards.map((c) => c.html).join("");
|
|
||||||
|
|
||||||
// Meetings – merge & deduplicate across both calendars
|
|
||||||
const odooMeetings = parseMeetings(calOdoo, "ODOO4projects");
|
|
||||||
const brandizeMeetings = parseMeetings(calBrandize, "Brandize");
|
|
||||||
const allMeetings = [...odooMeetings, ...brandizeMeetings];
|
|
||||||
|
|
||||||
const meetingById = {};
|
|
||||||
for (const m of allMeetings) {
|
|
||||||
if (!meetingById[m.id]) {
|
|
||||||
meetingById[m.id] = { ...m, _cals: [m._calName] };
|
|
||||||
} else if (!meetingById[m.id]._cals.includes(m._calName)) {
|
|
||||||
meetingById[m.id]._cals.push(m._calName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const dedupedMeetings = Object.values(meetingById).sort(
|
|
||||||
(a, b) => {
|
|
||||||
const aStart =
|
|
||||||
(a.start && (a.start.dateTime || a.start.date)) ||
|
|
||||||
"";
|
|
||||||
const bStart =
|
|
||||||
(b.start && (b.start.dateTime || b.start.date)) ||
|
|
||||||
"";
|
|
||||||
return new Date(aStart) - new Date(bStart);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const meetingsHtml = dedupedMeetings
|
|
||||||
.map(buildMeetingCard)
|
|
||||||
.join("");
|
|
||||||
const todosHtml = todos.map(buildTodoItem).join("");
|
|
||||||
const signups = buildSignupItems(currentTestsRaw);
|
|
||||||
|
|
||||||
document.getElementById("dashboard").innerHTML = `
|
|
||||||
|
|
||||||
<!-- SERVER STATUS -->
|
|
||||||
<div class="box" style="grid-column:1; grid-row:1/3;">
|
|
||||||
<div class="title">Server Status</div>
|
|
||||||
<div class="status-light ${overallColor}"></div>
|
|
||||||
<div class="servers">
|
|
||||||
${serverCardsHtml}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- TODAY'S MEETINGS -->
|
|
||||||
<div class="box" style="grid-column:2; grid-row:1;">
|
|
||||||
<div class="title">Meetings</div>
|
|
||||||
<div class="meetings-scroll">
|
|
||||||
${meetingsHtml || '<div style="opacity:0.4;font-size:13px;">No meetings</div>'}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- LATEST SIGNUPS -->
|
|
||||||
<div class="box" style="grid-column:3; grid-row:1/3;">
|
|
||||||
<div class="title">Latest Signups <span style="font-size:13px;font-weight:normal;opacity:0.6;background:#2f3e4e;border-radius:10px;padding:1px 9px;margin-left:6px;">${signups.count}</span></div>
|
|
||||||
<div class="signups-scroll">
|
|
||||||
${signups.html || '<div style="opacity:0.4;font-size:13px;">No signups</div>'}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- TODOS -->
|
|
||||||
<div class="box" style="grid-column:2; grid-row:2;">
|
|
||||||
<div class="title">Today's Todos</div>
|
|
||||||
<div class="todos-scroll">
|
|
||||||
${todosHtml || '<div style="opacity:0.4;font-size:13px;">No todos</div>'}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Fetch & refresh loop
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
async function loadData() {
|
|
||||||
try {
|
|
||||||
const res = await fetch(WEBHOOK_URL);
|
|
||||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
||||||
const data = await res.json();
|
|
||||||
render(data);
|
|
||||||
} catch (err) {
|
|
||||||
document.getElementById("dashboard").innerHTML = `
|
|
||||||
<div class="error-msg" style="grid-column:1/-1;grid-row:1/-1;padding:40px;">
|
|
||||||
⚠ Failed to load dashboard data: ${err.message}<br>
|
|
||||||
<span style="font-size:11px;opacity:0.6;">Retrying in ${REFRESH_INTERVAL_MS / 1000}s…</span>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
loadData();
|
|
||||||
setInterval(loadData, REFRESH_INTERVAL_MS);
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
+114
@@ -0,0 +1,114 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||||
|
<svg fill="#000000" width="800px" height="800px" viewBox="0 0 64 64" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
||||||
|
<g transform="matrix(1,0,0,1,-1152,-256)">
|
||||||
|
<rect id="Icons" x="0" y="0" width="1280" height="800" style="fill:none;"/>
|
||||||
|
<g id="Icons1" serif:id="Icons">
|
||||||
|
<g id="Strike">
|
||||||
|
</g>
|
||||||
|
<g id="H1">
|
||||||
|
</g>
|
||||||
|
<g id="H2">
|
||||||
|
</g>
|
||||||
|
<g id="H3">
|
||||||
|
</g>
|
||||||
|
<g id="list-ul">
|
||||||
|
</g>
|
||||||
|
<g id="hamburger-1">
|
||||||
|
</g>
|
||||||
|
<g id="hamburger-2">
|
||||||
|
</g>
|
||||||
|
<g id="list-ol">
|
||||||
|
</g>
|
||||||
|
<g id="list-task">
|
||||||
|
</g>
|
||||||
|
<g id="trash">
|
||||||
|
</g>
|
||||||
|
<g id="vertical-menu">
|
||||||
|
</g>
|
||||||
|
<g id="horizontal-menu">
|
||||||
|
</g>
|
||||||
|
<g id="sidebar-2">
|
||||||
|
</g>
|
||||||
|
<g id="Pen">
|
||||||
|
</g>
|
||||||
|
<g id="Pen1" serif:id="Pen">
|
||||||
|
</g>
|
||||||
|
<g id="clock">
|
||||||
|
</g>
|
||||||
|
<g id="external-link">
|
||||||
|
</g>
|
||||||
|
<g id="hr">
|
||||||
|
</g>
|
||||||
|
<g id="info">
|
||||||
|
</g>
|
||||||
|
<g id="warning">
|
||||||
|
</g>
|
||||||
|
<g id="plus-circle">
|
||||||
|
</g>
|
||||||
|
<g id="minus-circle">
|
||||||
|
</g>
|
||||||
|
<g id="vue">
|
||||||
|
</g>
|
||||||
|
<g id="cog">
|
||||||
|
</g>
|
||||||
|
<g id="logo">
|
||||||
|
</g>
|
||||||
|
<g id="radio-check">
|
||||||
|
</g>
|
||||||
|
<g id="eye-slash">
|
||||||
|
</g>
|
||||||
|
<g id="eye">
|
||||||
|
</g>
|
||||||
|
<g id="toggle-off">
|
||||||
|
</g>
|
||||||
|
<g id="shredder">
|
||||||
|
</g>
|
||||||
|
<g id="spinner--loading--dots-" serif:id="spinner [loading, dots]">
|
||||||
|
</g>
|
||||||
|
<g id="react">
|
||||||
|
</g>
|
||||||
|
<g id="check-selected">
|
||||||
|
</g>
|
||||||
|
<g id="turn-off">
|
||||||
|
</g>
|
||||||
|
<g id="code-block">
|
||||||
|
</g>
|
||||||
|
<g id="user">
|
||||||
|
</g>
|
||||||
|
<g id="coffee-bean">
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(0.638317,0.368532,-0.368532,0.638317,785.021,-208.975)">
|
||||||
|
<g id="coffee-beans">
|
||||||
|
<g id="coffee-bean1" serif:id="coffee-bean">
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<g id="coffee-bean-filled" transform="matrix(0.866025,0.5,-0.5,0.866025,717.879,-387.292)">
|
||||||
|
<g transform="matrix(1,0,0,1,0,-0.699553)">
|
||||||
|
<path d="M737.673,328.231C738.494,328.056 739.334,328.427 739.757,329.152C739.955,329.463 740.106,329.722 740.106,329.722C740.106,329.722 745.206,338.581 739.429,352.782C737.079,358.559 736.492,366.083 738.435,371.679C738.697,372.426 738.482,373.258 737.89,373.784C737.298,374.31 736.447,374.426 735.735,374.077C730.192,371.375 722.028,365.058 722.021,352C722.015,340.226 728.812,330.279 737.673,328.231Z"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(-1,0,0,-1,1483.03,703.293)">
|
||||||
|
<path d="M737.609,328.246C738.465,328.06 739.344,328.446 739.785,329.203C739.97,329.49 740.106,329.722 740.106,329.722C740.106,329.722 745.206,338.581 739.429,352.782C737.1,358.507 736.503,365.948 738.383,371.527C738.646,372.304 738.415,373.164 737.796,373.703C737.177,374.243 736.294,374.356 735.56,373.989C730.02,371.241 722.028,364.92 722.021,352C722.016,340.255 728.779,330.328 737.609,328.246Z"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(0.638317,0.368532,-0.368532,0.638317,913.062,-208.975)">
|
||||||
|
<g id="coffee-beans-filled">
|
||||||
|
<g id="coffee-bean2" serif:id="coffee-bean">
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<g id="clipboard">
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(1,0,0,1,128.011,1.35415)">
|
||||||
|
<g id="clipboard-paste">
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<g id="clipboard-copy">
|
||||||
|
</g>
|
||||||
|
<g id="Layer1">
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 4.5 KiB |
@@ -0,0 +1,224 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
fill="#000000"
|
||||||
|
width="800px"
|
||||||
|
height="800px"
|
||||||
|
viewBox="0 0 64 64"
|
||||||
|
version="1.1"
|
||||||
|
xml:space="preserve"
|
||||||
|
style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;"
|
||||||
|
id="svg6"
|
||||||
|
sodipodi:docname="bean_dark_Roast.svg"
|
||||||
|
inkscape:version="1.4.3 (0d15f75042, 2025-12-25)"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:serif="http://www.serif.com/"><defs
|
||||||
|
id="defs6" /><sodipodi:namedview
|
||||||
|
id="namedview6"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#000000"
|
||||||
|
borderopacity="0.25"
|
||||||
|
inkscape:showpageshadow="2"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pagecheckerboard="0"
|
||||||
|
inkscape:deskcolor="#d1d1d1"
|
||||||
|
inkscape:zoom="1.075"
|
||||||
|
inkscape:cx="400"
|
||||||
|
inkscape:cy="400"
|
||||||
|
inkscape:window-width="1920"
|
||||||
|
inkscape:window-height="1129"
|
||||||
|
inkscape:window-x="0"
|
||||||
|
inkscape:window-y="0"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="svg6" />
|
||||||
|
<g
|
||||||
|
transform="matrix(1,0,0,1,-1152,-256)"
|
||||||
|
id="g6">
|
||||||
|
<rect
|
||||||
|
id="Icons"
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
width="1280"
|
||||||
|
height="800"
|
||||||
|
style="fill:none;" />
|
||||||
|
<g
|
||||||
|
id="Icons1"
|
||||||
|
serif:id="Icons">
|
||||||
|
<g
|
||||||
|
id="Strike">
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="H1">
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="H2">
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="H3">
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="list-ul">
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="hamburger-1">
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="hamburger-2">
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="list-ol">
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="list-task">
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="trash">
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="vertical-menu">
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="horizontal-menu">
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="sidebar-2">
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="Pen">
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="Pen1"
|
||||||
|
serif:id="Pen">
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="clock">
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="external-link">
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="hr">
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="info">
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="warning">
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="plus-circle">
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="minus-circle">
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="vue">
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="cog">
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="logo">
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="radio-check">
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="eye-slash">
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="eye">
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="toggle-off">
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="shredder">
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="spinner--loading--dots-"
|
||||||
|
serif:id="spinner [loading, dots]">
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="react">
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="check-selected">
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="turn-off">
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="code-block">
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="user">
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="coffee-bean">
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
transform="matrix(0.638317,0.368532,-0.368532,0.638317,785.021,-208.975)"
|
||||||
|
id="g1">
|
||||||
|
<g
|
||||||
|
id="coffee-beans">
|
||||||
|
<g
|
||||||
|
id="coffee-bean1"
|
||||||
|
serif:id="coffee-bean">
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="coffee-bean-filled"
|
||||||
|
transform="matrix(0.866025,0.5,-0.5,0.866025,717.879,-387.292)">
|
||||||
|
<g
|
||||||
|
transform="matrix(1,0,0,1,0,-0.699553)"
|
||||||
|
id="g2">
|
||||||
|
<path
|
||||||
|
d="M737.673,328.231C738.494,328.056 739.334,328.427 739.757,329.152C739.955,329.463 740.106,329.722 740.106,329.722C740.106,329.722 745.206,338.581 739.429,352.782C737.079,358.559 736.492,366.083 738.435,371.679C738.697,372.426 738.482,373.258 737.89,373.784C737.298,374.31 736.447,374.426 735.735,374.077C730.192,371.375 722.028,365.058 722.021,352C722.015,340.226 728.812,330.279 737.673,328.231Z"
|
||||||
|
id="path1"
|
||||||
|
style="fill:#3e2723;fill-opacity:1" />
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
transform="matrix(-1,0,0,-1,1483.03,703.293)"
|
||||||
|
id="g3">
|
||||||
|
<path
|
||||||
|
d="M737.609,328.246C738.465,328.06 739.344,328.446 739.785,329.203C739.97,329.49 740.106,329.722 740.106,329.722C740.106,329.722 745.206,338.581 739.429,352.782C737.1,358.507 736.503,365.948 738.383,371.527C738.646,372.304 738.415,373.164 737.796,373.703C737.177,374.243 736.294,374.356 735.56,373.989C730.02,371.241 722.028,364.92 722.021,352C722.016,340.255 728.779,330.328 737.609,328.246Z"
|
||||||
|
id="path2"
|
||||||
|
style="stroke:#3e2723;stroke-opacity:1;fill:#3e2723;fill-opacity:1" />
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
transform="matrix(0.638317,0.368532,-0.368532,0.638317,913.062,-208.975)"
|
||||||
|
id="g4">
|
||||||
|
<g
|
||||||
|
id="coffee-beans-filled">
|
||||||
|
<g
|
||||||
|
id="coffee-bean2"
|
||||||
|
serif:id="coffee-bean">
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="clipboard">
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
transform="matrix(1,0,0,1,128.011,1.35415)"
|
||||||
|
id="g5">
|
||||||
|
<g
|
||||||
|
id="clipboard-paste">
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="clipboard-copy">
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="Layer1">
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 5.4 KiB |
@@ -0,0 +1,224 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
fill="#000000"
|
||||||
|
width="800px"
|
||||||
|
height="800px"
|
||||||
|
viewBox="0 0 64 64"
|
||||||
|
version="1.1"
|
||||||
|
xml:space="preserve"
|
||||||
|
style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;"
|
||||||
|
id="svg6"
|
||||||
|
sodipodi:docname="bean_light_brown.svg"
|
||||||
|
inkscape:version="1.4.3 (0d15f75042, 2025-12-25)"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:serif="http://www.serif.com/"><defs
|
||||||
|
id="defs6" /><sodipodi:namedview
|
||||||
|
id="namedview6"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#000000"
|
||||||
|
borderopacity="0.25"
|
||||||
|
inkscape:showpageshadow="2"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pagecheckerboard="0"
|
||||||
|
inkscape:deskcolor="#d1d1d1"
|
||||||
|
inkscape:zoom="1.075"
|
||||||
|
inkscape:cx="400"
|
||||||
|
inkscape:cy="400"
|
||||||
|
inkscape:window-width="1920"
|
||||||
|
inkscape:window-height="1129"
|
||||||
|
inkscape:window-x="0"
|
||||||
|
inkscape:window-y="0"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="svg6" />
|
||||||
|
<g
|
||||||
|
transform="matrix(1,0,0,1,-1152,-256)"
|
||||||
|
id="g6">
|
||||||
|
<rect
|
||||||
|
id="Icons"
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
width="1280"
|
||||||
|
height="800"
|
||||||
|
style="fill:none;" />
|
||||||
|
<g
|
||||||
|
id="Icons1"
|
||||||
|
serif:id="Icons">
|
||||||
|
<g
|
||||||
|
id="Strike">
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="H1">
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="H2">
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="H3">
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="list-ul">
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="hamburger-1">
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="hamburger-2">
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="list-ol">
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="list-task">
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="trash">
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="vertical-menu">
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="horizontal-menu">
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="sidebar-2">
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="Pen">
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="Pen1"
|
||||||
|
serif:id="Pen">
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="clock">
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="external-link">
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="hr">
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="info">
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="warning">
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="plus-circle">
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="minus-circle">
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="vue">
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="cog">
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="logo">
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="radio-check">
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="eye-slash">
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="eye">
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="toggle-off">
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="shredder">
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="spinner--loading--dots-"
|
||||||
|
serif:id="spinner [loading, dots]">
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="react">
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="check-selected">
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="turn-off">
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="code-block">
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="user">
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="coffee-bean">
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
transform="matrix(0.638317,0.368532,-0.368532,0.638317,785.021,-208.975)"
|
||||||
|
id="g1">
|
||||||
|
<g
|
||||||
|
id="coffee-beans">
|
||||||
|
<g
|
||||||
|
id="coffee-bean1"
|
||||||
|
serif:id="coffee-bean">
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="coffee-bean-filled"
|
||||||
|
transform="matrix(0.866025,0.5,-0.5,0.866025,717.879,-387.292)">
|
||||||
|
<g
|
||||||
|
transform="matrix(1,0,0,1,0,-0.699553)"
|
||||||
|
id="g2">
|
||||||
|
<path
|
||||||
|
d="M737.673,328.231C738.494,328.056 739.334,328.427 739.757,329.152C739.955,329.463 740.106,329.722 740.106,329.722C740.106,329.722 745.206,338.581 739.429,352.782C737.079,358.559 736.492,366.083 738.435,371.679C738.697,372.426 738.482,373.258 737.89,373.784C737.298,374.31 736.447,374.426 735.735,374.077C730.192,371.375 722.028,365.058 722.021,352C722.015,340.226 728.812,330.279 737.673,328.231Z"
|
||||||
|
id="path1"
|
||||||
|
style="fill:#5d4037;fill-opacity:1" />
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
transform="matrix(-1,0,0,-1,1483.03,703.293)"
|
||||||
|
id="g3">
|
||||||
|
<path
|
||||||
|
d="M737.609,328.246C738.465,328.06 739.344,328.446 739.785,329.203C739.97,329.49 740.106,329.722 740.106,329.722C740.106,329.722 745.206,338.581 739.429,352.782C737.1,358.507 736.503,365.948 738.383,371.527C738.646,372.304 738.415,373.164 737.796,373.703C737.177,374.243 736.294,374.356 735.56,373.989C730.02,371.241 722.028,364.92 722.021,352C722.016,340.255 728.779,330.328 737.609,328.246Z"
|
||||||
|
id="path2"
|
||||||
|
style="stroke:#5d4037;stroke-opacity:1;fill:#5d4037;fill-opacity:1" />
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
transform="matrix(0.638317,0.368532,-0.368532,0.638317,913.062,-208.975)"
|
||||||
|
id="g4">
|
||||||
|
<g
|
||||||
|
id="coffee-beans-filled">
|
||||||
|
<g
|
||||||
|
id="coffee-bean2"
|
||||||
|
serif:id="coffee-bean">
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="clipboard">
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
transform="matrix(1,0,0,1,128.011,1.35415)"
|
||||||
|
id="g5">
|
||||||
|
<g
|
||||||
|
id="clipboard-paste">
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="clipboard-copy">
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="Layer1">
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 5.4 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 115 KiB |
+116
@@ -0,0 +1,116 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
width="124.8711mm"
|
||||||
|
height="31.440542mm"
|
||||||
|
viewBox="0 0 124.8711 31.440542"
|
||||||
|
version="1.1"
|
||||||
|
id="svg1"
|
||||||
|
inkscape:version="1.4.3 (0d15f75042, 2025-12-25)"
|
||||||
|
sodipodi:docname="logo.svg"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg">
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="namedview1"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#000000"
|
||||||
|
borderopacity="0.25"
|
||||||
|
inkscape:showpageshadow="2"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pagecheckerboard="0"
|
||||||
|
inkscape:deskcolor="#d1d1d1"
|
||||||
|
inkscape:document-units="mm"
|
||||||
|
inkscape:zoom="0.70021044"
|
||||||
|
inkscape:cx="329.90082"
|
||||||
|
inkscape:cy="345.61039"
|
||||||
|
inkscape:window-width="1920"
|
||||||
|
inkscape:window-height="1046"
|
||||||
|
inkscape:window-x="0"
|
||||||
|
inkscape:window-y="0"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="layer1" />
|
||||||
|
<defs
|
||||||
|
id="defs1" />
|
||||||
|
<g
|
||||||
|
inkscape:label="Ebene 1"
|
||||||
|
inkscape:groupmode="layer"
|
||||||
|
id="layer1"
|
||||||
|
transform="translate(-17.727083,-56.885417)">
|
||||||
|
<g
|
||||||
|
id="g5"
|
||||||
|
transform="matrix(0.26458333,0,0,0.26458333,-67.310852,25.675979)">
|
||||||
|
<text
|
||||||
|
id="text5"
|
||||||
|
xml:space="preserve"
|
||||||
|
transform="matrix(1.3333333,0,0,1.3333333,316.00667,186.29426)"><tspan
|
||||||
|
id="tspan5"
|
||||||
|
style="font-variant:normal;font-weight:700;font-size:65.991px;font-family:'Noto Sans';writing-mode:lr-tb;fill:#3e2723;fill-opacity:1;fill-rule:nonzero;stroke:#3e2723;stroke-width:2.19965;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
x="0"
|
||||||
|
y="0">my</tspan></text>
|
||||||
|
<text
|
||||||
|
id="text6"
|
||||||
|
xml:space="preserve"
|
||||||
|
transform="matrix(1.3333333,0,0,1.3333333,452.48533,186.29426)"><tspan
|
||||||
|
id="tspan6"
|
||||||
|
style="font-variant:normal;font-weight:700;font-size:65.991px;font-family:'Noto Sans';writing-mode:lr-tb;fill:#e65100;fill-opacity:1;fill-rule:nonzero;stroke:#e65100;stroke-width:2.19965;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
x="0"
|
||||||
|
y="0">-</tspan></text>
|
||||||
|
<text
|
||||||
|
id="text7"
|
||||||
|
xml:space="preserve"
|
||||||
|
transform="matrix(1.3333333,0,0,1.3333333,480.79333,186.29426)"><tspan
|
||||||
|
id="tspan7"
|
||||||
|
style="font-variant:normal;font-weight:700;font-size:65.991px;font-family:'Noto Sans';writing-mode:lr-tb;fill:#3e2723;fill-opacity:1;fill-rule:nonzero;stroke:#3e2723;stroke-width:2.19965;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
x="0"
|
||||||
|
y="0">biz</tspan></text>
|
||||||
|
<text
|
||||||
|
id="text8"
|
||||||
|
xml:space="preserve"
|
||||||
|
transform="matrix(1.3333333,0,0,1.3333333,606.236,186.29426)"><tspan
|
||||||
|
id="tspan8"
|
||||||
|
style="font-variant:normal;font-weight:700;font-size:65.991px;font-family:'Noto Sans';writing-mode:lr-tb;fill:#e65100;fill-opacity:1;fill-rule:nonzero;stroke:#e65100;stroke-width:2.19965;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
x="0"
|
||||||
|
y="0">.app</tspan></text>
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="g8"
|
||||||
|
transform="matrix(0.26458333,0,0,0.26458333,-67.310852,25.675979)">
|
||||||
|
<path
|
||||||
|
id="path8"
|
||||||
|
d="M 249.449,279.212 H 391.181"
|
||||||
|
style="fill:none;stroke:#e65100;stroke-width:1;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
transform="matrix(1.3333333,0,0,-1.3333333,0,595.27559)" />
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="g9"
|
||||||
|
transform="matrix(0.26458333,0,0,0.26458333,-67.310852,25.675979)">
|
||||||
|
<path
|
||||||
|
id="path9"
|
||||||
|
d="M 442.205,279.212 H 583.937"
|
||||||
|
style="fill:none;stroke:#e65100;stroke-width:1;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
transform="matrix(1.3333333,0,0,-1.3333333,0,595.27559)" />
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="g10"
|
||||||
|
transform="matrix(0.26458333,0,0,0.26458333,-67.310852,25.675979)">
|
||||||
|
<path
|
||||||
|
id="path10"
|
||||||
|
d="m 420.661,289.332 c 0.369,-0.114 0.596,-0.454 0.596,-0.822 0.028,-0.171 0.028,-0.284 0.028,-0.284 0,0 -0.028,-4.535 -5.386,-8.702 -2.182,-1.701 -4.053,-4.451 -4.564,-7.03 -0.056,-0.34 -0.311,-0.624 -0.652,-0.681 -0.368,-0.085 -0.708,0.057 -0.907,0.369 -1.53,2.268 -3.259,6.491 -0.368,11.48 2.608,4.536 7.427,6.832 11.253,5.67 z"
|
||||||
|
style="fill:#3e2723;fill-opacity:1;fill-rule:evenodd;stroke:none"
|
||||||
|
transform="matrix(1.3333333,0,0,-1.3333333,0,595.27559)" />
|
||||||
|
<path
|
||||||
|
id="path11"
|
||||||
|
d="m 413.121,269.376 c -0.368,0.141 -0.623,0.482 -0.623,0.85 0,0.17 0,0.284 0,0.284 0,0 0,4.535 5.357,8.702 2.154,1.672 4.054,4.394 4.564,6.945 0.057,0.368 0.34,0.623 0.709,0.708 0.34,0.057 0.708,-0.085 0.907,-0.396 1.53,-2.268 3.174,-6.463 0.34,-11.424 -2.608,-4.507 -7.399,-6.803 -11.254,-5.669 z"
|
||||||
|
style="fill:#3e2723;fill-opacity:1;fill-rule:evenodd;stroke:none"
|
||||||
|
transform="matrix(1.3333333,0,0,-1.3333333,0,595.27559)" />
|
||||||
|
<path
|
||||||
|
id="path12"
|
||||||
|
d="m 413.121,269.376 c -0.368,0.141 -0.623,0.482 -0.623,0.85 0,0.17 0,0.284 0,0.284 0,0 0,4.535 5.357,8.702 2.154,1.672 4.054,4.394 4.564,6.945 0.057,0.368 0.34,0.623 0.709,0.708 0.34,0.057 0.708,-0.085 0.907,-0.396 1.53,-2.268 3.174,-6.463 0.34,-11.424 -2.608,-4.507 -7.399,-6.803 -11.254,-5.669 z"
|
||||||
|
style="fill:none;stroke:#3e2723;stroke-width:0.4428;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
transform="matrix(1.3333333,0,0,-1.3333333,0,595.27559)" />
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 5.8 KiB |
+4468
-2552
File diff suppressed because it is too large
Load Diff
Generated
-1025
File diff suppressed because it is too large
Load Diff
@@ -1,22 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "ngo",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"description": "",
|
|
||||||
"main": "index.js",
|
|
||||||
"scripts": {
|
|
||||||
"build:css": "tailwindcss -i src/input.css -o assets/site.css --minify",
|
|
||||||
"watch:css": "tailwindcss -i src/input.css -o assets/site.css --watch",
|
|
||||||
"test": "echo \"Error: no test specified\" && exit 1"
|
|
||||||
},
|
|
||||||
"repository": {
|
|
||||||
"type": "git",
|
|
||||||
"url": "git@git.odoo4projects.com:Oliver/NGO.git"
|
|
||||||
},
|
|
||||||
"keywords": [],
|
|
||||||
"author": "",
|
|
||||||
"license": "ISC",
|
|
||||||
"type": "commonjs",
|
|
||||||
"devDependencies": {
|
|
||||||
"tailwindcss": "^3.4.17"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+47
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
|||||||
|
{"area": "CRM", "Vertical": "NGO", "date": "2026-05-04", "title": "Impact Reporting in Minutes", "teaser": "Small NGOs waste countless hours turning donor data into reports—instead of serving those they’re meant to help.", "content": "<article>\n<h2>Impact Reporting in Minutes</h2>\n<p style=\"margin: 0; color: #374151;\">— \"Maria stared at her screen at 11 p.m., three days before the grant deadline. Her donor database had 872 entries. Excel had 14 tabs. She hadnt slept in 36 hours. Every email from a donor—How was the clean water project?—demanded a new report. She couldnt tell them. Not without weeks of work. <strong>Spreadsheets</strong> were drowning her. <strong>Manual reporting</strong> was stealing her purpose. And worst of all—she knew donors were slipping away, not because her work wasn’t impactful, but because she couldn’t prove it.\" —</p>\n\n<p style=\"margin: 0; color: #374151;\">Modern digital tools can transform how NGOs capture and communicate impact. Automated systems can pull data from donations, program logs, and field reports into unified dashboards. Reports once built by hand can now be generated with a single click, reducing errors, saving weeks of labor, and preserving the emotional integrity of the stories behind the numbers.</p>\n\n<h3>Stop drowning in spreadsheets. Start telling stories.</h3>\n<p style=\"margin: 0; color: #374151;\">When your team spends 20 hours a week just compiling donor data, you’re not serving beneficiaries—you’re serving Excel. Automated reporting tools reset that balance. They sync with your existing donation platforms, track program outcomes across locations, and turn fragmented inputs into cohesive, real-time narratives that donors actually read.</p>\n\n<h3>Trust isn’t built with PDFs. It’s built with clarity.</h3>\n<p style=\"margin: 0; color: #374151;\">Donors don’t want line items. They want to know lives changed. A clean, automated dashboard shows a child’s progress in school, not just the $50 spent on textbooks. When you can show, not tell, donors see their values reflected in real results. That’s how <strong>retention grows</strong>—not through emails begging for more, but through transparency that feels genuine.</p>\n\n<h3>Proof that matters. Without the panic.</h3>\n<p style=\"margin: 0; color: #374151;\">Grant applications and board reviews require compliance—not creativity. When audit season rolls around, having pre-built, audit-ready templates means your finance lead isn’t working through the night. No more frantic last-minute data fixes. Just a few clicks, and your compliance is done. That peace of mind? It’s priceless.</p>\n\n<div class=\"video-container\" style=\"display: flex; margin: 2rem 0; gap: 2rem;\">\n <div class=\"video-text\" style=\"flex: 1;\">\n <p style=\"margin: 0; color: #374151;\">This is how you generate an impact report for your donors—no more spreadsheets, no more stress. Just one click, and your story goes live.</p>\n </div>\n <div class=\"video-player\" style=\"flex: 1; min-width: 300px; height: 300px;\">\n <iframe width=\"100%\" height=\"100%\"\n src=\"https://www.youtube.com/embed/huVrXvkCAqg\"\n title=\"YouTube video player\"\n frameborder=\"0\"\n allow=\"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture\"\n allowfullscreen\n style=\"width: 100%; height: 100%; border-radius: 8px;\">\n </iframe>\n </div>\n</div>\n\n<p style=\"margin: 0; color: #374151;\"><em>Remember: Every hour you spend wrestling with data is an hour taken from the field, the classroom, the clinic.</em></p>\n\n<a class=\"cta\" href=\"https://calendar.google.com/calendar/u/0/appointments/schedules/AcZssZ3DDbaiHFlhNhWySszAQoPXE_H73QLqYT3w7H9IYWC76RA_TgNIhLESjb4N7ep_D2D_OyW9q4-c\">When you want to explore how this could work for your organization, you can book a meeting here:</a>\n</article>", "image": "ngo_office3"}
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
@tailwind base;
|
|
||||||
@tailwind components;
|
|
||||||
@tailwind utilities;
|
|
||||||
|
|
||||||
body {
|
|
||||||
@apply font-brand;
|
|
||||||
}
|
|
||||||
|
|
||||||
#lang-selector {
|
|
||||||
color: #ffffff;
|
|
||||||
}
|
|
||||||
|
|
||||||
#lang-selector:focus,
|
|
||||||
#lang-selector:focus-visible {
|
|
||||||
color: #201824;
|
|
||||||
background-color: #ffffff;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ── Blog post modal animations ───────────────────────── */
|
|
||||||
|
|
||||||
@keyframes modal-slide-in {
|
|
||||||
from {
|
|
||||||
opacity: 0;
|
|
||||||
transform: scale(0.95) translateY(20px);
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
opacity: 1;
|
|
||||||
transform: scale(1) translateY(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes modal-fade-in {
|
|
||||||
from {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-animate-box {
|
|
||||||
animation: modal-slide-in 0.3s cubic-bezier(0.16, 1, 0.3, 1) both;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-animate-backdrop {
|
|
||||||
animation: modal-fade-in 0.2s ease both;
|
|
||||||
}
|
|
||||||
+1630
File diff suppressed because it is too large
Load Diff
@@ -1,28 +0,0 @@
|
|||||||
/** @type {import('tailwindcss').Config} */
|
|
||||||
module.exports = {
|
|
||||||
content: ["./index.html"],
|
|
||||||
theme: {
|
|
||||||
extend: {
|
|
||||||
colors: {
|
|
||||||
"brand-ink": "#201824",
|
|
||||||
"brand-primary": "#603F57",
|
|
||||||
"brand-accent": "#F762B4",
|
|
||||||
"brand-sunrise": "#F8B84A",
|
|
||||||
"brand-emerald": "#39B982",
|
|
||||||
"brand-snow": "#F5F2F7",
|
|
||||||
"brand-smoke": "#E5DCE8",
|
|
||||||
},
|
|
||||||
fontFamily: {
|
|
||||||
brand: ["system-ui", "-apple-system", "Segoe UI", "sans-serif"],
|
|
||||||
},
|
|
||||||
boxShadow: {
|
|
||||||
brand: "0 20px 60px rgba(32, 24, 36, 0.15)",
|
|
||||||
},
|
|
||||||
backgroundImage: {
|
|
||||||
"hero-gradient":
|
|
||||||
"radial-gradient(circle at top left, rgba(247,98,180,0.35), rgba(32,24,36,0.95))",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
plugins: [],
|
|
||||||
};
|
|
||||||
Reference in New Issue
Block a user