尚未偵測到更新。GitHub 發布版本後會顯示在這裡。
個人 GitHub 帳號下公開儲存庫的擁有者可以登入並驗證所有權,自行管理介紹,無需管理員核准。
登入並產生驗證檔案,在 30 分鐘內將其提交至此儲存庫的預設分支。其他分支或分叉儲存庫中的檔案無效。然後在下方檢查檔案,即可無需管理員核准直接管理專案。請保留檔案以維持管理權限。
如需登入,將前往登入頁面。登入後請回到此處,再次點擊所選的驗證按鈕。
在專案資料夾中執行 /nomorevibe verify,即可驗證所有權並自行管理介紹。
一分鐘完成驗證正在載入完整 README。 以下為已儲存的摘錄。
Prayer times, live audio, Qur'an, zakat and community alerts for Taiyabah Masjid.
Bolton Central Islamic Society · Registered charity 1041569
31a Draycott Street, Bolton BL1 8HD
A dedicated app for Taiyabah Masjid, built so the community gets the masjid's
own prayer timetable, announcements and resources directly — not filtered
through a third-party aggregator. It installs to the home screen, works
offline, and its notifications reach the congregation the moment something
needs saying.
Progressive web app: no app store, no install friction, one URL.檢視完整 README ↗The masjid's own prayer times, notices, Qur'an and services — in one app,offline, in four languages.
Bolton Central Islamic Society · Registered charity 1041569 31a Draycott Street, Bolton BL1 8HD
[!NOTE] Reading this without a technical background? Start at The problem, What the app is and One Friday, end to end. Those three explain what this is and what it does for the congregation. Everything after How the pieces actually fit is for whoever maintains it.
Every picture in this file uses the same four colours, and they always mean the same thing. The connectors animate, so you can see which way the information actually travels.
| Means | |
|---|---|
| ⬛ Deep plum, gold border | This repository — our code, the masjid's own data |
| 🟩 Pale green | Live, in the congregation's hands today |
| 🟨 Sand, dashed border | In development — not built, and not described as built |
| ⬜ Stone | A person, or something outside the app |
Somebody wants to know when Jumuʿah is. Before this app there were four places to look and no way to tell which one the masjid had actually said.
The point is not that an app is nicer. It is that the times are the masjid's own published timetable and nothing else. There is no calculation engine in this repository. A third-party app can be excellent and still be wrong here, because it is answering a different question: what the astronomy says, rather than what this masjid prints on its board.
Four tabs, and a menu that opens in place rather than taking you somewhere else.
[!IMPORTANT] Two things carry the "in development" tag, and they stay tagged until they are real. Membership status publishes what membership is and when the fee falls due, but is not wired to the committee's members list, so it cannot tell an individual whether they have paid. Recordings — past bayaans inside the app's own player — waits on written permission from eMasjid Live, whose infrastructure holds the archive. The live stream itself works today.
Nothing else is tagged. Before adding or removing a tag anywhere, check what the code does rather than what this file says.
It installs to the home screen, works with no signal, and is also on Google Play as a Trusted Web Activity over the same URL — one codebase, not two.
The clearest way to explain the app is to follow a single Friday: a reminder sent at nine, a timetable that rearranges itself, and two automatic alerts before the jamāʿah.
[!TIP] The cross-check in the middle is the part worth knowing. The app shows times embedded in
index.html; the reminder scheduler readsdata/timetable-2026.json. Both are generated from the same source, but a refresh that updated one and not the other would announce a time nobody can see. So every scheduled run compares the two before sending — and halts with a logged discrepancy rather than guessing.
Three things are deliberately kept apart: the files the phone runs, the database that holds requests, and the one place a secret is allowed to exist.
[!CAUTION] The Supabase
service_rolekey must never appear in this repository, inindex.html, or anywhere else a browser can reach it. It bypasses Row Level Security entirely. It exists in exactly one place — Cloudflare's secret store — where the Worker uses it to write a notice. The OneSignal REST key can message the entire congregation and lives in the same one place: never in the repository, never inadmin.html, never in a browser.If GitHub's secret scanning ever blocks a push, do not click "Allow secret." Cancel, remove the key, rotate it.
The publishable key is in the app, and that is fine: Row Level Security is
the real access control. With it the app can write a hall booking, a nikāḥ
request or a request for the imam's advice, and read the notices view,
availability and term dates. It cannot read any of those requests back, and it
cannot write a notice. That was verified by querying as the anon role, not
inferred from the grants.
Hall bookings, nikāḥ requests, advice requests and notices all live in the same
Supabase project the masjid's website uses, so a request made in the app
lands in the same queue the office already works from. db/001_notices.sql is
this repository's own migration, applied by hand to the shared project.
Everything else in that schema belongs to the website repository.
Generated, never hand-entered, and never calculated.
Current dataset: 1 January – 31 December 2026 (1447–1448 AH).
[!WARNING] The 2027 timetable is the one dated job in this repository, and it cannot be done here. It needs the masjid's own published timetable; prayer times are never calculated and never invented. On 1 January 2027 the embedded dataset runs out, the app falls to its "timetable ended" state, and the automatic jamāʿah and Sūrah al-Kahf reminders stop. The masjid publishes the following year's around late November.
This used to say the Worker "fetches data/timetable-2027.json, finds
nothing" — it did not. TIMETABLE_URL was a constant naming the 2026 file,
which still exists on 1 January, so the fetch succeeded, today's date simply
was not in it, and the run returned { skipped: "no timetable entry for …" }
every minute for ever. The failure was quieter than the document describing it.
Three things now make it loud, because the day it matters nobody will be reading this file:
/api/health reports the timetable's last date and the days left, so the
question has an answer somebody can go and look atadmin.html and nobody opens a
Cloudflare logA fourth now asks on its own, because all three of those still need somebody to
go and look. .github/workflows/timetable-watchdog.yml runs every Monday
and asks the database the same question the app asks, through the same
publishable key and the same prayer_year() function. Until November a missing
next year is a note; from 1 November it fails the job, which turns the
Actions tab red and emails the repository owner, weekly, until it is done. It
also checks the year that is running — a part-published or duplicated year is
an error on any date — and fails loudly when it cannot reach the database at
all, because a check that had silently stopped working would look exactly like
a clean bill of health.
Its rules are unit-tested (native/scripts/test/next-year.test.mjs) rather than
only exercised live, because this repository's sandbox has no route to Supabase,
so the deciding half had to be verifiable without the network. That design paid
for itself immediately: the watchdog's first real run failed with "could not ask
the database", which is how the publishable key bug below was found.
[!NOTE] A scheduled workflow only runs from the default branch, and GitHub switches scheduled workflows off after 60 days with no activity in the repository (it emails first). If that mail arrives, press Enable workflow on this one.
The year is an argument now, so nothing in the pipeline is edited:
data/raw_timetable_<year>.txt, in the existing format.data/year-2026.json to data/year-<year>.json and fill in the two
things only the printed board can tell you: the Hijri year on 1 January, and
a few hand-checked days. Do not guess either — the script refuses to run
without them, and a day count does not catch a year read the American way
round.python3 data/parse_timetable.py <year> — it verifies and writes nothing at
all on any failure.index.html, replacing the const DATA block.data/timetable-<year>.json too — the scheduler reads it, and the
cross-check halts reminders if the two ever disagree.data/VERIFICATION.md, checking the generated times
against the printed board, before deploying.Step 6 is not optional. Automated checks confirm internal consistency; only a person can confirm it matches what the masjid intends.
[!NOTE] The refactor that made the year an argument was proved by rebuilding 2026 with it: the output is byte-identical to the committed file. The guards were proved by breaking them — no year, a missing
year-<year>.json, spot checks belonging to another year, and a config with no spot checks at all.
This is the single most important constraint in the notification code, and the one most likely to be broken by a well-meaning change.
[!IMPORTANT]
PREF_ORDERis positional and append-only. Inserting a preference in the middle, or reordering it, does not break a build — it silently reassigns the meaning of every value already stored on every subscriber's device. Somebody who asked only for janāzah alerts would start receiving everything, or stop receiving anything. New preferences go on the end. A release check holds the order, and holdsindex.htmland the Worker to the same names.
The obvious design gives each preference its own tag. That was built, and
OneSignal answered 409 entitlements-tag-limit. Hence one tag, p.
A notice is a send and a record, in that order: admin.html posts to the
Worker, the Worker uploads the poster to the Supabase notices bucket, inserts
the row through publish_notice(), then sends the notification — with the
poster as big_picture on Android, ios_attachments on iOS and
chrome_web_image on desktop. The notification body is truncated on a word
boundary at 180 characters; the full text is in the notice.
[!NOTE]
/api/test-remindersis not a test. It runs the real send path against real subscribers. It is safe only in the sense that the scheduler's own deduplication stops a reminder going out twice for the same prayer — if one is genuinely due when you press it, it goes out. It also covers the jamāʿah reminders only, not the Friday al-Kahf one.iOS: web push only reaches devices where the app has been added to the Home Screen. A bookmarked tab receives nothing. That is an Apple restriction, and it is what makes install instructions part of the feature.
Worth understanding before changing anything, because it is subtle and it has bitten this app twice.
The app's start_url is index.html, which the service worker serves from its
own cache. The code a phone runs is the code cached when that cache was
written — not what is on the server. Bump CACHE in sw.js with every
user-visible change. It is one line, and it is the difference between shipping
and appearing to ship. Current: taiyabah-v164.
Home
history.back(), and until v164 nothing in the app had ever pushed a
history entry, so Back left the start URL — which finishes the activity. Back
with nothing open still leaves the app; this makes Back work, it does not trap
anybodyPrayer times
Reminders on screen
Notifications
admin.html), password-protected, sending through
the masjid's own Cloudflare WorkerRamadan
null in ramadan/content.js
and reads "To be confirmed" rather than disappearing — a row that vanishes
looks like a row nobody promised. Nothing computable lives in that file: the
night, the times and the juz are read from the timetable at the moment of
asking, because a time copied into a content file is a time that will be wrong
the year the timetable movesindex.html#ramadan and
native/src/screens/Ramadan.jsx, with the gate itself in native/src/ramadan.js
so it can be unit-tested rather than read. native/scripts/check-ramadan.mjs
holds the two halves to each other — 175 assertions. The drawn design and what
was deliberately not built from it are in
Hajj & Umrah (built, and held)
by field. The drafts in hajj/content.js
exist to save the ʿulamā a blank page. Until one is approved it is invisible
to every reader and cannot open anything, whoever flips the switchnative/scripts/check-hajj.mjs enforces both that
and all three locks — 170 assertions, and 59 drafts are currently waiting on
the ʿulamā, none of them visible to anyoneRecite
scripts/build-hadith-duas.mjs)Qibla
Listen live
Zakat calculator
Giving
The madrasah
Life-stage services
Hall hire and education
The imam's advice
imam role on the masjid's website — there is no imam mailbox to maintain and
no address published for anyone to harvestCharity collections
Notices
Community information
Settings
The form asks for a date and a prayer, the requester's own details, and then the five people the masjid records for every nikāḥ: the bridegroom, the bride, the bride's representative and two witnesses, each with a full name, an age and a home address.
[!IMPORTANT] The bridegroom and the bride are refused under 18, in the app and again in the database. Since February 2023 performing a marriage ceremony for anyone under 18 is a criminal offence in England and Wales, whether or not the marriage is registered. That is not a preference and it is not a setting.
This is the second fault that screen had, and the first one hid it. The
duplicate id="nk-email" meant Submit threw before anything was sent, so the
request never reached the database — and the database had required those five
people since the website added them. Fixing the id took the form from "does
nothing" to "refused every time", with "The bridegroom is missing from the
request".
nkProbe() could not see it, and that is the lesson worth keeping. It asks
whether the function exists by posting an empty payload and reading the 400 —
but that 400 is the function's own date validation firing first, so the
people requirement was never reached and a function that refused every real
submission passed the test. Existence is not compatibility. Check 3l was written
from the same idea and inherited the same blind spot.
NK_PEOPLE is now the single list the markup ids, the validation and the
payload are all built from, because three copies of a list like that is how one
of them ends up with four entries. Check 3l holds NK_FORM_CAN_SUBMIT to it:
turn the form on without sending all five roles, or drop one of the 25 fields,
and the build fails.
Tested against the live database, not against a fixture. The contract was
read out of the function, probed inside a transaction that rolled back, then the
browser was driven through the real form — click a day, click a prayer, fill
twenty-five fields, press send — and the captured payload was submitted for
real. It created NK-26-0014 with all five people, their ages and their
postcodes, and the row was then deleted. Refusals were checked too: a blank
person flags all five of their fields, a 17-year-old groom or bride flags the
age, a missing postcode flags that box, and nothing is sent in any of those
cases.
anon holds no privileges at all on either table. The app writes through a
SECURITY DEFINER function and cannot read a single row back; the office reads
them under verified_office() with two-step completed.
The repository root is the public website — GitHub Pages serves it verbatim. Anything committed here is reachable by URL.
├── index.html The app. Self-contained — the whole year's
│ timetable is embedded, so prayer times need
│ no network request.
├── admin.html Password-gated notification compose screen.
├── privacy.html The privacy notice. Its URL is what the app
│ stores are given, so it must stay reachable.
├── delete-data.html The account and data deletion route Play requires.
├── sw.js Service worker — offline shell, push handlers,
│ and the update path.
├── manifest.webmanifest Home-screen install metadata.
├── twa-manifest.json The Android wrapper's configuration.
├── LICENSE.md Ownership, the charity's licence, and the
│ third-party content this does NOT cover.
│
├── .github/workflows/
│ ├── release-checks.yml Runs scripts/check-release.mjs on every push.
│ └── deploy-worker.yml Deploys the Worker automatically on push,
│ so no local tooling is ever required.
│
├── scripts/ Build and verification. Node, no dependencies.
│ ├── check-release.mjs 38 checks — the release gate. See below.
│ ├── check-i18n.mjs Measures translation coverage against the app.
│ ├── check-back-button.mjs Drives a real browser through eleven Back
│ │ scenarios. Not in CI; needs Playwright.
│ ├── build-diagrams.mjs assets/diagrams/*.mmd → the README's SVGs.
│ ├── i18n-keys.mjs Extracts every translatable string there is.
│ ├── build-lang.mjs lang/src/*.json → lang/{ur,gu,ar}.js
│ ├── fetch-quran.mjs Builds quran/surahs/ from the source text.
│ ├── verify-rabbanas.mjs Checks the 40 Rabbanā against the Qur'an.
│ ├── build-quran-duas.mjs Lifts the Qur'anic duʿās out of quran/surahs/
│ ├── verify-quran-duas.mjs by word span, and holds them to it after.
│ ├── build-hadith-duas.mjs The same for the duʿās that come from hadith,
│ ├── verify-hadith-duas.mjs out of the public-domain Arabic editions.
│ └── build-bukhari.mjs Builds quran/hadith/bukhari/ — 97 books.
│
├── assets/diagrams/ The pictures in this README, as .mmd source
│ beside the .svg each one builds to.
│
├── fonts/ The four typefaces, served from this origin
│ ├── fonts.css rather than from Google. fonts.css is
│ ├── *.woff2 Google's own stylesheet with the URLs
│ └── OFL.txt rewritten — same files, same unicode-range,
│ so the same glyphs. OFL.txt is the licence.
│ One file is not Google's: a 692-byte cut of
│ Noto Naskh Arabic holding U+08E2 alone, the
│ one Qurʼanic mark Google carves out of its
│ own Arabic range. Declared in index.html.
│
├── push/onesignal/ OneSignal's own service workers, kept on a
│ └── … separate scope so they don't collide with
│ sw.js. Duplicated one level down because
│ the dashboard still asks for the old path.
│
├── worker/ Notification backend — a Cloudflare Worker.
│ ├── worker.js Holds the OneSignal REST key and the Supabase
│ ├── wrangler.toml service key as secrets; handles manual sends,
│ ├── hash-password.js notices, and the scheduled jamāʿah and
│ ├── REMINDERS.md Sūrah al-Kahf reminders. See README.md to
│ └── README.md deploy, REMINDERS.md for the scheduler.
│
├── db/ Migrations owned by the app rather than the
│ └── 001_notices.sql website. Applied to the shared Supabase
│ project by hand, once.
│
├── store/ App-store material — the Play listing copy and
│ ├── PLAY-LISTING.md answers, the screenshots and the feature
│ ├── PERMISSION-LETTERS.md graphic, and two letters ready to send for
│ ├── screenshots/ permission to bundle an English hadith
│ ├── feature-graphic.png translation. Not served to users; kept here
│ └── promo/ so the listing can be rebuilt from the app.
│
├── .well-known/ Digital Asset Links, so the Android wrapper can
│ └── assetlinks.json prove it owns this domain and drops the
│ browser bar. Both signing certificates —
│ Play App Signing and the upload key.
│
├── lang/ Language packs.
│ ├── src/*.json The editable source — one file per area,
│ │ each key as [Urdu, Gujarati, Arabic].
│ └── ur.js gu.js ar.js Generated. Never edit these by hand.
│
├── quran/hadith/bukhari/ Ṣaḥīḥ al-Bukhārī, Arabic. Third-party open
│ ├── index.json data — it carries its own LICENCE.txt and is
│ ├── LICENCE.txt NOT covered by this repository's code licence.
│ ├── search.json Built by scripts/build-bukhari.mjs.
│ └── c/1.json … c/71.json
│
├── ramadan/content.js What the COMMITTEE supplies for Ramadan — the
│ taraweeh rota, the two bayaan slots, the
│ iʿtikāf booking, the fitrana, Eid. Half of it
│ is null on purpose; a null reads as "To be
│ confirmed" rather than vanishing. Nothing
│ computable lives here: the night, the times
│ and the juz are read from the timetable at
│ the moment of asking. One file, both
│ platforms — the website loads it directly and
│ native/scripts/import-content.mjs turns it
│ into the app's ramadan.json.
│
├── hajj/content.js The same arrangement for Hajj & ʿUmrah, and
│ the section is HELD. Every ruling in here is
│ a draft until a scholar's name is in its `by`
│ field; without one it is invisible to every
│ reader and cannot open its branch, whatever
│ the release switch says. 59 are waiting.
│
├── docs/design/ramadan/ The Ramadan screens as they were drawn, kept
│ so the build has something to be checked
│ against — and a README recording what was
│ deliberately NOT built from them, and the two
│ things only the masjid can create.
│
├── native/ The React Native app that is now on Play —
│ ├── src/screens/ the same words as index.html, lifted rather
│ ├── scripts/ than retyped. Its own gate: 26 checks and
│ └── README.md 246 unit tests. See native/README.md.
│
├── quran/ Qur'anic content — lazy-loaded, so the main
│ ├── surahs/index.json app never pays for carrying it.
│ │ └── 1.json … 114.json
│ ├── mushaf/indopak13/ The 13-line mushaf: 848 page images and an
│ │ ├── index.json index carrying its source, licence, and the
│ │ └── p/1.webp … 848.webp page each sūrah and juz begins on.
│ ├── athkar.js duas.js
│ └── rabbanas.js
│
├── portal/ The madrasah portal, mirrored from the website.
│
└── data/ Timetable pipeline — not served to users.
├── parse_timetable.py Converts the masjid's published PDF into
├── timetable-2026.json the app's dataset, with verification.
└── VERIFICATION.md Also read once a minute by the Worker's
reminder scheduler.
scripts/check-release.mjs runs on every push and is the reason a mistake in
this repository tends to be caught by a build rather than by somebody in the
congregation. It is not a linter. Each check exists because something went
wrong once, and each is written so that removing the behaviour it guards makes
the build fail.
There are 38, and each prints what it confirmed rather than a tick. Among them:
| Check | What it caught |
|---|---|
| Qur'an complete | 114 sūrahs and 6,236 āyāt, every count against the canonical table |
| 40 Rabbanā | each duʿā matched against the Qur'an text, not trusted as transcribed |
| Duʿās | 12 categories pinned so a duʿā cannot be inserted mid-list and slide three languages of translation onto the wrong Arabic; the Qur'anic ones checked against quran/surahs/, the hadith ones against the text they were lifted from, and none repeating another |
| Translations | 2,178 strings in all three languages, nothing missing and nothing spare |
| Ramadan | the two platforms' gate numbers and month pattern agree; the gate is run on fixture months for the Fajr rollover and a 29-day month's last ten; no clock time can be typed into ramadan/content.js; no field in it is drawn nowhere; the booking stays dark until the database answers yes; and a null place count cannot become a zero, which would read as fully booked. native/scripts/check-ramadan.mjs, 175 assertions |
| Hajj & ʿUmrah | all three locks — the release switch, the per-branch gate, and the rule that a fiqh entry counts only if a scholar's name is in its by field — plus the rule that a picture never says which option passes. native/scripts/check-hajj.mjs, 170 assertions |
| Language packs | lang/*.js still matches what lang/src would build — twice now, a translation was edited and the generated pack was not rebuilt, leaving English on an Urdu screen |
| Latin identifiers | 30 postcodes, phone numbers and account numbers that must not be re-numeralled — "Bolton BL1 8HD" once became "Bolton BL۱ ۸HD" |
| Arabic marks | scripture on a font stack that actually has glyphs for the marks it ships |
| CSS variables | every custom property used is defined — an undefined one silently drops the whole declaration |
| Duplicate selectors | a second copy of a rule quietly overriding the first, which is how the 40 Rabbanā lost their padding |
| Donations | all 24 Stripe links present, in live mode, with no old shop links surviving |
| Update path | five behaviours that together let a new build and new words reach an installed phone without a reinstall |
| Android Back |
node scripts/check-release.mjs # the release gate
node scripts/check-i18n.mjs # translation coverage
Both are plain Node with no dependencies, and both run in CI anyway. Running them first saves a red build.
The native app has its own gate, because the things that can be wrong in it
are different ones: npm run check in native/ is 26 checks and npm test is
246 unit tests, and the release workflow adds four more that only matter when a
bundle is being cut — a smoke run on that exact commit, the publishable key
matching the website's, the permission list read back out of the merged
manifest, and the signing certificate read back out of the finished .aab. See
native/README.md.
[!NOTE]
scripts/check-back-button.mjsis deliberately not in CI. It drives a real browser through eleven scenarios and asserts only on what is visible from outside — 39 checks. It needs Playwright and a server, neither of which this repository declares, and a suite that silently cannot run is worse than none. The static part of that behaviour is check 3n2, which does run on every push.
Most of this repository is original work under one licence. Three things are not, and the distinction matters more than it looks — both app stores can ask to see the right to ship content, and "it was on GitHub" is not an answer.
The Qurʼan. The Arabic is the IndoPak (King Fahd Complex Nastaliq) text,
converted to standard Unicode, from
quran-api (edition
ara-quranindopak). The English is Abdullah Yusuf Ali (1934), which is out
of copyright, taken from Tanzil — en.yusufali, one ayah
per line, which is the file the JSON editions in circulation are made from.
scripts/fetch-quran.mjs fetches both and refuses to write anything that does
not match the canonical ayah counts.
It needs a font that can draw it. The IndoPak edition uses Arabic
Extended-A as well as the main Arabic block — the waqf signs, the sajda and
rukūʿ marks, the sakta, U+08D4 to U+08E2 — and 1,031 of the 6,236 āyāt carry at
least one. Amiri has a glyph for none of them. Scripture is therefore set in
Noto Naskh Arabic on both sides: .quranic on the website, F.quranic in
the app, with Amiri kept for short display Arabic. native/scripts/check-arabic.mjs
fails the build if any Arabic character in the bundled data has no glyph in the
font that draws it, or if a Qurʼanic mark turns up in a file Amiri draws.
[!NOTE] The app's four typefaces are renamed, and nothing else about them is touched. iOS registers a bundled font under the names inside the file and React Native looks it up by family, so a font whose file is
X.ttfbut which calls itself "X Y" is bundled, loaded, and never used — it draws in the system font with no error.native/scripts/name-fonts.pyrewrites each name table so the font answers to its own file name, which is the name Android resolves by anyway;native/scripts/check-fonts.mjsfails the build if one ever drifts. Same glyphs, same outlines, same metrics. None of the four families reserves a font name, so the OFL permits it — the web copies infonts/are untouched and still byte-for-byte Google's.
[!WARNING] Take the translation from Tanzil, not from an API. The English used to come from the same JSON API as the Arabic, and that edition is a punctuation-stripped derivative of the Tanzil file: every ayah had its closing mark shaved off, so not one verse in 6,236 ended in a full stop, 1,112 carried an opening quotation mark with nothing to close it, and the twelve ayahs that open with the disjointed letters lost their first letter outright — Yusuf Ali's "A. L. M." came out as "L. M". 6,215 of 6,236 verses were wrong.
The words were right in all but those twelve. Only the punctuation was gone, which is why it survived every count this script already did: 114 sūrahs, 6,236 āyāt, nothing empty, numbering unbroken. A translation can be whole and still not be the translation. The script now also measures how many āyāt end on a mark of punctuation and refuses a source below 95% — Yusuf Ali's own text is 99.7%, the stripped edition 12.5%.
Ṣaḥīḥ al-Bukhārī is Arabic text and book structure from
hadith-api (edition ara-bukhari),
released under The Unlicense — an outright dedication to the public domain.
Nothing must be attributed as a condition and nothing must be passed on.
quran/hadith/bukhari/LICENCE.txt records it anyway, and the provenance is
printed on screen, because where a text came from is worth knowing even when no
licence compels it.
The same editions — ara-muslim, ara-abudawud and the rest, all public
domain — are also where the hadith duʿās come from. They are not committed: they
are ~44 MB and the app needs only a dozen short passages. So
scripts/build-hadith-duas.mjs lifts each duʿā by collection, hadith number and
word span, and writes quran/duas-hadith-sources.json — the citation and a hash
of the extracted Arabic. A clean clone can therefore still prove the Arabic in
the app is the Arabic that was lifted, and with the editions present
scripts/verify-hadith-duas.mjs re-extracts and compares byte for byte.
Several duʿās a reader might expect are deliberately absent, each for a reason the sources gave rather than one we chose:
The one on undressing is included, and shows how the line is drawn. It is
Jāmiʿ al-Tirmidhī 606, where Tirmidhī writes that its chain is not strong — so
the card says exactly that. Its words are "Bismillāh", which the app already
carries for the start of a meal, so it is the one duʿā in the set marked
sameAs: a deliberate repeat, written down as a decision rather than slipping
past the duplicate check unnoticed.
[!IMPORTANT] The English is linked, not bundled. Every complete English Bukhārī in circulation is a modern work still in copyright — the one carried by every open dataset is Muhsin Khan's, published by Darussalam, whatever licence the dataset attaches to it. A repository cannot give away rights it never held.
So each hadith links out to its English on sunnah.com, by number. Linking is not copying: no licence to hold, nothing to evidence if a store asks, and the app never claims a translation as its own. It needs a connection; the Arabic does not. To bundle one properly — offline, beside the Arabic — the masjid needs written permission, and two letters ready to send are in
store/PERMISSION-LETTERS.md. A release check fails the build if a translation is ever bundled without that permission being recorded in the pack'sLICENCE.txt.
No commentary is included. Ibn Ḥajar's Fatḥ al-Bārī is available in an earlier source but keyed to its own 1–7,008 numbering. Carrying it across means matching the two editions by text, and they do not match: an exact full-text comparison aligns 22.7%, and positional windows align none at all. Attaching commentary on a fuzzy match would put the wrong scholar's words under the wrong hadith, so it waits for a source keyed to the standard numbering.
Four languages: English, Urdu, Gujarati and Arabic.
[!WARNING] Never edit
lang/ur.js,lang/gu.jsorlang/ar.js. They are generated. An edit there is overwritten by the next build and looks, in the diff, exactly like an edit that worked.
The source is lang/src/*.json, one file per area of the app, each entry a key
and its three translations:
"nikah.request_a_date": ["تاریخ کی درخواست", "તારીખની વિનંતી", "اطلب موعداً"]
# edit lang/src/*.json, then
node scripts/build-lang.mjs # regenerates the three packs
node scripts/check-i18n.mjs # every string on screen has a translation, and
# no translation exists for a string that is
# no longer on screen
check-i18n.mjs measures the packs against the app, not against each other.
An earlier version compared the packs to themselves and reported 100% coverage
while 406 strings were reaching the screen untranslated.
Each pack carries a content hash as its version. The app caches the pack it has so it works offline, fetches a fresh copy in the background on every launch, and — this part matters — uses the copy it just downloaded rather than reading it back out of storage. A phone that cannot store the pack (at its quota, in private mode, or evicted by iOS after a week unopened) still shows the right words for as long as it is open.
[!NOTE] The imam approved the translations that existed when he was asked. The 24 strings added for Imam's Advice have not been through a speaker of Urdu, Gujarati or Arabic. They are live, and they are the one part of the language packs nobody has checked.
No build step, no framework, no dependencies for the app itself — edit
index.html directly and commit. GitHub Pages deploys from main.
The compass, notifications and home-screen install all require HTTPS and will not work from a local file — test against the deployed URL.
worker/ is a small Cloudflare project with its own deploy step. Pushing any
change inside worker/ triggers the GitHub Action, so no local Node or
Wrangler is needed — edit and push from anywhere, including a tablet.
Performance principle: content that is small, permanent and core to daily use (prayer times, duʿās) is embedded so it works instantly and offline. Content that is large, growing or supplementary (Qur'an, language packs) lives in its own folder and is fetched only when opened. Architecture is matched to the content, not applied uniformly.
Seven SVGs, and the connectors animate — GitHub renders mermaid but cannot animate it, so these are built rather than inlined. The sources stay in the repository as text, so a diagram is still something you can edit and diff rather than a picture nobody can change:
assets/diagrams/*.mmd # the source, one file per diagram
node scripts/build-diagrams.mjs # regenerates the SVGs
The build script holds the palette and the animation in one place. It declares
the colours light-first and redefines them under prefers-color-scheme, so the
diagrams follow GitHub's theme, and it keeps a prefers-reduced-motion guard.
Rendering needs a Chromium. If the machine has one already:
PUPPETEER_EXECUTABLE_PATH=/path/to/chrome node scripts/build-diagrams.mjs
[!NOTE] Do not add a
font-familyoverride to that stylesheet. Mermaid measures every label and sizes each box before the stylesheet is appended, so changing the face afterwards reflows text inside boxes built for a different font and silently clips the last line. Mermaid embeds the face it measured with — leave it alone. ThewrappingWidthin the mermaid config is safe to change, because it is applied during measurement.
Live: the web app at taiyabahapp.ysbdesigns.uk,
installable on any phone, and the Android app on Google Play —
com.taiyabahmasjid.app, version 2.0.1, version code 8.
That Play app is now the native React Native build in native/, not the
Trusted Web Activity it started as; the TWA ended at version code 1. See
native/README.md. The web app remains live, maintained,
and the source of every word the native app says — npm run content in
native/ lifts the copy straight out of index.html, so the committee changes
wording in one place.
Still outstanding, roughly in the order it matters:
prayer_years holds 2026 only, 365 days,
published 15 September. A watchdog now nags weekly and fails from 1 November.
See The prayer timetable.taiyabahapp.ysbdesigns.uk today: the notification
landing URL, the timetable the Worker fetches, and the privacy policy URL given
to the stores. All three move together or one of them breaks.admin by the developer's account and should be
granted to the imam through the portal's own access screen.itikaf_is_open() and request_itikaf_place() in the masjid's own
Postgres. The form asks the first before it shows itself, so a missing
function means nobody is offered a form that posts into nowhere.
is the worked example.native/README.mdnative/src/supabase.js shipped a
publishable key the project answers 401 Invalid API key to, from the commit
that first wired the native app up until 8 October 2026. The website and the
portal always had the right one. So the installed app never authenticated to
the database — and nothing said so, because every server-backed path degrades
quietly: prayer times fell back to the bundled timetable (correct for 2026,
which is exactly why nobody saw it), notices came back empty, form
submissions failed, and no crash was ever reported. app_crashes sitting
empty had been read as "no crashes"; it meant "no connection".
native/scripts/check-keys.mjs now refuses a key that does not match the
website's, and runs first in both npm run check and the release workflowACTIVITY_RECOGNITION, which triggered Play's Health declaration, and
SYSTEM_ALERT_WINDOW — "draw over other apps" — from a development overlay.
The release workflow reads the permission list back out of the merged
manifest and fails if either returnsid="nk-email", getElementById returned the anchor, and nkValidate()
threw a TypeError on every press. Check 3z2 now refuses any duplicate id in
either page. That turned out to be the first of two faults — see
Built for Bolton Central Islamic Society by Yameen Bux.
Ownership is split, and the distinction matters:
Owned by Bolton Central Islamic Society (Taiyabah Masjid)
Owned by Yameen Bux — © 2026, all rights reserved
The charity holds a perpetual, irrevocable, royalty-free licence to use, host and operate this application for the purposes of the masjid and its community — including engaging others to maintain it on the charity's behalf. That licence covers use of this app; it does not transfer ownership of the code or design, and it grants no right to reuse either elsewhere.
No permission is granted to any other person or organisation to copy, reuse or redeploy this code or design — including for another masjid.
Third-party content is not covered by the above: quran/hadith/bukhari/
carries its own LICENCE.txt, and the 13-line mushaf names its own source and
licence in quran/mushaf/indopak13/index.json.
All rights reserved. Not open source.
| the sentinel history entry exists if and only if a layer is open. Its first version was decoration: the regex matched the call inside the comment used to disable it, so the negative control passed. It now requires a line that is nothing but the call |
| Holiday planner | the prose ("180 teaching days, 36 weeks") re-derived from the closure dates beside it |
| Nikāḥ requests | the form is shown only when the server confirms it can receive one, fails closed, and is never a dead end when closed |
| 13-line mushaf | a page pack that names no source and licence is treated as not installed, and the sūrah mapping is held to 114 entries in order, cross-checked against the juz table |
| Tab bar | recovers its place on the screen after the keyboard has been up |
| Zakat nisab | the metal price survives a provider going down, and the screen says so when it is standing on an older figure |
| Hall hire | whole-day booking, the masjid's own rate card, the deposit that holds the date, and the terms behind the checkbox — all held to what the website says |
| Nikāḥ fee | the published rate, payable online against a checked reference, and paying still does not book a date |
| Charity collections | all 18 fields sent, the BMCC certificate uploaded before the request and checked against the same 3 months the database enforces |
| Notification preferences | the five flags are in the order subscribers' already-stored values expect, and the app and Worker agree on every name. Reordering them would silently rewrite everybody's settings |
| Notices | the app reads the public view, the Worker writes through publish_notice() on a key with no table privileges of its own, and a poster reaches all three platforms |
| Android asset links | com.taiyabahmasjid.app verified against both signing certificates, and .nojekyll lets Pages serve them |
| Ṣaḥīḥ al-Bukhārī | the pack names its source and licence, numbers to 7,563 across 97 named books with every book file present, and the app refuses an unlicensed pack, browses by book, and prints the provenance on screen |
| Bundled translations | no English translation may appear in the hadith pack unless the pack's LICENCE.txt records permission for it — every complete English Bukhārī is in copyright, and a dataset claiming otherwise does not own it |
| Arabic normalisation | the app's search normaliser is run against the index the builder produced, on a real hadith from the pack — a character class written literally has been corrupted in transit three times, and the failure is silent: every query matches everything, or nothing |
| Element references | every tab pane is listed in switchTab() and nothing reaches for an id that is not there — the hadith reader and the hall booking once collided on four ids, and getElementById takes the first, which broke both screens at once |
| Reachability | every home tile is wired to something, and no panel exists that nothing can open — Ṣaḥīḥ al-Bukhārī shipped into a "Recite" screen the app had no route to, and a test that called the open function directly never noticed |
| Swipes | all gestures go through onSwipe and must beat the other axis, and no sheet scrolls sideways |
| Everything parses | index.html, admin.html, sw.js and the Worker |
request_hall_bookingfitrana.payLink is null and draws no button.register_for_course exists in Postgres and nothing
in index.html calls it. Both courses still say registration opens shortly and
send people to the office.fonts/ now holds the same 18 .woff2
subsets Google serves and fonts/fonts.css is Google's own stylesheet with
the URLs rewritten, so a browser picks the same file for the same characters
as before. The service worker keeps them, so the Arabic renders in Amiri in
the basement:root rather than
four more hand-picked hexes, which is how the sister repo ended up with four
different goldsindex.html names against a
list with a decision beside it, and fails on a host nobody has classified, on
a recipient the notice omits, and on a recipient the notice still claims
after the app stopped contacting itLICENSE.md, which four source-file headers had pointed at since the
first commit and which had never existedimam role in the
portal, an alert to the office, and the answer emailed back