<!DOCTYPE html><html><head><meta charSet="utf-8"/><meta name="viewport" content="width=device-width, initial-scale=1"/><link rel="stylesheet" href="/_next/static/css/8fe28fa6b3de985f.css" data-precedence="next"/><link rel="preload" as="script" fetchPriority="low" href="/_next/static/chunks/webpack-538344e1ec16ef6b.js"/><script src="/_next/static/chunks/4bd1b696-49012485b3ca2f76.js" async=""></script><script src="/_next/static/chunks/684-5c2ddb8b7e49e93d.js" async=""></script><script src="/_next/static/chunks/main-app-f38f0d9153b95312.js" async=""></script><script src="/_next/static/chunks/app/layout-02ca981f6e1344ee.js" async=""></script><script src="/_next/static/chunks/635-25b07200ed7b345e.js" async=""></script><script src="/_next/static/chunks/453-421af95ed4166657.js" async=""></script><script src="/_next/static/chunks/43-b40fe770b6144d31.js" async=""></script><script src="/_next/static/chunks/app/%5Blocale%5D/layout-fad3f3b8971eaa47.js" async=""></script><title>NGO Jobs</title><meta name="description" content="NGO job leads review dashboard"/><script>document.querySelectorAll('body link[rel="icon"], body link[rel="apple-touch-icon"]').forEach(el => document.head.appendChild(el))</script><meta id="__next-page-redirect" http-equiv="refresh" content="1;url=./leads"/><script src="/_next/static/chunks/polyfills-42372ed130431b0a.js" noModule=""></script></head><body class="font-sans"><div hidden=""><!--$--><!--/$--></div><!--$!--><template data-dgst="NEXT_REDIRECT;replace;./leads;307;"></template><div class="min-h-screen flex items-center justify-center"><div class="text-muted-foreground">Loading...</div></div><!--/$--><script src="/_next/static/chunks/webpack-538344e1ec16ef6b.js" async=""></script><script>(self.__next_f=self.__next_f||[]).push([0])</script><script>self.__next_f.push([1,"1:\"$Sreact.fragment\"\n5:I[9665,[],\"OutletBoundary\"]\n8:I[4911,[],\"AsyncMetadataOutlet\"]\na:I[9665,[],\"ViewportBoundary\"]\nc:I[9665,[],\"MetadataBoundary\"]\ne:I[6614,[],\"\"]\nf:I[6946,[\"558\",\"static/chunks/app/layout-02ca981f6e1344ee.js\"],\"AuthProvider\"]\n10:I[7555,[],\"\"]\n11:I[1295,[],\"\"]\n12:\"$Sreact.suspense\"\n13:I[4911,[],\"AsyncMetadata\"]\n15:I[2103,[\"635\",\"static/chunks/635-25b07200ed7b345e.js\",\"453\",\"static/chunks/453-421af95ed4166657.js\",\"43\",\"static/chunks/43-b40fe770b6144d31.js\",\"450\",\"static/chunks/app/%5Blocale%5D/layout-fad3f3b8971eaa47.js\"],\"default\"]\n16:I[5920,[\"635\",\"static/chunks/635-25b07200ed7b345e.js\",\"453\",\"static/chunks/453-421af95ed4166657.js\",\"43\",\"static/chunks/43-b40fe770b6144d31.js\",\"450\",\"static/chunks/app/%5Blocale%5D/layout-fad3f3b8971eaa47.js\"],\"NgoHeader\"]\n17:I[2152,[\"635\",\"static/chunks/635-25b07200ed7b345e.js\",\"453\",\"static/chunks/453-421af95ed4166657.js\",\"43\",\"static/chunks/43-b40fe770b6144d31.js\",\"450\",\"static/chunks/app/%5Blocale%5D/layout-fad3f3b8971eaa47.js\"],\"CommitInfo\"]\n:HL[\"/_next/static/css/8fe28fa6b3de985f.css\",\"style\"]\n0:{\"P\":null,\"b\":\"tGA8V-KIyAizZMtLDMNOB\",\"p\":\"\",\"c\":[\"\",\"sitemap.xml\"],\"i\":false,\"f\":[[[\"\",{\"children\":[[\"locale\",\"sitemap.xml\",\"d\"],{\"children\":[\"__PAGE__\",{}]}]},\"$undefined\",\"$undefined\",true],[\"\",[\"$\",\"$1\",\"c\",{\"children\":[[[\"$\",\"link\",\"0\",{\"rel\":\"stylesheet\",\"href\":\"/_next/static/css/8fe28fa6b3de985f.css\",\"precedence\":\"next\",\"crossOrigin\":\"$undefined\",\"nonce\":\"$undefined\"}]],\"$L2\"]}],{\"children\":[[\"locale\",\"sitemap.xml\",\"d\"],[\"$\",\"$1\",\"c\",{\"children\":[null,\"$L3\"]}],{\"children\":[\"__PAGE__\",[\"$\",\"$1\",\"c\",{\"children\":[\"$L4\",null,[\"$\",\"$L5\",null,{\"children\":[\"$L6\",\"$L7\",[\"$\",\"$L8\",null,{\"promise\":\"$@9\"}]]}]]}],{},null,false]},null,false]},null,false],[\"$\",\"$1\",\"h\",{\"children\":[null,[\"$\",\"$1\",\"9NovBime929dzcLsUYDCmv\",{\"children\":[[\"$\",\"$La\",null,{\"children\":\"$Lb\"}],null]}],[\"$\",\"$Lc\",null,{\"children\":\"$Ld\"}]]}],false]],\"m\":\"$undefined\",\"G\":[\"$e\",\"$undefined\"],\"s\":false,\"S\":false}\n2:[\"$\",\"html\",null,{\"suppressHydrationWarning\":true,\"children\":[\"$\",\"body\",null,{\"classNam"])</script><script>self.__next_f.push([1,"e\":\"font-sans\",\"children\":[\"$\",\"$Lf\",null,{\"children\":[\"$\",\"$L10\",null,{\"parallelRouterKey\":\"children\",\"error\":\"$undefined\",\"errorStyles\":\"$undefined\",\"errorScripts\":\"$undefined\",\"template\":[\"$\",\"$L11\",null,{}],\"templateStyles\":\"$undefined\",\"templateScripts\":\"$undefined\",\"notFound\":[[[\"$\",\"title\",null,{\"children\":\"404: This page could not be found.\"}],[\"$\",\"div\",null,{\"style\":{\"fontFamily\":\"system-ui,\\\"Segoe UI\\\",Roboto,Helvetica,Arial,sans-serif,\\\"Apple Color Emoji\\\",\\\"Segoe UI Emoji\\\"\",\"height\":\"100vh\",\"textAlign\":\"center\",\"display\":\"flex\",\"flexDirection\":\"column\",\"alignItems\":\"center\",\"justifyContent\":\"center\"},\"children\":[\"$\",\"div\",null,{\"children\":[[\"$\",\"style\",null,{\"dangerouslySetInnerHTML\":{\"__html\":\"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}\"}}],[\"$\",\"h1\",null,{\"className\":\"next-error-h1\",\"style\":{\"display\":\"inline-block\",\"margin\":\"0 20px 0 0\",\"padding\":\"0 23px 0 0\",\"fontSize\":24,\"fontWeight\":500,\"verticalAlign\":\"top\",\"lineHeight\":\"49px\"},\"children\":404}],[\"$\",\"div\",null,{\"style\":{\"display\":\"inline-block\"},\"children\":[\"$\",\"h2\",null,{\"style\":{\"fontSize\":14,\"fontWeight\":400,\"lineHeight\":\"49px\",\"margin\":0},\"children\":\"This page could not be found.\"}]}]]}]}]],[]],\"forbidden\":\"$undefined\",\"unauthorized\":\"$undefined\"}]}]}]}]\nd:[\"$\",\"div\",null,{\"hidden\":true,\"children\":[\"$\",\"$12\",null,{\"fallback\":null,\"children\":[\"$\",\"$L13\",null,{\"promise\":\"$@14\"}]}]}]\n7:null\n"])</script><script>self.__next_f.push([1,"3:[\"$\",\"$L15\",null,{\"locale\":\"sitemap.xml\",\"now\":\"$D2026-06-13T09:08:21.892Z\",\"timeZone\":\"UTC\",\"messages\":{\"leads\":{\"title\":\"NGO Leads\",\"filter_all\":\"All\",\"filter_pending\":\"Pending\",\"filter_approved\":\"Approved\",\"filter_rejected\":\"Rejected\",\"filter_sent\":\"Sent\",\"filter_postponed\":\"Postponed\",\"filter_outbox\":\"Outbox\",\"filter_sources\":\"Filter by source\",\"approve\":\"Approve\",\"reject\":\"Reject\",\"skip\":\"Skip / Postpone\",\"send\":\"Send\",\"send_all\":\"Send All Approved\",\"send_selected\":\"Send Selected\",\"select_all\":\"Select All\",\"unselect_all\":\"Unselect All\",\"no_leads\":\"No leads found\",\"loading\":\"Loading leads…\",\"status_pending\":\"Pending\",\"status_approved\":\"Approved\",\"status_rejected\":\"Rejected\",\"status_sent\":\"Sent\",\"status_opened\":\"Opened\",\"status_clicked\":\"Clicked\",\"run_scraper\":\"Run Scraper\",\"scraper_running\":\"Scraper running…\",\"scraper_triggered\":\"Scraper triggered\",\"restore\":\"Restore to pending\"},\"header\":{\"title\":\"NGO Jobs\",\"logout\":\"Sign out\"},\"documentation\":{\"title\":\"Documentation\",\"subtitle\":\"Complete system documentation for the ngojobs platform — scraping, classification, outreach, and the learning system.\",\"section_system_overview\":\"System Overview\",\"section_lead_management\":\"Lead Management\",\"section_scraping_portals\":\"Scraping \u0026 Portals\",\"section_email_generation\":\"Email Generation \u0026 Sending\",\"section_workflow_engine\":\"Workflow Engine\",\"section_authentication\":\"Authentication \u0026 Authorization\",\"section_api_reference\":\"API Reference\",\"section_deployment\":\"Deployment \u0026 Operations\",\"section_development\":\"Development\",\"faq_ngojobs_what_title\":\"What is ngojobs?\",\"faq_ngojobs_what_body\":\"ngojobs is an automated job scraping and outreach platform for Swiss NGO recruitment. It scrapes jobs from multiple Swiss job portals, classifies them as NGO-relevant using AI, enriches them with contact data, and sends personalized outreach emails to hiring managers. The system is built with Next.js 15 (TypeScript strict), Python pipeline on Dokku (single Docker container), with MongoDB as the primary data store and SQLite for workflow engine state.\",\"faq_architecture_title\":\"What is the architecture overview?\",\"faq_architecture_body\":\"Frontend: Next.js 15 with TypeScript strict, served at ngojobs.kampaspace.ch. Users authenticate via Google OAuth, view leads in a filterable table, approve/reject/skip leads, and manage the outbox. Python API Server: FastAPI on port 8008 (workflow_engine/api.py). Handles lead CRUD, workflow triggering, SMTP sending, tracking pixels, and vote API. Pipeline Engine: Python workflows (YAML definitions in engine/workflows/). Each workflow has steps backed by plugins in engine/plugins/. Data Stores: MongoDB (ngo_leads collection, lead_votes collection, entities) is the primary store. SQLite (data/workflows.db) stores workflow definitions, runs, and component scores. LLM Providers: DeepSeek (primary), MiniMax, Claude, Gemini — used for classification, email generation, and relevance checking.\",\"faq_pipeline_title\":\"What does the full pipeline workflow look like?\",\"faq_pipeline_body\":\"1. ngo_job_scraper workflow triggers daily. 2. ngo_job_scraper plugin scrapes listings from ~14 Swiss portals (jobs.ch, jobscout24, jobwinner, sozjobs, etc.). 3. ngo_leads_writer plugin upserts raw leads into MongoDB. 4. ngo_relevance_checker plugin classifies each job as NGO-relevant via DeepSeek LLM (caches per employer). 5. ngo_data_normalizer plugin extracts and normalizes contact info, job details. 6. ngo_email_generator plugin generates personalized outreach emails using DeepSeek. 7. ngo_lead_mongo_writer writes final lead data to MongoDB. 8. Admin reviews leads in UI — approves, rejects, skips, or moves to outbox. 9. ngo_email_sender (manual or scheduled) sends approved leads via SMTP, sorted by quality_score. 10. Tracking pixels record opens/clicks and update quality_score.\",\"faq_statuses_title\":\"What are the lead statuses?\",\"faq_statuses_body\":\"pending — Newly scraped, not yet reviewed by admin. approved — Admin approved, ready to be sent or moved to outbox. rejected — Admin rejected (e.g. not actually an NGO, wrong sector). Additionally, a lead can be skipped (temporarily postponed, max 30 days) or placed in outbox (approved but not yet sent, allows email editing before sending).\",\"faq_outbox_title\":\"What is the outbox and how does it work?\",\"faq_outbox_body\":\"The outbox is a staging area for approved leads. When you move a lead to outbox: the lead stays approved but is not immediately sent; you can edit the email_subject and email_body before sending; you can send individual leads from outbox via the Send button; you can send all outbox leads in batch (future feature). Outbox leads have outbox=1 and outbox_created_at set in MongoDB.\",\"faq_quality_score_title\":\"What is the lead quality score?\",\"faq_quality_score_body\":\"Every lead has a quality_score (0.0 to 1.0) computed from three weighted signals: Vote Signal (35%) — upvotes vs downvotes from user feedback. Cold-start neutral at 0.5 until 20 votes, then converges to true positive rate. Engagement Signal (35%) — email open rate + click rate. Neutral 0.5 if not yet sent. Pipeline Quality (30%) — average of ngo_confidence (0–100 from LLM) and email_confidence (0–1 from email finder). Quality score is recomputed via update_lead_quality_score() whenever votes change or when email engagement is recorded.\",\"faq_voting_title\":\"How does user voting affect leads?\",\"faq_voting_body\":\"Users can upvote or downvote any lead via the ThumbsUp/ThumbsDown buttons in the LeadDetailPanel. Each device gets a ngojobs_device_id stored in localStorage — this is the voter_id (no login required). Votes are stored in MongoDB lead_votes collection with a compound unique index on (lead_id, voter_id). Each vote update triggers a quality_score recompute. The email sender sorts approved leads by quality_score descending before sending — highest quality leads get sent first.\",\"faq_portals_title\":\"Which job portals are scraped?\",\"faq_portals_body\":\"Currently scraped portals (~14 total): jobs.ch, jobscout24.ch, jobwinner.ch, sozjobs.ch, publicjobs.ch, job-room.ch, nzz.ch/jobs, naturschutz.ch, cinfo.ch, profonds.org, linkedin.com, greenpeace.org, caritas.ch, worldbank.org (Careers). See engine/plugins/scrapers/portals/ for all scraper implementations.\",\"faq_relevance_title\":\"How does the relevance classifier work?\",\"faq_relevance_body\":\"The NgoRelevanceChecker plugin sends each job to the DeepSeek LLM with a system prompt asking it to classify as NGO-relevant or not (output: is_ngo bool, confidence 0–100, reasoning, category). Verdicts are cached per employer in workflow_memory (SQLite) to avoid repeated LLM calls for the same organization. The confidence threshold is configurable (default: 50). Jobs below threshold are filtered out unless keep_non_ngo=true. The threshold is dynamically adjusted based on the component's trust weight from the learning system: high trust → lower bar (more permissive), low trust → higher bar (stricter).\",\"faq_dedup_title\":\"How are job listings deduplicated?\",\"faq_dedup_body\":\"The ngo_deduplicator plugin checks each new lead against existing leads in MongoDB using a composite key of (portal + job_url). Exact URL duplicates are skipped. Hash-based fuzzy dedup (simhash on title+employer) is available for near-duplicates.\",\"faq_email_gen_title\":\"How are outreach emails generated?\",\"faq_email_gen_body\":\"The OutreachEmailGenerator plugin uses DeepSeek to generate personalized emails. It receives the lead's job data (title, employer, summary, contact first name) and produces: email_subject — compelling subject line referencing the specific role. email_body — personalized body (300–500 chars) explaining why the recipient should hire through Kampajobs, with a soft call-to-action. The LLM call uses force_provider=deepseek and caches results per employer+role combination.\",\"faq_email_send_title\":\"How does email sending work?\",\"faq_email_send_body\":\"The ngo_email_sender plugin reads approved+unsent leads from MongoDB, sorts them by quality_score (highest first), and sends via SMTP. Each email includes: tracking pixel (/api/track/open/{lead_id}) to record opens. Click tracking — job URLs are replaced with redirect links (/api/track/click/{lead_id}?url=...) to record clicks. Set dry_run=true in the workflow YAML to log sending intent without actually mailing.\",\"faq_tracking_title\":\"What tracking is in place for email engagement?\",\"faq_tracking_body\":\"Open tracking: A 1x1 transparent GIF pixel at the bottom of every email. When loaded, it calls /api/track/open/{lead_id} which marks opened=1 and opened_at on the lead in MongoDB. Click tracking: All job URLs are replaced with redirect URLs /api/track/click/{lead_id}?url={encoded_url}. When clicked, it marks clicked=1, clicked_at, then redirects to the real URL. Both signals feed into the quality_score computation (engagement signal, 35% weight).\",\"faq_workflow_engine_title\":\"How does the workflow engine work?\",\"faq_workflow_engine_body\":\"Workflows are defined in YAML files in engine/workflows/. Each workflow has a name, schedule (manual/cron), and ordered steps. Each step references a plugin component. The runner (src/workflow_engine/runner.py) executes workflows as asyncio tasks. It loads the YAML definition, iterates steps, calls each plugin's execute(input_data) method, and chains outputs to next inputs. Workflows are registered to the SQLite database at server startup via _register_workflows_from_disk(). Cron workflows are scheduled via APScheduler.\",\"faq_workflows_exist_title\":\"What workflows exist?\",\"faq_workflows_exist_body\":\"ngo_job_scraper — Daily portal scraping + classification + email generation. Schedule: daily cron. ngo_email_sender — Manual trigger to send approved leads. Can also be triggered via API POST /api/leads/send-approved. All workflows are defined in engine/workflows/*.yaml and registered in SQLite on startup.\",\"faq_hitl_title\":\"What is HITL (Human-in-the-Loop)?\",\"faq_hitl_body\":\"Phase 3 of the pipeline enables Human-in-the-Loop pauses. When a workflow step calls request_pause(run_id, question), the runner pauses and stores the HITL request in SQLite. The frontend polls GET /api/runs/{run_id}/hitl-pending. When admin submits an answer via POST /api/runs/{run_id}/hitl-response, the runner resumes. Currently, no workflow steps request HITL pauses — this is infrastructure ready for future use.\",\"faq_learning_system_title\":\"How does the learning system (Prompt 22) work?\",\"faq_learning_system_body\":\"Vote Infrastructure: Each lead can be upvoted/downvoted by device (localStorage ngojobs_device_id). Votes stored in MongoDB lead_votes collection. FastAPI endpoints at GET/POST/DELETE /api/leads/{lead_id}/vote. Next.js proxy at app/api/leads/[id]/vote/route.ts. Quality Score: compute_quality_score() in src/lead_quality/scorer.py combines vote signal (35%), engagement signal (35%), and pipeline quality (30%) into a 0.0–1.0 score. update_lead_quality_score() recomputes and persists to MongoDB on every vote change. Feedback Loop: Email sender sorts by quality_score (highest first). Relevance checker adjusts confidence threshold based on component trust weight from SQLite component_scores table.\",\"faq_auth_title\":\"How does authentication work?\",\"faq_auth_body\":\"The frontend uses Google OAuth via useAuth() hook in lib/auth.tsx. Auth state is stored in localStorage and sent as a Bearer token to Python API endpoints. The Python API validates JWT tokens using JWT_SECRET (from src/auth/main.py or env). Development mode allows WORKFLOW_ENGINE_DEV_AUTH=1 to bypass auth. Roles: viewer (read-only), editor (can approve/reject/skip/send), admin/superadmin (full access including delete and workflow management).\",\"faq_roles_title\":\"What permissions does each role have?\",\"faq_roles_body\":\"viewer — View leads only. Cannot approve, reject, send, or modify. editor — Can approve, reject, skip, restore, move to outbox, send individual emails from outbox. admin — All editor permissions + delete leads, add/delete job sources, regenerate emails, batch send. superadmin — All admin permissions + create/update/delete workflows, trigger any workflow run.\",\"faq_api_endpoints_title\":\"What are the main API endpoints?\",\"faq_api_endpoints_body\":\"Leads: GET /api/leads (list, filter by approval/sent/skipped), GET /api/leads/{id} (single lead), PATCH /api/leads/{id}/approve, PATCH /api/leads/{id}/reject, PATCH /api/leads/{id}/skip, PATCH /api/leads/{id}/restore, POST /api/leads/{id}/outbox, PATCH /api/leads/{id}/update-outbox, POST /api/leads/{id}/send-from-outbox, POST /api/leads/send-approved (batch). Votes: GET /api/leads/{id}/vote?voter_id=X (get vote + counts), POST /api/leads/{id}/vote (submit/update), DELETE /api/leads/{id}/vote?voter_id=X (remove). Tracking: GET /api/track/open/{lead_id} (1x1 GIF), GET /api/track/click/{lead_id}?url=X (redirect).\",\"faq_env_vars_title\":\"What environment variables are required?\",\"faq_env_vars_body\":\"MongoDB: MONGODB_URI, MONGODB_DATABASE=ngojobs_mongo. LLM: DEEPSEEK_API_KEY, DEEPSEEK_MODEL, MINIMAX_API_KEY, ANTHROPIC_API_KEY. Auth: GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, JWT_SECRET. SMTP: SMTP_HOST, SMTP_PORT=587, SMTP_USER, SMTP_PASSWORD, SMTP_FROM_ADDR. App: ENV=production, WORKFLOW_ENGINE_DEV_AUTH=0. Next.js: NEXT_PUBLIC_SERVER_URL, PYTHON_API_URL.\",\"faq_deploy_title\":\"How is the system deployed?\",\"faq_deploy_body\":\"Dokku (Docker-based PAAS) on server 37.27.29.113. Single Docker container runs both Next.js (port 3001) and Python FastAPI (port 8008). SSH: ssh -i ~/.ssh/id_ed25519_kampaspace_opencode root@37.27.29.113. Container name: ngojobs.web.1. Deploy: git push dokku main from the ngojobs repo (double nested: ngojobs/ngojobs/). Logs: dokku logs ngojobs.web.1.\",\"faq_restart_title\":\"How do I restart the service?\",\"faq_restart_body\":\"SSH to the server and run: dokku ps:restart ngojobs. Or rebuild and restart: dokku ps:rebuild ngojobs.\",\"faq_mongodb_title\":\"How do I check MongoDB data?\",\"faq_mongodb_body\":\"Connect to MongoDB at mongodb://37.27.29.113:27017, database ngojobs_mongo. Use mongosh CLI or MongoDB Compass. Check vote counts with: db.lead_votes.aggregate([{$group: {_id: '$lead_id', up: {$sum: 1}}}]). Check leads: db.ngo_leads.find({}, {metadata.source_id:1, data.title:1, data.approval:1}).limit(3).toArray().\",\"faq_local_dev_title\":\"How do I run the project locally?\",\"faq_local_dev_body\":\"1. Install dependencies: npm install in ngojobs/. 2. Copy .env.example to .env and fill in keys. 3. Start MongoDB locally or set MONGODB_URI to remote. 4. Run npm run dev — starts Next.js on port 3001. 5. Run Python API separately: cd ngojobs \u0026\u0026 uvicorn src.workflow_engine.api:app --port 8008 --reload.\",\"faq_tests_title\":\"How do I run tests?\",\"faq_tests_body\":\"Unit tests (no server needed): cd ngojobs \u0026\u0026 pytest tests/e2e/test_vote_learning_system.py -v -k 'not API and not Proxy'. Scraper E2E tests (requires network): cd ngojobs \u0026\u0026 pytest tests/e2e/test_scraper_e2e.py -v. Against remote server: pytest tests/e2e/test_vote_learning_system.py -v --remote.\",\"faq_structure_title\":\"What is the project structure?\",\"faq_structure_body\":\"ngojobs/ngojobs/ — project root (double nesting). app/ — Next.js App Router pages and API routes. components/ — React components (layout, ngo, ui). engine/ — Python pipeline plugins and workflows. plugins/ — ngo_*.py plugins (scraper, classifier, etc.). workflows/ — YAML workflow definitions. src/ — Python source (workflow_engine, auth, agent_team, lead_quality). data/ — SQLite DB, uploads, screenshots. tests/ — pytest and Playwright tests.\",\"section_email_templates\":\"Email Template Selection \u0026 Rules\",\"faq_email_templates_title\":\"How do we choose which email template to use?\",\"faq_email_templates_body\":\"We have three email templates that are automatically selected based on the lead's customer status:\\n\\n**Existing Customer Template:**\\nUsed when the organization has previous order history with us. This template includes:\\n- {{order_count_text}}: The number of previous job postings (e.g., \\\"5 Stelleninserate\\\")\\n- {{year_range}}: The span of years they've worked with us (e.g., \\\"2021–2023\\\")\\n\\n**New Customer B Template:**\\nUsed when the organization is marked as a Kampajobs customer but has no previous order history (order_count = 0). This is a special welcome template for new customers.\\n\\n**New Customer Template:**\\nUsed when the organization is not yet a Kampajobs customer. This is our standard outreach template for brand new prospects.\",\"faq_existing_clients_title\":\"How do we determine if an organization is an existing client?\",\"faq_existing_clients_body\":\"We determine existing client status through PostgreSQL data:\\n\\n**Order History:**\\nWe query the kampajobs database for all previous job postings (orders) from the organization. If order_count \u003e 0, the organization has purchase history.\\n\\n**Kampajobs Customer Flag:**\\nEach lead has a `kampajobs_client` field that marks whether the organization is/was a Kampajobs customer.\\n\\n**Combination:**\\nAn organization is considered an \\\"existing customer\\\" only when BOTH conditions are true:\\n1. `kampajobs_client` = Yes\\n2. `order_count` \u003e 0 (has previous orders in PostgreSQL)\\n\\nIf kampajobs_client = Yes but order_count = 0, they're treated as a new customer (template B). If kampajobs_client = No, they're a prospect regardless of order history.\",\"faq_placeholder_mapping_title\":\"What placeholders are available in email templates?\",\"faq_placeholder_mapping_body\":\"Email templates support these placeholders that are automatically replaced with actual data:\\n\\n**All Templates:**\\n- {{salutation}}: Formal greeting based on gender (\\\"Sehr geehrte Frau\\\" / \\\"Sehr geehrter Herr\\\" / \\\"Guten Tag\\\")\\n- {{last_name}}: Contact person's last name\\n- {{employer}}: Organization name\\n- {{job_title}}: Job title from the posting\\n- {{COUPON_CODE}}: Special offer code (replaced when coupon is assigned)\\n\\n**Existing Customer Template Only:**\\n- {{order_count_text}}: \\\"N Stelleninserate\\\" (e.g., \\\"3 Stelleninserate\\\")\\n- {{year_range}}: Year span (e.g., \\\"2021–2023\\\")\\n\\nTemplates are managed in PostgreSQL and updated without requiring container restart (cached for up to 1 hour).\"}},\"children\":[\"$\",\"div\",null,{\"className\":\"flex flex-col h-screen\",\"children\":[[\"$\",\"$L16\",null,{}],[\"$\",\"main\",null,{\"className\":\"flex-1 overflow-hidden\",\"children\":[\"$\",\"$L10\",null,{\"parallelRouterKey\":\"children\",\"error\":\"$undefined\",\"errorStyles\":\"$undefined\",\"errorScripts\":\"$undefined\",\"template\":[\"$\",\"$L11\",null,{}],\"templateStyles\":\"$undefined\",\"templateScripts\":\"$undefined\",\"notFound\":\"$undefined\",\"forbidden\":\"$undefined\",\"unauthorized\":\"$undefined\"}]}],[\"$\",\"$L17\",null,{}]]}]}]\n"])</script><script>self.__next_f.push([1,"b:[[\"$\",\"meta\",\"0\",{\"charSet\":\"utf-8\"}],[\"$\",\"meta\",\"1\",{\"name\":\"viewport\",\"content\":\"width=device-width, initial-scale=1\"}]]\n6:null\n9:{\"metadata\":[[\"$\",\"title\",\"0\",{\"children\":\"NGO Jobs\"}],[\"$\",\"meta\",\"1\",{\"name\":\"description\",\"content\":\"NGO job leads review dashboard\"}]],\"error\":null,\"digest\":\"$undefined\"}\n14:{\"metadata\":\"$9:metadata\",\"error\":null,\"digest\":\"$undefined\"}\n4:E{\"digest\":\"NEXT_REDIRECT;replace;./leads;307;\"}\n"])</script></body></html>