31 Commits

Author SHA1 Message Date
Your Name f3cb8ca848 fix: show total hours right-aligned in Today's Todos headline 2026-04-07 05:50:00 -03:00
Your Name acec840d91 Add minutes 2026-04-07 05:48:45 -03:00
Your Name 0d5205ac94 fix: display meeting times from ISO offset string, not browser timezone 2026-03-31 09:58:12 -03:00
Your Name 1a4a9542f5 fix: Latest Signups full height, count badge, remove Current Test box, handle array/object JSON 2026-03-30 10:59:23 -03:00
Your Name bd319d05a5 del 2026-03-30 09:14:06 -03:00
Your Name 2a0af148c8 block 2026-03-30 08:21:13 -03:00
Your Name ad7a531ea0 typo 2026-03-30 08:02:42 -03:00
Your Name fd91306cb0 added Dash 2026-03-30 08:01:42 -03:00
Oliver 4ea7586a7f google verification 2026-03-27 18:40:05 -03:00
Oliver 671ddb2b75 content: update impact strip stats — 55% no-ERP, 32% admin reduction 2026-03-27 18:32:05 -03:00
Oliver 7d447bee93 fix: footer links align-bottom; send btn inside textarea bottom-right, border color 2026-03-27 18:23:24 -03:00
Oliver 5cc4486ab7 fix: 2× textarea height; footer copyright br + remove dash 2026-03-27 18:18:09 -03:00
Oliver 6e4b1c891c feat: append IP + geo (city, region, country, org) to feedback webhook via ipapi.co 2026-03-27 18:15:26 -03:00
Oliver f1f67b29dd fix: footer textarea 50% smaller; translate placeholder EN/ES/DE 2026-03-27 18:13:46 -03:00
Oliver d568a365df fix: footer textarea size+scrollbar+arrow; bundle i18n; meeting copy update 2026-03-27 18:09:42 -03:00
Oliver 4781e84ebc perf: remove Google Fonts — system font stack for body and headings; rebuild site.css -48% 2026-03-27 18:04:06 -03:00
Oliver be09564a25 feat: badge text, 6-item bundle card, footer textarea 2×wide 30%tall, SVG send icon 2026-03-27 17:59:40 -03:00
Oliver 0fc5b59d5f feat: footer feedback widget — textarea + send to webhook 2026-03-27 17:46:37 -03:00
Oliver 5526873a41 design: promote NGO warm design to index.html; AU Sydney; bundle card; india.webp hero 2026-03-27 17:40:57 -03:00
Oliver d111b55de0 feat: blog modal with animation, replace trial form with product/location/webhook widget 2026-03-27 16:56:52 -03:00
Oliver 5bd3c7b398 feat: multi-language blog posts — add blog.es.json, blog.de.json; load by locale with EN fallback 2026-03-27 16:40:03 -03:00
Oliver 3627c2c73d build: add .gitignore, npm scripts, fix font-brand token, clear audit warnings 2026-03-27 16:30:07 -03:00
Oliver 4d2f57c20c Checked AGENTS.md 2026-03-27 16:23:57 -03:00
Oliver 05719aae4e INIT Blogs 2026-03-27 13:37:03 -03:00
Web_Designer b2a41d1a7b Add blog post: ODOO has 1,000,000 NGOs online! 2026-03-27 16:29:07 +00:00
Web_Designer 4bdef11b3c Publish: localisation-in-practice-shifting-power-and-funding-to-local-partners-effectively 2026-03-27 16:25:10 +00:00
Web_Designer aafb99460c Update homepage title and H1 to 'OPENCODEis NOICE' 2026-03-27 15:10:00 +00:00
Oliver a74de43248 Update README.md 2026-03-25 18:21:03 -03:00
Web_Designer 819665225f Add blog post: Reduce NGO Admin by 20–40%: 7 Workflows You Can Automate This Month 2026-03-25 20:55:58 +00:00
Oliver 3dd49559ac 0 2026-03-25 16:14:17 -03:00
Oliver c81565749a 0 2026-03-25 16:12:40 -03:00
15 changed files with 3717 additions and 647 deletions
+21
View File
@@ -0,0 +1,21 @@
# Dependencies
node_modules/
# Tailwind build temp
tmp.json
# Logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# OS
.DS_Store
Thumbs.db
# Editor
.vscode/
.idea/
*.swp
*.swo
+233
View File
@@ -0,0 +1,233 @@
# AGENTS.md — NGO Landing Page
Read this file fully before taking any action.
---
## Project Context
Single-page static landing page for an NGO ERP product.
| Item | Detail |
|---|---|
| Pages | `index.html` — the only HTML file |
| Styling | Tailwind CSS v3 · source `src/input.css` → compiled `assets/site.css` |
| Data | `blog.json` — flat array of blog posts, loaded at runtime by inline JS |
| i18n | Inline `translations` object inside `index.html` (EN / ES / DE) |
| Blog renderer | Inline `<script>` in `index.html` — no external JS files |
| Dev server | `./start` (runs `live-server`) |
| Git remote | `git@git.odoo4projects.com:Oliver/NGO.git` |
### File map
```
index.html ← only HTML file — layout + all inline JS + i18n
src/input.css ← Tailwind source (edit here, then rebuild)
assets/site.css ← compiled output — DO NOT edit by hand
blog.json ← English blog posts (canonical + fallback)
blog.es.json ← Spanish blog posts
blog.de.json ← German blog posts
tailwind.config.js ← design tokens (colors, fonts, shadows)
```
### Design tokens (from `tailwind.config.js`)
| Token | Hex | Usage |
|---|---|---|
| `brand-ink` | `#201824` | default text / dark backgrounds |
| `brand-primary` | `#603F57` | section labels, bullet accents |
| `brand-accent` | `#F762B4` | CTA buttons, links |
| `brand-sunrise` | `#F8B84A` | hero highlights, hover states |
| `brand-emerald` | `#39B982` | positive indicators |
| `brand-snow` | `#F5F2F7` | page background |
| `brand-smoke` | `#E5DCE8` | borders, subtle dividers |
NEVER use raw hex values in HTML — always use the token class names above.
---
## 🎯 Goal
- Deliver an **SEO-optimised, fast-loading, responsive** single-page site.
- Keep all edits **minimal and targeted** — preserve design, structure, and functionality.
- NEVER introduce new frameworks, libraries, or external dependencies.
### Quality bar for every change
| Concern | Rule |
|---|---|
| SEO | Keep all existing meta tags, `ld+json`, `alt` attributes, and heading hierarchy |
| Performance | No comments in production HTML, no unused CSS classes, no render-blocking additions |
| Responsive | Every element must work on mobile (375 px), tablet (768 px), and desktop (1280 px+) |
| Consistency | Use only brand token classes; match existing spacing and rounding patterns |
---
## Commands
```bash
# Build CSS after editing src/input.css or tailwind.config.js
npm run build:css
# Watch CSS during active development
npm run watch:css
# Validate blog.json
jq empty blog.json && echo "JSON valid"
# Start dev server
./start
```
---
## Git Rules
- ALWAYS work on **`main`** branch.
- Pull before every session:
```bash
git pull origin main
```
- Commit messages must follow the pattern:
- `blog: add post <title>`
- `fix: <short description>`
- `content: <short description>`
- Push immediately after committing — never leave unpushed commits.
- Branch creation is permitted ONLY under the **Branch Handling Exception** (see bottom).
---
## File Safety Rules
- ONLY modify files required for the task.
- NEVER delete or restructure content unrelated to the task.
- NEVER remove existing blog posts from `blog.json`.
- NEVER edit `assets/site.css` directly — always rebuild from `src/input.css`.
---
## Blog Post Schema
### Language-to-file mapping
| Language | File loaded | Fallback |
|---|---|---|
| English (`en`) | `blog.json` | — |
| Spanish (`es`) | `blog.es.json` | `blog.json` |
| German (`de`) | `blog.de.json` | `blog.json` |
The loader in `index.html` fetches the language-specific file automatically when the visitor switches language. If a language file is missing or returns an error, it falls back to `blog.json` silently.
`blog.json` is always the **English source and fallback**. Keep all three files structurally identical — same number of posts, same order, translated content only.
---
`blog.json` (and every language variant) is a root-level JSON array. Every post object contains **exactly these five fields**:
```json
[
{
"area": "Automation",
"date": "2026-03-25",
"title": "Article title as plain text",
"teaser": "<p>One or two sentences visible before 'Read more'.</p>",
"content": "<h3>Section</h3><p>Full article as HTML.</p>"
}
]
```
| Field | Type | Constraint |
|---|---|---|
| `area` | plain string | Category label rendered above the title |
| `date` | ISO 8601 (`YYYY-MM-DD`) | Used by `Intl.DateTimeFormat` |
| `title` | plain string | Rendered as `<h3>` — no HTML |
| `teaser` | HTML string | Always visible — wrap in `<p>` tags |
| `content` | HTML string | Revealed by "Read more" toggle — use `<h3>`, `<p>`, `<ul>`, `<li>` |
NEVER add extra fields (`slug`, `author`, `tags`, `excerpt`, `hero_image`, etc.) — they are ignored by the renderer and pollute the file.
### Adding a new blog post (jq method)
> Add to **all three files** in the same session. A post that exists in English but not in Spanish/German will silently fall back to the English version for those visitors — which is acceptable temporarily but should be resolved promptly.
```bash
# 1. Pull latest
git pull origin main
# 2. Backup all blog files once per session
mkdir -p /backup
for f in blog.json blog.es.json blog.de.json; do
cp $f /backup/$f.$(date +%Y%m%d%H%M%S)
done
# 3. Prepend new post to each language file
# $NEW_EN, $NEW_ES, $NEW_DE must each be a valid single post object
jq --argjson new "$NEW_EN" '. = [$new] + .' blog.json > tmp.json && mv tmp.json blog.json
jq --argjson new "$NEW_ES" '. = [$new] + .' blog.es.json > tmp.json && mv tmp.json blog.es.json
jq --argjson new "$NEW_DE" '. = [$new] + .' blog.de.json > tmp.json && mv tmp.json blog.de.json
# 4. Validate all three
jq empty blog.json && echo "blog.json OK"
jq empty blog.es.json && echo "blog.es.json OK"
jq empty blog.de.json && echo "blog.de.json OK"
# 5. Commit and push
git add blog.json blog.es.json blog.de.json
git commit -m "blog: add post <title>"
git push origin main
```
NEVER use Python, Node scripts, or manual text editing to modify `blog.json`.
---
## Execution Workflow
Follow this order for every task:
1. `git pull origin main`
2. Identify the exact files to change — nothing else.
3. Apply minimal edits.
4. If `src/input.css` or `tailwind.config.js` were changed → rebuild CSS.
5. Validate: `jq empty blog.json` for JSON; visual check for HTML.
6. Commit with a descriptive message.
7. `git push origin main`
---
## Success Criteria
- Changes are minimal, validated, and committed.
- `blog.json` is a valid array with exactly five fields per post.
- `assets/site.css` reflects any CSS changes (rebuilt, not hand-edited).
- HTML passes a visual check — layout and design unchanged except for the intended edit.
- All commits pushed to `main`.
---
## Failure Conditions
These actions constitute a failure:
- Refusing to act due to perceived access restrictions.
- Failing to push after committing.
- Adding undeclared fields to `blog.json`.
- Removing or overwriting existing blog posts.
- Editing `assets/site.css` by hand.
- Modifying files outside the scope of the task.
- Leaving a branch open without merging.
---
## Branch Handling Exception
Branch creation is permitted **only** when:
- The task explicitly requires a feature branch, **or**
- A direct push to `main` is blocked by branch protection.
If a branch is created it **must**:
- Follow the naming convention `<type>/<short-description>` (e.g. `fix/blog-json-schema`).
- Be merged into `main` via PR immediately after the task is complete.
- Be deleted after merge — never abandoned.
@@ -1,26 +0,0 @@
Rollback and Recovery Changelog
Timestamp: ${TS}
Actions performed:
- Reset main branch (working tree and HEAD) to commit 6127867d51c9f0e4e86d1d8390081208f45136ab and force-pushed to origin/main.
- Confirmed README.md content matches version at commit 6127867d51.
- Investigated deleted backups: located backups in parent commit 5189b23 and extracted available files into recovered_backups/ directory in the working tree (not yet committed).
- Created this changelog and saved recovered backup files under recovered_backups/ for review.
Recovered backup files:
- backups/index.html.20260324164129.bak -> recovered_backups/index.html.20260324164129.bak (extracted from commit 5189b23)
- backups/index.html.bak.20260324164053 -> recovered_backups/index.html.bak.20260324164053 (extracted from commit 5189b23)
Notes on recovery:
- The deleted backup files were present in previous commits and have been extracted from git history into recovered_backups/.
- Additional backup files referencing other timestamps exist in git history (e.g., backups/index.html.20260324164556.bak and backups/index.html.20260324164626.bak). These can be extracted on request.
Suspicious findings:
- Commit 6127867d51 (message: "Deleted Backups and changed readme") removed backup files. Author: Oliver. This commit may be responsible for missing backups. Please confirm whether this deletion was authorized.
Remaining tasks / recommendations:
- Decide whether to reintroduce recovered backups into main branch. Restoring them to main will create a new commit on top of 6127867d51 (this changelog can be used), or they can be provided in a separate branch or as artifacts.
- If you want me to commit the recovered_backups/ content and this changelog to the repository and push, confirm and I will proceed.
- If you prefer the repository to remain exactly at 6127867d51 (no additional commits), I will not commit recovered files; instead I will provide them separately (download/attach) and leave main at the rollback state.
+16 -26
View File
@@ -5,12 +5,24 @@
**Stack:** Static HTML · Tailwind CSS · Vanilla JS **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 ## Repository Rules
- Do not touch this readme
- Blog posts live in `blog.json` — do not create separate backup files. - Keep the seperation of Site and the blogposts in blog.json
- No backup files in the repo. Example of JSON structure for blog.json
- Update this README with every commit. {
"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"
},
--- ---
@@ -26,22 +38,6 @@
--- ---
## CI Checks (required on every push)
- **HTML lint** — valid markup, no broken anchors
- **JS lint** — no errors, no `console.error` left in production paths
- **Tailwind build** — `npx tailwindcss -i ./src/input.css -o ./assets/site.css --minify` must exit 0
- **Corporate design compliance:**
- No CSS gradients — `gradient`, `bg-*-gradient`, `linear-gradient`, `radial-gradient` are forbidden
- No CSS animations or keyframes — `animate-*`, `@keyframes`, transform-based `hover:translate-*` are forbidden
- No blur or glassmorphism effects — `backdrop-blur`, `backdrop-filter` are forbidden
- Colors must use the approved brand palette only: `brand-ink · brand-primary · brand-accent · brand-sunrise · brand-emerald · brand-snow · brand-smoke`
- Images must be served from `odoo4projects.com`, `images.unsplash.com`, or local `assets/`
- **Accessibility** — heading hierarchy (one `<h1>` per page), `alt` on all images, sufficient contrast (WCAG AA)
- **Performance budget** — total page weight ≤ 200 KB (excluding cached fonts); critical render path ≤ 1 s on fast 3G
- **SEO** — `<title>`, `<meta name="description">`, `<h1>`, canonical `<link>`, and JSON-LD structured data must be present
---
## Languages ## Languages
@@ -52,9 +48,3 @@
| `de` | Deutsch | | `de` | Deutsch |
All UI strings are managed via the inline `translations` object in `index.html`. All UI strings are managed via the inline `translations` object in `index.html`.
---
## Focus Keywords
`NGO donor management` · `transparent donations` · `Odoo for NGOs` · `donor communication` · `minimal admin for charities`
+1 -1
View File
File diff suppressed because one or more lines are too long
+16
View File
@@ -0,0 +1,16 @@
[
{
"area": "Automatisierung",
"date": "2026-03-25",
"title": "NGO-Verwaltung um 2040 % 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 2040 % 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 &amp; 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>"
}
]
+16
View File
@@ -0,0 +1,16 @@
[
{
"area": "Automatización",
"date": "2026-03-25",
"title": "Reduce el trabajo administrativo de tu ONG un 2040 %: 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 2040 % 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>"
}
]
+10 -39
View File
@@ -1,45 +1,16 @@
[ [
{ {
"area": "Transparency", "area": "Automation",
"title": "Every Peso on the Page: Ana's Ledger Story", "date": "2026-03-25",
"teaser": "<p>Ana replaced five notebooks with a four-stop impact ledger and now answers donor questions in seconds.</p>", "title": "Reduce NGO Admin by 2040%: 7 Workflows You Can Automate This Month",
"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>Receipts hid in mugs, inboxes, and backpacks, so every audit meant copying the same row five times.</p>\n<p>Boards pushed for proof faster than her laptop could export sheets, so calls ended with apologies.</p>\n<p>Vendors waited weeks because approvals depended on whoever remembered to forward a blurry photo.</p>\n<p>Community meetings turned tense when no one could connect a bus ticket to a childs name in real time.</p>\n<h3>System upgrade</h3>\n<p>Ana listed every stop money touches: donation, allocation, program spend, impact note.</p>\n<p>Each funding source now sits on a simple card showing who owns it and what is due.</p>\n<p>Odoo acts like a shared notebook; every card links to tags, receipts, and the promise behind the peso.</p>\n<p>Receipts arrive through a one-question webform, and n8n files them while pinging the right teammate.</p>\n<p>An AI scribe drafts the Friday recap so donors hear “Twelve bus rides covered” instead of raw numbers.</p>\n<p>Dashboards mix pesos, quotes, and photos so anyone can understand the path without spreadsheets.</p>\n<h3>Controls and rituals</h3>\n<p>Program leads drop 20-second voice notes beside transactions, turning expenses into mini field reports.</p>\n<p>Monthly reviews start with one shared view, so no one emails attachments named “final_v7.xlsx.”</p>\n<p>Vendors sign reimbursements inside the same screen, keeping cash flow visible for everyone.</p>\n<p>Audit packets assemble in three clicks because every receipt already lives on its card.</p>\n<p>Community assemblies now show a single screen that pairs pesos with tickets and class photos.</p>\n<ul>\n <li>Ledger lane: donation → tag → receipt → quote.</li>\n <li>Reminder loop: webform → n8n alert → approval in Odoo.</li>\n <li>Donor pack: one graph, two sentences, one photo.</li>\n</ul>\n<h3>Governance wins</h3>\n<p>Restricted funds carry color labels so staff know which pesos can move and which must stay put.</p>\n<p>Variance alerts fire when a promise slips, giving Ana 48 hours to fix it before donors notice.</p>\n<p>Impact notes sync to donor cards, so every thank-you includes the latest field quote.</p>\n<p>Training new volunteers takes twenty minutes because the process lives inside the cards, not in Ana's head.</p>\n<ul>\n <li>Color coding: blue for scholarship, green for bus fuel, gold for emergency repairs.</li>\n <li>Two-minute onboarding video embedded on every card.</li>\n <li>Alert ladder: email at 24 hours, WhatsApp ping at 48, phone call at 72.</li>\n </ul>\n<h3>Results that stick</h3>\n<p>Reimbursements clear within 48 hours, so vendors keep extending gentle credit.</p>\n<p>Audit fees dropped by 15% because firms spend less time chasing evidence.</p><p>Board calls shifted from “Where is the proof?” to “Can we share this ledger publicly?”</p>\n<p>Staff now clock out at 7 p.m. because reconciliation no longer eats the night.</p>\n<p>Students narrate the dashboard column by column, which builds trust faster than static PDFs.</p><p>Grant partners log in as viewers and pull their own numbers instead of emailing last-minute lists.</p>\n<p>The finance committee now focuses on scenario planning rather than detective work.</p>\n<p><strong>Micro-CTA:</strong> Want Anas four-stop ledger map? Reply “ledger” and Ill send the template.</p>\n<h3>Story beats returned</h3>\n<p>The organization still hustles, but the hustle now has lanes, and Ana rides the 7:15 bus home holding a calm heart instead of a stack of IOUs.</p>\n<h3>Your invitation</h3>\n<p>Book a 30-minute impact-ledger clinic. Well map two of your funding streams, load them into an Odoo sandbox, and show how a reminder bot can keep receipts in line without late nights.</p>\n", "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>",
"date": "2026-03-22" "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 2040% 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": "Fundraising", "area": "Donor Communication",
"title": "From One-Time Gifts to Farm Friends: Tomas' Relationship Journal", "date": "2026-03-18",
"teaser": "<p>Tomas' relationship journal keeps every donor promise on time and turns farm visitors into recurring supporters.</p>", "title": "How Maya Turned Donor Chaos into Clear Connections",
"content": "<h3>From one-time gifts to farm friends</h3>\n<p>Tomas dreaded the silence after every big appeal because each unanswered thank-you felt like losing a future harvest.</p>\n<h3>Where the leaks lived</h3>\n<p>Welcome emails left days late since volunteers worked from separate inboxes.</p>\n<p>Birthdays hid in a paper planner that once soaked up a rainstorm.</p>\n<p>Corporate sponsors received the same PDF regardless of what crops they cared about.</p>\n<p>Board reviews turned into guessing games about which families might donate again.</p>\n<h3>The rebuild</h3>\n<p>Tomas moved everything into a lightweight Odoo workspace he calls the relationship journal.</p>\n<p>Each supporter card shows origin story, favorite program, and next touchpoint in one glance.</p>\n<p>He mapped three paths: new friend, seasonal donor, recurring guardian.</p>\n<p>Tasks now show one line: owner, due date, and purpose.</p>\n<p>n8n schedules nudges the moment a gift arrives, so someone always follows up within 48 hours.</p>\n<p>An AI drafting buddy writes first-pass thank-you notes using the details already on each card.</p>\n<h3>Data made friendly</h3>\n<p>Supporters are tagged by crop, channel, and volunteer interest in plain words like “loves cassava fairs.”</p>\n<p>Segment dashboards show which stories pull one-time donors into the recurring lane.</p>\n<p>Monthly reviews ask one question: Which color column needs attention?</p>\n<ul>\n <li>Blue tiles = families who gave at events.</li>\n <li>Orange tiles = businesses who sponsor seedlings.</li>\n <li>Purple tiles = diaspora donors following WhatsApp updates.</li>\n </ul>\n<h3>Human touches</h3>\n<p>Field photos upload straight from phones, and Odoo resizes them so they look great on any device.</p><p>Tomas records 60-second voice notes from the fields and attaches them to donor cards.</p>\n<p>Volunteers pull scripts straight from Odoo, so language stays warm even when teams rotate.</p>\n<p>Dashboard tiles show who has not heard from the collective in 30, 60, and 90 days.</p>\n<ul>\n <li>Green cards = steady supporters with next visit scheduled.</li>\n <li>Yellow cards = need a story update this week.</li>\n <li>Red cards = missed promise; assign a call immediately.</li>\n</ul>\n<h3>Metrics that matter</h3>\n<p>Open tasks dropped by half because every promise now has one owner and timestamp.</p><p>Recurring donors grew 38% once the journal kept promises on time.</p>\n<p>Average thank-you response time dropped to nine hours.</p>\n<p>Volunteers onboard in fifteen minutes because every step sits inside the card.</p>\n<p>Corporate partners now sponsor recurring kits because they see their updates logged in real time.</p><p>Board decks now show journey health instead of raw totals, so planning conversations stay calm.</p>\n<p><strong>Micro-CTA:</strong> Want the three-path relationship map Tomas uses? Reply “journey” and Ill send the canvas.</p>\n<h3>Implementation sprint</h3>\n<ul>\n <li>Export your top 50 donors and tag them by story interest.</li>\n <li>Draft three touchpoint paths with one welcome, one update, one invite.</li>\n <li>Connect n8n so each donation triggers the right checklist.</li>\n <li>Record one voice memo per week and attach it to the cards that need warmth.</li>\n</ul>\n<p>Teams that follow this sprint usually see dormant donors reply within two weeks.</p>\n<p>Volunteers love the clarity because they can grab the next action during a lunch break.</p><p>Quarterly reviews compare each path, so Tomas can tweak scripts without rebuilding the system.</p>\n\n<h3>Story beats returned</h3>\n<p>The farm still hustles, but now Tomas walks home at sunset knowing every donor hears a fresh story before the soil cools.</p>\n<h3>Your invitation</h3>\n<p>Spin up a free Odoo sandbox and lets load ten supporter cards together. In one 30-minute clinic well copy Tomas paths, hook up n8n reminders, and leave you with scripts volunteers can read tomorrow.</p>\n", "teaser": "<p>Maya transformed scattered donor messages into a smooth, trustworthy flow that grows support and keeps every team member aligned.</p>",
"date": "2026-03-18" "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 &amp; 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>"
},
{
"area": "Digital presence",
"title": "Turn Your NGO Website into a Field Walk",
"teaser": "<p>Elena's five-block layout makes every site visit feel like a river cleanup, even on a phone.</p>",
"content": "<h3>Turn your site into a field walk</h3>\n<p>Elena cringed every time someone opened their website at a street fair because the spinning loader felt like an apology.</p>\n<h3>Issues to fix</h3>\n<p>Pages mixed board minutes with volunteer invites, so nobody knew where to click.</p>\n<p>The donate form had eight required fields and crashed on older phones.</p>\n<p>Images lived on personal devices, so updates stalled whenever volunteers traveled.</p>\n<p>Analytics showed people bounced in under ten seconds.</p>\n<h3>Build blocks, not code</h3>\n<p>Elena moved the site into Odoos drag-and-drop builder so every block felt like a card.</p>\n<p>Each section fits on a phone screen: mission, todays project, proof, ways to help, calendar.</p>\n<p>Real photos upload straight from WhatsApp and resize automatically.</p>\n<p>Testimonials now sit beside the action they describe.</p>\n<p>Donate buttons follow the reader down the page with a single tap checkout.</p>\n<h3>Governance & workflow</h3>\n<p>Editorial calendar lives inside Odoo with color labels for campaigns, reports, and evergreen stories.</p>\n<p>Reviewers get one-click preview links, so approvals happen on WhatsApp instead of email chains.</p>\n<p>Accessibility checks run before publish, flagging low-contrast text in plain language.</p>\n<p>Translations sit beside each block, letting bilingual volunteers ship updates fast.</p>\n<h3>Automation assist</h3>\n<p>n8n resizes images, tags blogs by theme, and cross-posts stories to WhatsApp and email.</p>\n<p>Elena writes once and the reminder bot republishes everywhere.</p>\n<p>An AI headline buddy suggests two simple options per story, so copy stays human.</p><p>Heatmaps highlight which block loses attention so Elena can adjust the layout weekly.</p>\n<p>Error logs show if someones phone fails mid-form, helping the team fix issues before a campaign.</p>\n<p>Weekly summary emails pull site stats plus top-performing sections for the board.</p>\n<h3>Content structure</h3>\n<p>Downloadable kits live behind simple buttons, so teachers grab lesson plans without emails.</p><p>Each article follows a three-block rhythm: hook, proof, invite.</p>\n<p>Videos autoplay muted with captions so people understand the scene on crowded buses.</p>\n<p>Callout cards explain tools in plain phrases: “Map every cleanup in one shared calendar.”</p>\n<ul>\n <li>Hero row: bold photo + 12-word promise.</li>\n <li>Impact grid: three stats with icons.</li>\n <li>Action tray: volunteer form, donate, share.</li>\n</ul>\n<h3>Results</h3>\n<p>Average time on page tripled and bounce rate dropped below 30%.</p>\n<p>Volunteer signups happen on mobile because the form now has three fields.</p>\n<p>SEO basics improved because every block now has alt text and descriptive slugs.</p><p>Board members review live dashboards instead of PDFs.</p>\n<p>Funders cite the site during calls because proof is easy to show.</p>\n<p><strong>Micro-CTA:</strong> Need Elenas five-section layout? Reply “walk” and Ill send the wireframe.</p>\n<h3>Quick launch plan</h3>\n<ul>\n <li>Sketch five sections on paper and test with two volunteers.</li>\n <li>Upload one real photo per section; skip stock imagery.</li>\n <li>Wire n8n to resize and tag assets automatically.</li>\n <li>Schedule a weekly 30-minute content sweep to keep blocks fresh.</li>\n</ul>\n<p>Most teams ship a refreshed homepage in under two days using this plan.</p>\n<p>Keep a punch list of “next swap” items so volunteers can help during downtime.</p><p>Use simple analytics goals like “Time on page > 90 seconds” to measure if the story lands.</p>\n\n<h3>Story beats returned</h3>\n<p>The site finally feels like the river trail Elena loves, and neighbors stay long enough to smell the wet soil through their screens.</p>\n<h3>Your invitation</h3>\n<p>Book a website tune-up clinic. Well sketch your content blocks, load them into an Odoo sandbox, hook n8n to your photo sources, and leave you with headlines any volunteer can publish.</p>\n",
"date": "2026-03-12"
},
{
"area": "Operations",
"title": "Calm Ops for Five-Person Teams: Paula's Playbook",
"teaser": "<p>Paula's shared board, three kits, and reminder bots give her tiny team calm lanes again.</p>",
"content": "<h3>Small team, calmer ops</h3>\n<p>Paulas five-person crew once survived on coffee and miracles, and every missed grant email felt like letting the clinic down.</p>\n<h3>Where chaos hid</h3>\n<p>Tasks lived in WhatsApp, paper folders, and sticky notes taped to doors.</p>\n<p>Expense approvals stalled because the volunteer bookkeeper checked email at midnight.</p>\n<p>Volunteers requested time off via voice notes, so schedules overlapped without warning.</p>\n<p>Board calls devolved into status therapy instead of strategic planning.</p>\n<h3>Operating spine</h3>\n<p>Paula mapped every recurring task and moved it into Odoo boards labeled This Week, Blocked, Done.</p>\n<p>Each card lists owner, due date, and checklist in two lines.</p>\n<p>Expense records attach directly to tasks, so reimbursements sit next to receipts.</p>\n<p>Standard kits—clinic, workshop, grant—bundle supplies, links, and photos of “done.”</p><p>Each kit also lists backup owners so vacations stop being crises.</p>\n<ul>\n <li>Clinic kit: tents, coolers, intake forms, walkie-charge reminder.</li>\n <li>Workshop kit: markers, snacks, attendance sheet, follow-up template.</li>\n <li>Grant kit: metrics link, testimonial prompt, submission portal.</li>\n</ul>\n<h3>Visibility upgrades</h3>\n<p>Risk cards surface at the top whenever deadlines approach, so leadership sees trouble before it burns staff.</p>\n<p>Meeting notes live inside the task they reference, making handoffs painless.</p>\n<p>Quarterly calendars hang beside the board and mirror the same color codes.</p>\n<h3>Automation assist</h3>\n<p>n8n routes webforms, pings owners when tasks age, and batches approvals before lunch.</p><p>Escalations ladder from email to WhatsApp to phone call if a blocker lingers more than 48 hours.</p>\n<p>Volunteer hours log automatically when someone submits the attendance form.</p>\n<p>An AI summary buddy sends Paula a nightly paragraph with wins, risks, and supply levels.</p>\n<p>Color labels show which funder cares about each task, avoiding compliance missteps.</p><p>Grant attachments stay versioned inside the card, so no one emails “final_final.pdf” ever again.</p>\n<h3>Working rhythm</h3>\n<p>Monday stand-ups start with the shared board projected on the wall.</p>\n<p>Quiet Wednesdays block meetings so staff can finish deep work.</p>\n<p>Weekly “clear bins” hour means everyone empties inboxes while the board stays open.</p><p>Friday reflections capture lessons learned directly on the board so next week starts sharper.</p>\n<p>Energy check stickers track morale on a wall chart.</p><p>Red stickers trigger a five-minute retro so the team fixes friction before it festers.</p>\n<h3>Metrics</h3>\n<p>Grant packets now ship three days early.</p>\n<p>Average reimbursement time fell to 36 hours.</p><p>Procurement cycle time shrank by 40% because approvals travel with the task.</p>\n<p>Volunteer onboarding lasts 20 minutes because the process lives inside the cards.</p>\n<p>Funder check-ins shortened because dashboards answer status questions at a glance.</p><p>Burnout signals—sick days, tense meetings—dropped sharply once lanes were visible.</p>\n<p><strong>Micro-CTA:</strong> Want Paulas three-kit template? Reply “ops” and Ill send the board layout.</p>\n<h3>Four-step rollout</h3>\n<ul>\n <li>List every recurring task and tag who truly owns it.</li>\n <li>Clone Paula's three kits and rename them for your programs.</li>\n <li>Hook n8n to your intake forms so work lands in the right lane.</li>\n <li>Set two check windows per week to clear blockers in under 15 minutes.</li>\n</ul>\n<p>Teams usually reclaim 68 staff hours per week once this cadence sticks.</p>\n<p>Use those hours for prevention work: training, donor care, or rest.</p>\n\n<h3>Story beats returned</h3>\n<p>The organization still hustles, but the hustle now has lanes, and Paula walks home before sunset while the reminder bot keeps promises steady.</p>\n<h3>Your invitation</h3>\n<p>Book a free ops audit call. Well clone Paulas board in your sandbox, wire n8n to your forms, and leave you with a checklist that keeps five-person teams sane.</p>\n",
"date": "2026-03-05"
},
{
"area": "Donor data",
"title": "Build a Relationship Atlas Like Idris",
"teaser": "<p>Idris' relationship atlas turns scattershot spreadsheets into humane tags, reminders, and quick compliance wins.</p>",
"content": "<h3>Build a relationship atlas</h3>\n<p>Idris hated guessing which donors cared most because every wrong thank-you felt like letting someone down.</p>\n<h3>Pains</h3>\n<p>Six spreadsheets, three CRMs, and a shoebox of pledge cards all told different stories.</p>\n<p>Privacy requests took weeks because data sat in personal inboxes.</p>\n<p>Board members asked for regional insights he could only estimate.</p>\n<p>Volunteers kept their own contact lists, so follow-ups collided.</p>\n<h3>Single source of truth</h3>\n<p>Idris imported everything into Odoo and nicknamed it the relationship atlas.</p>\n<p>Each supporter card shows giving history, volunteer hours, favorite program, and preferred channel in one view.</p>\n<p>Tags read like stories: “cares about maternal health,” “joins WhatsApp lives,” “hosts clinics in the Chaco.”</p>\n<p>Consent fields sit at the top so compliance is a two-click job.</p><p>Notes include pronunciation tips and personal preferences so every interaction feels human.</p>\n<h3>Automation loop</h3>\n<p>n8n watches forms, webinars, and replies, then updates the right card automatically.</p>\n<p>Reminder bots create tasks when someone goes 45 days without a touch.</p>\n<p>An AI scribe scans meeting notes and suggests segments Idris might have missed.</p><p>Suggested segments come with sample language, so outreach stays consistent even when staff rotate.</p>\n<p>Alerts escalate from email to WhatsApp when a promise is about to slip.</p>\n<h3>Dashboards that talk like humans</h3>\n<p>Heat maps show supporters by region with real photos.</p>\n<p>Rhythm boards display who heard from the team this week, month, and quarter.</p>\n<p>Promise trackers highlight pending callbacks with color codes.</p><p>Leaders can filter by “next visit” to plan travel without extra spreadsheets.</p>\n<ul>\n <li>Blue = steady supporters ready for deeper invite.</li>\n <li>Yellow = needs story update within seven days.</li>\n <li>Red = missed commitment; assign personally.</li>\n</ul>\n<h3>Implementation steps</h3>\n<ul>\n <li>Export contacts from every source and label them with plain-language tags.</li>\n <li>Deduplicate inside Odoo using email + phone as anchors.</li>\n <li>Embed a simple preference form so supporters update their own data.</li>\n <li>Schedule weekly “atlas sweeps” to review alerts and close loops.</li>\n</ul>\n<p>These sweeps take 20 minutes when everyone owns their slice.</p>\n<p>Volunteers see the same view, so handoffs finally feel respectful.</p><p>Partner organizations can receive read-only links, keeping coalitions aligned without extra exports.</p>\n<h3>Operational wins</h3>\n<p>Event RSVPs sync automatically, so Idris knows who to greet by name.</p><p>Segmented campaigns now send health advocates clinic updates while corporate donors get quarterly kits.</p>\n<p>Lapsed supporters receive friendly voice memos instead of generic emails.</p><p>Data health score appears on the dashboard so the team knows when to clean duplicates.</p>\n<p>Deletion requests finish in under an hour with audit logs attached.</p><p>Data exports for funders now take minutes because filters remember the last settings.</p>\n<p>Retention jumped 12% because people hear from the team before they feel forgotten.</p><p>Board decks now include one slide per segment with photos and next steps.</p>\n<p><strong>Micro-CTA:</strong> Need the atlas tagging sheet? Reply “atlas” and Ill send the workbook.</p>\n<h3>Signals worth monitoring</h3>\n<p>Weekly: touchpoint gaps by segment.</p><p>Daily: check for bounced emails or unsubscribes so you can switch channels fast.</p>\n<p>Monthly: conversion from one-time to recurring gifts.</p>\n<p>Quarterly: data health score and consent updates.</p>\n<p>Flag anything red for 48 hours and assign a specific owner.</p>\n<h3>Story beats returned</h3>\n<p>The atlas means Idris walks into meetings knowing who needs care, and donors feel remembered instead of managed.</p>\n<h3>Your invitation</h3>\n<p>Start a free Odoo sandbox and bring three messy lists to a 30-minute map-making session. Well clean them live, wire n8n updates, and leave you with segments you can act on tomorrow.</p>\n",
"date": "2026-02-26"
},
{
"area": "Sustainability",
"title": "How Lila Turned Cash Flow into a Calm Heartbeat",
"teaser": "<p>Lila's money-weather calendar shows every cliff before it hits and keeps reserves growing.</p>",
"content": "<h3>Cash flow needs a heartbeat</h3>\n<p>Lila was tired of praying every March because each late rent payment felt like telling families their shelter might vanish.</p>\n<h3>Cliffs to smooth</h3>\n<p>Income arrived in unpredictable bursts while expenses stayed steady.</p>\n<p>Sticky-note calendars overlapped, so no one saw when grants and payroll collided.</p>\n<p>Vendors heard “soon” more than “paid,” which strained trust.</p>\n<p>Staff carried stress home because every storm meant emergency appeals.</p>\n<h3>Calendar overhaul</h3>\n<p>Lila sketched a wall-sized calendar and marked every income stream in one color.</p>\n<p>Expenses earned their own colors so cliffs jumped off the page.</p>\n<p>She moved the plan into Odoo so updates happen once and appear everywhere.</p>\n<p>Each funding source now has a simple card showing owner, due date, and health.</p>\n<p>Reserve tracker nudges her to move money into savings whenever cash exceeds a safe line.</p><p>Each nudge includes a suggested amount so she builds reserves in bite-sized transfers.</p>\n<h3>Data on one screen</h3>\n<p>Cards show amount expected, confidence level, and last touch in plain words.</p>\n<p>Color labels: green for confirmed, yellow for pending, red for risk.</p>\n<p>Scenario slider lets Lila test “what if grant X delays 30 days” without new sheets.</p>\n<h3>Automation guardrails</h3>\n<p>n8n watches deadlines, renewals, and grant reports, then pings owners before crunch time.</p>\n<p>Dashboards display twelve months of runway plus a “money weather” icon the team understands at a glance.</p>\n<p>Alerts escalate if reserves dip below target, giving leadership days to respond.</p><p>Expense approvals show impact notes so finance knows why the spend matters.</p>\n<p>Vendors submit invoices through a webform so approvals travel with documentation.</p><p>Vendors now see status labels on their invoices, so they know when funds will land.</p>\n<h3>Communication rhythm</h3>\n<p>Staff receive a monthly money weather note using sunny/cloudy/rainy icons.</p>\n<p>Quarterly “friends of Puentes” emails explain where funds went and whats next.</p>\n<p>Community budget circles vote on flexible funds and suggest low-lift fundraisers.</p><p>Notes from each circle feed back into Odoo so ideas become tasks instantly.</p>\n<p>Dashboards stream to a TV in the workspace, making cash position visible all day.</p><p>Board members log in remotely and leave comments on specific cards instead of firing off emails.</p>\n<ul>\n <li>Monday: review runway line and upcoming cliffs.</li>\n <li>Wednesday: confirm reserve transfers.</li>\n <li>Thursday: check subsidy renewals against alerts.</li>\n <li>Friday: send weather note before people log off.</li>\n</ul>\n<h3>Metrics</h3>\n<p>Reserve now covers two months of core costs.</p>\n<p>Vendors are paid within five days, improving terms.</p>\n<p>Emergency appeals dropped because runway forecasts flag issues early.</p><p>Scenario drills now happen quarterly so nothing feels like a surprise fire drill.</p>\n<p>Board meetings focus on partnerships instead of patching leaks.</p><p>Staff stress surveys show a 30% drop in “money worries” mentions.</p>\n<h3>Rollout plan</h3>\n<ul>\n <li>List every income stream with amount, timing, and confidence.</li>\n <li>Plot expenses in the same calendar, highlighting spikes.</li>\n <li>Move both into Odoo cards and tag owners.</li>\n <li>Set n8n alerts for renewals, reserve dips, and vendor payments.</li>\n</ul>\n<p>This takes one afternoon and replaces months of scrambling.</p>\n<p><strong>Micro-CTA:</strong> Want the money weather template? Reply “weather” and Ill send the kit.</p>\n\n<h3>Story beats returned</h3>\n<p>The organization still hustles, but the hustle now has lanes, and Lila rings a tiny bell every time the reserve line inches upward.</p>\n<h3>Your invitation</h3>\n<p>Subscribe to the NGO ops newsletter or book a funding calendar clinic. Well map your cliffs, load them into an Odoo sandbox, wire n8n alerts, and leave you with a calm heartbeat you can explain in five minutes.</p>\n",
"date": "2026-02-19"
} }
] ]
+685
View File
@@ -0,0 +1,685 @@
<!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>
${t.Minutes != null ? `<span style="margin-left:auto;opacity:0.55;font-size:12px;flex-shrink:0;">${t.Minutes}</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">&#128421; ${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 ? " &middot; 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 totalMinutes = todos.reduce(
(sum, t) => sum + (t.Minutes || 0),
0,
);
const totalHours = (totalMinutes / 60)
.toFixed(1)
.replace(/\.0$/, "");
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" style="display:flex;align-items:center;">Today's Todos<span style="margin-left:auto;font-size:13px;font-weight:normal;opacity:0.6;background:#2f3e4e;border-radius:10px;padding:1px 9px;">${totalHours}h</span></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;">
&#9888; 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>
+1
View File
@@ -0,0 +1 @@
google-site-verification: google261cafffa78d8c3d.html
+2510 -378
View File
File diff suppressed because it is too large Load Diff
+6 -8
View File
@@ -537,11 +537,10 @@
"license": "ISC" "license": "ISC"
}, },
"node_modules/picomatch": { "node_modules/picomatch": {
"version": "2.3.1", "version": "2.3.2",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
"dev": true, "dev": true,
"license": "MIT",
"engines": { "engines": {
"node": ">=8.6" "node": ">=8.6"
}, },
@@ -968,11 +967,10 @@
} }
}, },
"node_modules/tinyglobby/node_modules/picomatch": { "node_modules/tinyglobby/node_modules/picomatch": {
"version": "4.0.3", "version": "4.0.4",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
"dev": true, "dev": true,
"license": "MIT",
"engines": { "engines": {
"node": ">=12" "node": ">=12"
}, },
+2
View File
@@ -4,6 +4,8 @@
"description": "", "description": "",
"main": "index.js", "main": "index.js",
"scripts": { "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" "test": "echo \"Error: no test specified\" && exit 1"
}, },
"repository": { "repository": {
+31 -1
View File
@@ -3,7 +3,7 @@
@tailwind utilities; @tailwind utilities;
body { body {
font-family: 'Inter', 'Segoe UI', system-ui, sans-serif; @apply font-brand;
} }
#lang-selector { #lang-selector {
@@ -15,3 +15,33 @@ body {
color: #201824; color: #201824;
background-color: #ffffff; 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;
}
+15 -14
View File
@@ -1,27 +1,28 @@
/** @type {import('tailwindcss').Config} */ /** @type {import('tailwindcss').Config} */
module.exports = { module.exports = {
content: ['./index.html'], content: ["./index.html"],
theme: { theme: {
extend: { extend: {
colors: { colors: {
'brand-ink': '#201824', "brand-ink": "#201824",
'brand-primary': '#603F57', "brand-primary": "#603F57",
'brand-accent': '#F762B4', "brand-accent": "#F762B4",
'brand-sunrise': '#F8B84A', "brand-sunrise": "#F8B84A",
'brand-emerald': '#39B982', "brand-emerald": "#39B982",
'brand-snow': '#F5F2F7', "brand-snow": "#F5F2F7",
'brand-smoke': '#E5DCE8' "brand-smoke": "#E5DCE8",
}, },
fontFamily: { fontFamily: {
brand: ['Inter', 'Segoe UI', 'system-ui', 'sans-serif'] brand: ["system-ui", "-apple-system", "Segoe UI", "sans-serif"],
}, },
boxShadow: { boxShadow: {
brand: '0 20px 60px rgba(32, 24, 36, 0.15)' brand: "0 20px 60px rgba(32, 24, 36, 0.15)",
}, },
backgroundImage: { backgroundImage: {
'hero-gradient': 'radial-gradient(circle at top left, rgba(247,98,180,0.35), rgba(32,24,36,0.95))' "hero-gradient":
} "radial-gradient(circle at top left, rgba(247,98,180,0.35), rgba(32,24,36,0.95))",
}
}, },
plugins: [] },
},
plugins: [],
}; };