尚未检测到更新。GitHub 发布版本后会显示在这里。
个人 GitHub 账号下公开仓库的所有者可以登录并验证所有权,自行管理介绍,无需管理员批准。
登录并生成验证文件,在 30 分钟内将其提交到此仓库的默认分支。其他分支或分叉仓库中的文件无效。然后在下方检查文件,即可无需管理员批准直接管理项目。请保留文件以维持管理权限。
如需登录,将前往登录页面。登录后请回到此处,再次点击所选的验证按钮。
在项目文件夹中运行 /nomorevibe verify,即可验证所有权并自行管理介绍。
一分钟完成验证正在加载完整 README。 以下为已保存的摘录。
Your dzongkhag. Your data. Your right. An open-source civic-transparency platform for Bhutan: where public money goes — development projects, budgets and allocations by dzongkhag (and gewog/village, where documents go that deep) — with the funding source and a link to the original document on every figure.
Modelled on the district-dashboard approach of ForThePeople.in (MIT), reimplemented for Bhutan.查看完整 README ↗Your dzongkhag. Your data. Your right. An open-source civic-transparency platform for Bhutan: where public money goes — development projects, budgets and allocations by dzongkhag (and gewog/village, where documents go that deep) — with the funding source and a link to the original document on every figure.
/en/thimphu) with a module sidebar, and a national one (/en/bhutan): Overview · Budget & allocations · Projects · Funding sources · Weather & rainfall · Tenders · Leadership & offices.Next.js 15 (App Router, TypeScript) · PostgreSQL (pg, plain SQL migrations) · Vercel Cron collectors · Vitest.
| Source | How | Refresh |
|---|---|---|
| World Bank Projects & Operations API | automated (pipeline/worldbank.mjs) | daily |
| Asian Development Bank IATI file | automated (pipeline/adb.mjs; override with ADB_IATI_URL) | daily |
| Parliament of Bhutan member records (National Assembly + National Council) | automated (pipeline/leaders.mjs; override with PARLIAMENT_API) | daily |
| Kuensel and The Bhutanese feeds — leadership-change watch | automated (pipeline/news.mjs; override with KUENSEL_FEED / BHUTANESE_FEED) | daily, with the members |
| e-GP — Electronic Government Procurement System (PMDD, Ministry of Finance) — every tender currently out to bid (203 notices on 2026-10-11) | automated (pipeline/egp.mjs; override with EGP_BASE) | daily |
| Open-Meteo weather | automated (pipeline/weather.mjs) | daily (Vercel Hobby allows at most one run/day per cron; on a Pro plan change vercel.json to e.g. 37 */6 * * *) |
| Ministry of Finance, National Budget Report FY 2026-27 — national summary, sector allocation, and the allocation of every dzongkhag, thromde and all 205 gewogs | reference file data/reference/budget-2026-27.json | yearly (new budget each May) |
| NSB 2017 Population & Housing Census (Table 2.1) | reference file data/reference/population-2017.json | per census |
| Ministry of Health, Annual Health Bulletin 2025 (Table 6.1) — all hospitals, PHCs, sub-posts, HISCs with beds | reference file data/reference/health-facilities.json | yearly |
| MoESD Annual Education Statistics 2024 (Annexure IV) — all 566 schools by name and level | reference file data/reference/schools-2024.json | yearly |
| Parliament member records, as shipped | reference file data/reference/leaders.json — the fallback shown until the daily refresh above has run | on change |
| MoICE Annual Industry Report FY 2025-26 (Annexure 3) and National Employment Report 2024-25 — industry licences and jobs filled per dzongkhag | reference file data/reference/economy.json | yearly |
| Dzongkhag administration websites and dated appointment notices — Dzongdags and office contacts | reference file data/reference/dzongdags.json, each entry graded by how well it is confirmed | on change |
| MoF / MoHCA / MoAL & other ministries' budgets, allocations, tenders, offices | curated CSV in data/manual/ — these are PDFs/web pages with no API. Every row must cite its public document | on commit |
Reference files ship with the code and work even without a database. tests/reference.test.ts checks each against the publication's own totals (census sums, the Health Bulletin's per-dzongkhag totals rows, MoESD Table A, 47 NA seats with the constitutional split), so a transcription slip fails CI.
Collectors run via vercel.json crons → /api/cron/<job> (rate-limited per job; a CRON_SECRET bearer skips the limit), and from the Load / refresh data now button on the Data sources page. Each job is isolated: on failure the previous good rows stay, the source is flagged stale on the Data sources page, and the run is written to the update log. A batch with any invalid record loads nothing.
A job that runs and fails flags its own source stale. A job that stops running at all is the harder case: the
status stays ok and the data simply ages, which nothing inside the app notices. So /api/cron/alerts checks
the age of each source's last success once a day against a 72 h limit (STALE_ALERT_HOURS), and an overdue
source is reported in three places:
ALERT_WEBHOOK_URL, if set — one POST, at most once a day per source, plus a line when it recovers. The
body carries the message under text, content and message, which covers Slack, Google Chat, Discord and
ntfy without a per-service adapter;/api/health, which answers 503 while any source is overdue (degraded: "stale-sources") or the
database is unreachable (degraded: "database").Point any uptime monitor at /api/health: it is the only one of the three that still works when the scheduler
running the other two has itself stopped.
The daily leaders job reads Parliament's own member records and replaces the list — but only if the response is a complete Parliament: 47 National Assembly seats with the constitutional split per dzongkhag, and one elected National Council member per dzongkhag. Anything less is rejected, the previous list stays, and the source is marked stale, so a half-written or broken response can never leave a gap on the site. The job finds the current term itself (the highest-numbered Parliament that is fully seated), so it keeps working after the next election without a code change. Until the job has run, the bundled data/reference/leaders.json is shown instead.
Newspapers are a watch, never a source. The same job reads the Kuensel and The Bhutanese feeds and flags articles that read like a change of office-holder in a named dzongkhag. A flag is a dated link inviting someone to re-check that entry against the official source — no name on DrukView is ever taken from a newspaper, because a headline such as "Dasho takes charge in Trongsa" cannot be parsed into a correct name, and a wrong name on a transparency site is worse than a late one. A feed that fails never fails the members refresh.
Bhutan's e-GP portal publishes no API, feed or export, and its tender page is a shell: the rows are fetched by the page's own script. The daily tenders job makes that same request — POST /TenderDetailsServlet with funName=AllTenders, carrying a session cookie from a prior GET of the search page — and reads the bare run of <tr> rows it returns. This request was observed against the live portal, not inferred; the first version of this collector guessed at it, fetched nothing for a day, and is written up in ADR-0002.
What it collects is the portal's live register: viewType=Live is 11 pages of 20, 203 notices when last measured, every one still open. The job reads all of it — 11 requests, about 13 seconds — so DrukView holds every tender out to bid rather than an arbitrary slice. The portal's other views are not read: the unfiltered view is 731 pages of history, Archive is 685 pages of closed notices and Cancelled is 37. A page size above 20 makes the servlet drop the connection, so the page size stays at 20.
Because that response's shape is the whole agreement, the parser is the tested part (tests/egp.test.ts runs it against a captured real response) and the job is all-or-nothing: a response without a readable row fails the run, the previous notices stay published and the source is marked stale. A redesign at e-GP therefore shows up as a stale source, never as missing or invented notices — and the Tenders page says so in as many words, because "no tenders here" and "we could not read e-GP" are different claims and only one of them is about Bhutan.
A tenders page shows at most 500 notices, one more than that is fetched so a cut-off list can say it is cut off rather than passing for the whole register, and the e-GP live register held 203 when last measured. Three honesty constraints: e-GP publishes no estimated value, so DrukView shows none; each notice cites its own page on e-GP (ViewNotice.jsp?id=…, which opens without a session) rather than a search page; and a notice is placed in a dzongkhag only when the procuring agency's own hierarchy names exactly one — never from a place name in the title, where it is usually where the work is rather than who is buying it. Of 203 notices, 129 were placed in a dzongkhag and 74 left national.
DrukView is open source (MIT) and open to anyone. Start with CONTRIBUTING.md — it sets out the seven data rules (never invent a number; cite every row; never mix Nu. and US$; never guess an identity or a place; reject the batch, not the row; say what you do not know; keep the date), how to add cited CSV rows or a reference dataset, and how to write a collector.
The most valuable contribution needs no code: if a figure here is wrong, open a Data correction issue with a link to the public document that proves it. Also see the Code of Conduct and, for anything security-related, SECURITY.md — report privately, never in a public issue. CI runs typecheck, tests and a production build on Node 20 and 22 for every push, plus a check that every curated row cites an https document and that no secret is committed.
cp .env.example .env.local # set DATABASE_URL (CRON_SECRET optional)
npm install
npm run db:migrate && npm run db:seed # optional: the app also does this itself on first visit
npm run job -- manual # load data/manual/*.csv
npm run job -- worldbank # needs network access to the source
npm run dev # http://localhost:3000
Tests: npm test (unit). Database integration tests run when TEST_DATABASE_URL points at a throwaway Postgres database (CI does this automatically; the suite drops and recreates the public schema there).
DATABASE_URL — the only required setting.Optional: ALERT_WEBHOOK_URL (where to send a notice when a source has not refreshed for three days — any Slack/Google Chat/Discord/ntfy incoming-webhook URL) and STALE_ALERT_HOURS to move that limit.
Optional: CRON_SECRET. The data jobs only read public data, so they run without a secret but at most once per interval (World Bank/ADB and Parliament 6 h, e-GP tenders 3 h, weather 2 h, curated CSVs 10 min; 5 min after a failure). A request carrying Authorization: Bearer $CRON_SECRET skips that interval. Every job stops after 50 s and records why, so a slow source can never fail silently.
The automated sources were checked against their real responses, and the parsers' tests use trimmed copies of those (tests/fixtures/*real*):
project_abstract = {"cdata!": …}, US-style dates (12/31/2027 12:00:00 AM), and totalcommamt "0" (= not reported, shown as such, never "US$0").https://www.adb.org/iati/iati-activities-bt.xml is valid IATI 2.03; 62 activities are merged into 38 projects (e.g. grants 0088 + 0089 → project 39221-022, US$14.30M, matching adb.org). IATI status 3 "Finalisation" = completed (adb.org shows these as Closed). Sector codes are resolved to names from the official IATI codelist (shared/iati-sectors.json).src/geo/bhutan-dzongkhags.json, public/geo/bhutan-gewogs.json) from OCHA ROAP / HDX via geoBoundaries (gbHumanitarian 2019, CC BY 3.0 IGO): 20 dzongkhags + 205 gewogs from one consistent dataset; every gewog's dzongkhag was assigned geometrically and checked against the official count per dzongkhag (tested).What is still unproven is the runtime on your host (network egress, timeouts, database permissions): run the collectors once after deploying and read the Data sources page.
/en/…) but ships English only; a Dzongkha translation will be added once a native speaker can review it.