Local, self-hosted dashboard to search funding calls (Austria/EU mix) from configurable sources.
Start here:
docs/HANDOVER.md
More:
docs/ARCHITECTURE.mddocs/SOURCES.mddocs/OPERATIONS.mddocs/AI_MATCHING.md
- Start the server:
python3 /Users/ajz/Desktop/notes/Work/Coxed/app.py serve
- Open the UI:
- Pull data (optional; can take a bit):
python3 /Users/ajz/Desktop/notes/Work/Coxed/app.py refresh --source ffgpython3 /Users/ajz/Desktop/notes/Work/Coxed/app.py refresh --source eu
The dashboard can rank calls against pasted CV text using OpenAI embeddings (and optionally generate short match explanations).
Setup:
- Set an API key in your shell (server-side only):
export OPENAI_API_KEY="..."(orGRANTDASH_OPENAI_API_KEY)
Models:
- Embeddings:
text-embedding-3-smallviaPOST /v1/embeddings. - Explanations:
gpt-4.1-miniviaPOST /v1/responses(configurable).
Notes:
- Your CV text is only sent when you click Find matches.
- Don’t put the API key in any browser code; it must stay in environment variables on the server.
You can run matching fully locally with Ollama:
- Install and start Ollama (it runs a local HTTP server on
http://localhost:11434). - Pull models you want to use, e.g.:
ollama pull embeddinggemma(embeddings)ollama pull gemma3(optional explanations)
- Start GrantDash with Ollama selected:
export GRANTDASH_AI_PROVIDER=ollamapython3 app.py serve
Environment variables:
GRANTDASH_AI_PROVIDER=ollama|openai(auto-selects if unset)GRANTDASH_OLLAMA_HOST(defaulthttp://localhost:11434)GRANTDASH_OLLAMA_EMBED_MODEL(defaultembeddinggemma)GRANTDASH_OLLAMA_CHAT_MODEL(defaultgemma3)
Ollama endpoints used:
POST /api/embedfor embeddingsPOST /api/chatfor optional explanations
There are two separate limits:
- How much you ingest (refresh):
refreshpulls a finite number of pages per source (default--max-pages 3).- Increase it to ingest more:
python3 app.py refresh --source eu --max-pages 20
- Increase it to ingest more:
- How much the UI shows per request: the UI paginates results (25/50/100 per page). Use Load more to page through.
The safe way to avoid missing open/upcoming calls for scraped sources is:
- Fetch “open + forthcoming” (not just open), and
- Refresh frequently (daily/weekly depending on how often the source changes), and
- Use
--max-pages 0occasionally as a backfill to make sure you’re not capped by paging.
Suggested cadence:
- EU (search backend): daily if you cache; otherwise live query + optional small cache for resilience.
- FFG (HTML listings): daily/2–3x per week.
- Other funders without APIs (HTML/PDF pages): daily/weekly depending on volume.
Backfill example (scan until exhausted):
python3 app.py refresh --source eu --max-pages 0python3 app.py refresh --source ffg --max-pages 0
This project also tracks first_seen_at / last_seen_at per call in the DB so you can audit freshness.
Implemented:
eu(EU Funding & Tenders portal-backed search)ffg(FFG HTML listing scrape)fwf(FWF portfolio program pages scrape; open submissions only)vw(VolkswagenStiftung HTML listing scrape)thyssen(Fritz Thyssen Stiftung “Nächste Fristen” scrape; lightweight)
UI placeholders (checkboxes exist, fetchers not implemented yet): (none)
Environment variables:
GRANTDASH_DB_PATH(default:./data/grants.db)GRANTDASH_PORT(default:8765)GRANTDASH_EU_API_KEY(default:SEDIA)
Notes:
- The EU source uses the same
api.tech.ec.europa.eu/search-apiendpoint that powers the Funding & Tenders Portal search (multipart/form-data). It is not an officially documented public API and may change. - FFG currently does not advertise a public API for open calls; this project parses the public HTML pages.
For student assistants and future maintainers.
- Create a feature branch:
git checkout -b <topic> - Make changes, keep them small and source-scoped.
- Quick sanity checks:
PYTHONPYCACHEPREFIX=/private/tmp/grantdash-pycache python3 -m py_compile app.py grantdash/server.py- Start server:
python3 app.py serveand smoke test:- Update database (1–2 sources)
- Search (keyword + filter)
- Find matches (if AI is configured)
- Commit + push:
git add -A && git commit -m "<message>" && git push
- Implement in
grantdash/sources/<source>.pywith afetch_<source>_calls(...)function. - Wire into
grantdash/refresh.py(_fetch_source_calls). - Add UI checkbox in
static/index.html+static/app.js. - Document it in
docs/SOURCES.md. - Run a refresh for that one source and verify:
- non-zero results
- stable IDs (
<source>:<url-or-key>) - deadlines/status parse as expected
- Update user-facing copy in
static/index.htmlif UX changes. - Update docs when behavior changes:
docs/HANDOVER.mdfor workflowsdocs/SOURCES.mdfor ingest changes/known limitationsdocs/AI_MATCHING.mdfor provider/model config