Skip to content

plugins: add Prayer Times 1.4.0 - #204

Merged
neur0map merged 15 commits into
Ryoku-dev:mainfrom
mysweetpea:plugin/prayer-times
Nov 8, 2026
Merged

neur0map merged 15 commits into
Ryoku-dev:mainfrom
mysweetpea:plugin/prayer-times

Conversation

@mysweetpea

@mysweetpea mysweetpea commented Nov 8, 2026 •

Copy link
Copy Markdown
Contributor

What is this?

Prayer Times, a bar widget: Prayer times in the QS Bar: next prayer with a live countdown, a full-day panel with Umm al-Qura Hijri dates, Islamic holidays, a month t...

Checklist

  • One folder under its catalogue with a manifest and a real preview image (not a placeholder).
  • One entry added to that catalogue's registry.json (lastUpdated in YYYY-MM-DD; official: false for community items).
  • Tested live in Ryoku, it renders and behaves correctly (installed with ryoku plugin add).
  • tests/validate-catalogue.sh passes from the repo root.
  • I own or have permission to submit this content, and it is licensed for redistribution.
  • It does not overwrite user configuration or run privileged commands without clear consent.

Opened with ryoku plugin share; the product manifest and the registry entry were generated from the installed plugin.

- product-manifest no longer lists .oc-tmp/ drafts (they are not in the tree)
- registry entry gains upstream and a fresh manifestSha256
- summary/tagline no longer truncated
- screenshots use a neutral demo city, no personal location in fixtures or defaults
- clock24 defaults to false; tables show compact 5:11a times
- clicking the glyph opens the panel; pointer cursor on hover
- today's row in week/month is tinted, barred and bolded
- invalid latitude/longitude/offsets/Hijri shift are rejected rather than clamped,
  so a corrupt setting cannot push the widget off its defaults
- CI pins actions to commit SHAs and runs read-only
- no personal city left in defaults, tests or docs
- single metrics layer drives every dimension; week and month tables share it
- labels come from the service instead of copied arrays
- month header pinned above the scroller
- outlined chips, hover affordances, pointer cursors
- ring track, endpoint dot and a readable micro-label
- HOLIDAYS segment lists the next six Islamic holidays, not just the nearest
- table headers no longer collide (8.5px, 2px right padding)
- footer zone with divider and two-column meta row
- tappable holiday teaser with chevron and pointer cursor
- week separators in the month table
@mysweetpea mysweetpea changed the title plugins: add Prayer Times 1.0.2 plugins: add Prayer Times 1.3.0 Nov 8, 2026
@mysweetpea

Copy link
Copy Markdown
Contributor Author

Status on the two things visible in the timeline:

  • The red X on b6bd603 is the first commit (the initial scaffold export): the trusted validator rejected it for missing registry field upstream and for .oc-tmp/ scaffold files still declared in the manifest. Both were fixed in 2366d0e; every commit after that, including the current head d8bb860, is green.
  • tests/validate-catalogue.sh now passes from the repo root on the current head (catalogue OK: every bundle item, manifest, and installer resolves., exit 0), alongside python3 tests/validate-store.py --categories plugins and ryoku plugin validate (0 blocking, 0 warnings). Box ticked.

One tidy-up while I was in there: manifest.json files[] only listed the two preview images even though the service imports lib/{cities,hijri,praytimes}.js and ships bin/prayer-times, so those four are now declared, and the product manifest/registry hash were regenerated with tools/pack-product.py.

…theast Asia and Russia methods, nine more cities, precise ring
@neur0map

neur0map commented Nov 8, 2026 •

Copy link
Copy Markdown
Member

🦊 mika's review

Incremental review: new commits since the last checkpoint.

Summary

  • Add an offline prayer-times widget with countdown and calendar views.
  • Calculate prayer times, Hijri dates, holidays, and Qibla locally.
  • Provide a Node CLI, Waybar example, and calculation tests.
  • Register the community plugin and its configurable settings.

Estimated review effort: 3/5 · risk: high

📁 Walkthrough details

plugins/prayer-times/service/Main.qml

  • Coordinates location settings, prayer calculations, countdowns, and calendar data for the widget and panel.
    • Detects a starter city from the system timezone and validates settings.
    • Builds daily, weekly, monthly, and holiday views.
    • Updates the countdown each second while the panel is open.

plugins/prayer-times/lib/praytimes.js

  • Adds a shared solar calculation engine for QML and Node.
    • Solves prayer events using event-specific solar positions and nine calculation methods.
    • Supports high-latitude rules, Ramadan Isha intervals, and prayer offsets.
    • Calculates night periods and Qibla bearings.

plugins/prayer-times/lib/hijri.js

  • Adds table-based Umm al-Qura date conversion and Islamic holiday lookup.
    • Converts dates across 1343–1500 AH and validates calendar inputs.
    • Finds upcoming holidays and supports Hijri date adjustments.
    • Includes attribution for the upstream calendar table.

plugins/prayer-times/lib/cities.js

  • Defines 45 city presets with coordinates, timezones, and default calculation methods.
    • Resolves city IDs and estimates a starter city from an IANA timezone.
    • Marks unmatched timezones separately from matched estimates.

plugins/prayer-times/content/Widget.qml

  • Adds a bar widget showing the next prayer, time, and optional countdown.
    • Adapts its label to the available width and opens the panel on click.

plugins/prayer-times/content/Panel.qml

  • Adds today, week, month, and holiday views for prayer and calendar information.
    • Displays a countdown ring, prayer rows, night times, and Qibla bearing.
    • Highlights current dates and holidays in the schedule tables.

plugins/prayer-times/bin/prayer-times

  • Adds a Node CLI for prayer times on a preset city's clock.
    • Supports JSON, text, and Waybar output with date and method overrides.
    • Validates arguments and rejects unmapped automatic timezone selections.

plugins/prayer-times/manifest.json

  • Declares plugin entry points, packaged files, dependencies, and user settings.
    • Exposes location, calculation, offset, display, and Hijri adjustment options.

plugins/prayer-times/product-manifest.json

  • Defines installation paths, file modes, sizes, and checksums for the plugin package.
    • Includes the CLI, QML components, shared libraries, Waybar configuration, and tests.

plugins/registry.json

  • Registers Prayer Times 1.4.0 as a community bar plugin with preview metadata and upstream link.
    • Adds the product manifest reference and catalogue details.

plugins/prayer-times/tests/engine-test.js

  • Tests solar calculations, calendar conversion, holidays, location selection, and derived values.
    • Checks reference fixtures and independent solar crossings.
    • Covers equinox, polar, high-latitude, and Ramadan cases.
    • Round-trips every month in the embedded Hijri table.

plugins/prayer-times/tests/fixtures.json

  • Adds reference prayer-time fixtures across locations, methods, seasons, and polar conditions.
    • Supplies expected values for the engine tests.

plugins/prayer-times/tests/cli-test.js

  • Exercises CLI output formats, timezone handling, input validation, and calculation regressions.
    • Covers equinox and Ramadan behavior through the executable.

plugins/prayer-times/README.md

  • Documents installation, settings, calculation accuracy, CLI usage, privacy, and credits.
    • Describes panel views, timezone behavior, and high-latitude limitations.

plugins/prayer-times/contrib/waybar/config.jsonc

  • Provides a Waybar custom-module configuration using the CLI.
    • Refreshes the JSON output every 60 seconds.

Findings

Severity Category Fix effort File Issue
🟡 minor correctness low plugins/prayer-times/bin/prayer-times Bug — Inherited names such as constructor pass this check even though they are not calculation methods. Check for an own property so invalid CLI input cannot reach the engine as a method.
🟡 minor correctness medium plugins/prayer-times/bin/prayer-times Possible Bug — Waybar switches to tomorrow's event immediately after the final prayer, but its tooltip still shows today's date and today's times. After Isha, the highlighted text and tooltip therefore describe different schedules; inc…
🟡 minor correctness low plugins/prayer-times/content/Panel.qml Bug — If active becomes true before service is available, panelOpen remains false when the service arrives, so the open panel never receives second-precision updates. Synchronize it when service changes as well.
🟡 minor correctness low plugins/prayer-times/content/Panel.qml Bug — rowNext matches only the time, so two prayers with the same adjusted minute can both be highlighted. Match nextKey as rowNow already does.
🟡 minor correctness high plugins/prayer-times/content/Panel.qml Possible Bug — The new panel uses raw English labels and messages, including the tab names and twilight warning, so they remain untranslated when the shell runs in another locale. Route these strings and the service's user-visible labe…
🟡 minor correctness low plugins/prayer-times/lib/hijri.js Bug — toGregorian accepts numeric strings, but adding a string hd to the month start concatenates rather than adds, producing an invalid date. Reject nonnumeric inputs before calculating the index.
🟡 minor correctness low plugins/prayer-times/lib/praytimes.js Bug — tomorrowFajr is relative to tomorrow's midnight even when it is greater than today's Maghrib, so the conditional can produce a night that is a day too short. Always add 1440 minutes before subtracting today's Maghrib.

⚡ Quick fixes by effort

Low effort (apply from the diff):

  • plugins/prayer-times/lib/praytimes.js — Correct tomorrow's Fajr day offset
  • plugins/prayer-times/lib/hijri.js — Reject nonnumeric Hijri date components
  • plugins/prayer-times/content/Panel.qml — Synchronize panel state when service arrives
  • plugins/prayer-times/bin/prayer-times — Reject inherited method names
  • plugins/prayer-times/content/Panel.qml — Match the next prayer by key
  • plugins/prayer-times/lib/praytimes.js:162 — Bug — tomorrowFajr is relative to tomorrow's midnight even when it is greater than today's Maghrib, so the conditional can produce a night that is a day too short. Always add 1440 minutes before subtracting today's Maghrib.
  • plugins/prayer-times/lib/hijri.js:247 — Bug — toGregorian accepts numeric strings, but adding a string hd to the month start concatenates rather than adds, producing an invalid date. Reject nonnumeric inputs before calculating the index.
  • plugins/prayer-times/content/Panel.qml:44 — Bug — If active becomes true before service is available, panelOpen remains false when the service arrives, so the open panel never receives second-precision updates. Synchronize it when service changes as well.
  • plugins/prayer-times/bin/prayer-times:106 — Bug — Inherited names such as constructor pass this check even though they are not calculation methods. Check for an own property so invalid CLI input cannot reach the engine as a method.
  • plugins/prayer-times/content/Panel.qml:278 — Bug — rowNext matches only the time, so two prayers with the same adjusted minute can both be highlighted. Match nextKey as rowNow already does.

Medium effort (a function-level change):

  • plugins/prayer-times/bin/prayer-times:190 — Possible Bug — Waybar switches to tomorrow's event immediately after the final prayer, but its tooltip still shows today's date and today's times. After Isha, the highlighted text and tooltip therefore describe different schedules; inc…
**High effort** (design / multi-file — consider splitting)
  • plugins/prayer-times/content/Panel.qml:31 — Possible Bug — The new panel uses raw English labels and messages, including the tab names and twilight warning, so they remain untranslated when the shell runs in another locale. Route these strings and the service's user-visible labe…
🤖 Prompt for AI agents (click, copy, paste into Claude/Codex)
Fix the following issues found by mika-agent on Ryoku-dev/ryostore#204 (branch plugin/prayer-times):

- plugins/prayer-times/lib/praytimes.js:162-167 — Bug (minor severity, low effort): `tomorrowFajr` is relative to tomorrow's midnight even when it is greater than today's Maghrib, so the conditional can produce a night that is a day too short. Always add 1440 minutes before subtracting today's Maghrib.
  suggested fix:

function nightInfo(today, tomorrowFajr) {
if (!today || typeof today.maghrib !== "number" || !isFinite(today.maghrib)) return null;
var fajr = tomorrowFajr;
if (typeof fajr !== "number" || !isFinite(fajr)) fajr = today.fajr;
if (typeof fajr !== "number" || !isFinite(fajr)) return null;
var night = fajr + 1440 - today.maghrib;

- plugins/prayer-times/lib/hijri.js:247-249 — Bug (minor severity, low effort): `toGregorian` accepts numeric strings, but adding a string `hd` to the month start concatenates rather than adds, producing an invalid date. Reject nonnumeric inputs before calculating the index.
suggested fix:

function toGregorian(hy, hm, hd) {
if (typeof hy !== "number" || typeof hm !== "number" || typeof hd !== "number") return null;
if (hm < 1 || hm > 12 || hm % 1 !== 0 || hd % 1 !== 0 || hy % 1 !== 0) return null;
var i = (hy - FIRST_YEAR) * 12 + (hm - 1);

- plugins/prayer-times/content/Panel.qml:44-47 — Bug (minor severity, low effort): If `active` becomes true before `service` is available, `panelOpen` remains false when the service arrives, so the open panel never receives second-precision updates. Synchronize it when `service` changes as well.
suggested fix:

onActiveChanged: if (service) service.panelOpen = active
onServiceChanged: if (service) service.panelOpen = active
Component.onDestruction: if (service) service.panelOpen = false

implicitWidth: root.widthBudget

- plugins/prayer-times/bin/prayer-times:106-111 — Bug (minor severity, low effort): Inherited names such as `constructor` pass this check even though they are not calculation methods. Check for an own property so invalid CLI input cannot reach the engine as a method.
suggested fix:

} else if (arg === '--method') {
if (++i >= process.argv.length || process.argv[i].startsWith('-'))
fail('--method requires an id');
methodOverride = process.argv[i];
if (!Object.prototype.hasOwnProperty.call(PT.METHODS, methodOverride))
fail(unknown method: ${methodOverride});

- plugins/prayer-times/content/Panel.qml:278-283 — Bug (minor severity, low effort): `rowNext` matches only the time, so two prayers with the same adjusted minute can both be highlighted. Match `nextKey` as `rowNow` already does.
suggested fix:

readonly property bool rowNext: root.service !== null && !rowNow
&& root.service.nextKey === modelData.key
&& modelData.at !== null && modelData.at === root.service.nextAt
&& modelData.at >= root.service.now
readonly property bool rowPast: root.service !== null && modelData.at !== null
&& modelData.at < root.service.now && !rowNow

- plugins/prayer-times/content/Panel.qml:31-36 — Possible Bug (minor severity, high effort): The new panel uses raw English labels and messages, including the tab names and twilight warning, so they remain untranslated when the shell runs in another locale. Route these strings and the service's user-visible labe…
- plugins/prayer-times/bin/prayer-times:190-195 — Possible Bug (minor severity, medium effort): Waybar switches to tomorrow's event immediately after the final prayer, but its tooltip still shows today's date and today's times. After Isha, the highlighted text and tooltip therefore describe different schedules; inc…

For each: verify it is real in the current code, apply the smallest correct fix, and run the repo's tests. Skip anything already resolved and say why.
🛠 Deterministic checks run
  • linters: skipped (no checkout)

🚦 Merge recommendation: changes_required


🔍 mika-agent · self-hosted review for the Ryoku org · @mika-agent help for commands

@neur0map neur0map left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Changes requested — 1 critical and 2 major issue(s): see the inline comments and the walkthrough for fixes and an agent-ready prompt.

Comment on lines +1 to +5
name: validate

on:
push:
pull_request:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 major · Tests · fix: medium effort

GitHub Actions discovers workflows only in the repository-root .github/workflows directory, so this nested workflow never runs for this PR. Move or integrate these checks into a root workflow, with commands executed from the plugin directory.

Comment on lines +10 to +13
jobs:
engine:
runs-on: ubuntu-latest
steps:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 minor · Bug · fix: low effort

GitHub Actions only discovers workflows under the repository-root .github/workflows/ directory, so this nested workflow never runs. Move it to that directory and set its working directory to plugins/prayer-times so its relative paths resolve.

Suggested change

Suggested change
jobs:
engine:
runs-on: ubuntu-latest
steps:
jobs:
engine:
runs-on: ubuntu-latest
defaults:
run:
working-directory: plugins/prayer-times
steps:

Comment thread plugins/prayer-times/bin/prayer-times Outdated
Comment on lines +111 to +114
} else if (arg === '--city') {
if (++i >= process.argv.length)
fail('--city requires an id');
cityId = process.argv[i];

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 minor · Possible Bug · fix: low effort

--city auto is documented but passes the literal auto to byId, which rejects it as an unknown city. Treat that value as automatic detection, as the bare auto option already does.

Suggested change

Suggested change
} else if (arg === '--city') {
if (++i >= process.argv.length)
fail('--city requires an id');
cityId = process.argv[i];
cityId = process.argv[i] === 'auto' ? null : process.argv[i];

Comment thread plugins/prayer-times/bin/prayer-times Outdated
Comment on lines +124 to +126
const city = C.byId(cityId === null ? C.guess(Intl.DateTimeFormat().resolvedOptions().timeZone).city.id : cityId);
if (!city)
fail(`unknown city: ${cityId}`);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Suggestion (bug)

Handle unsupported timezones without crashing

Suggested change

Suggested change
const city = C.byId(cityId === null ? C.guess(Intl.DateTimeFormat().resolvedOptions().timeZone).city.id : cityId);
if (!city)
fail(`unknown city: ${cityId}`);
const guessed = cityId === null ? C.guess(Intl.DateTimeFormat().resolvedOptions().timeZone) : null;
const city = cityId === null ? (guessed && guessed.city) : C.byId(cityId);
if (!city)
fail(cityId === null ? 'no preset city for the system timezone; specify --city' : `unknown city: ${cityId}`);

To apply: click the sparkletip, or ask @mika-agent ask <question>.

Comment on lines +127 to +129

const now = zoneNow(city.tz);
const date = parseDate(dateValue || now.iso);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 minor · Bug · fix: low effort

C.guess() can return no city for an unsupported timezone, so dereferencing .city.id crashes instead of reporting the problem. Check the guessed city before using it.

Suggested change

Suggested change
const now = zoneNow(city.tz);
const date = parseDate(dateValue || now.iso);
const guessed = cityId === null ? C.guess(Intl.DateTimeFormat().resolvedOptions().timeZone) : null;
const city = cityId === null ? (guessed && guessed.city) : C.byId(cityId);
if (!city)
fail(cityId === null ? 'no preset city for the system timezone; specify --city' : `unknown city: ${cityId}`);

Comment on lines +70 to +76
}

function ids() {
var out = [];
for (var i = 0; i < CITIES.length; i++) out.push(CITIES[i].id);
return out;
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 minor · Bug · fix: low effort

A timezone does not identify a city: America/New_York covers locations far from Baltimore, yet this marks Baltimore as exact and suppresses the estimated-location warning. Mark timezone guesses as estimated so users are prompted to select their actual city.

Suggested change

Suggested change
}
function ids() {
var out = [];
for (var i = 0; i < CITIES.length; i++) out.push(CITIES[i].id);
return out;
}
function guess(tz) {
if (tz) {
for (var i = 0; i < CITIES.length; i++)
if (CITIES[i].tz === tz) return { city: CITIES[i], exact: false };
}
return { city: byId(FALLBACK), exact: false };
}

Comment on lines +201 to +204
function toHijri(y, m, d, adjust) {
var idx = dayIndex(y, m, d) + (adjust || 0);
if (idx < MONTH_STARTS[0] || idx >= MONTH_STARTS[MONTH_STARTS.length - 1] + 30) return null;
var lo = 0, hi = MONTH_STARTS.length - 1;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 minor · Bug · fix: low effort

The final table entry is the boundary after 1500 AH, not the start of another supported month. Treating it as a month returns fabricated dates in 1501 AH instead of reporting that the table is exhausted.

Suggested change

Suggested change
function toHijri(y, m, d, adjust) {
var idx = dayIndex(y, m, d) + (adjust || 0);
if (idx < MONTH_STARTS[0] || idx >= MONTH_STARTS[MONTH_STARTS.length - 1] + 30) return null;
var lo = 0, hi = MONTH_STARTS.length - 1;
function toHijri(y, m, d, adjust) {
var idx = dayIndex(y, m, d) + (adjust || 0);
if (idx < MONTH_STARTS[0] || idx >= MONTH_STARTS[MONTH_STARTS.length - 1]) return null;
var lo = 0, hi = MONTH_STARTS.length - 1;

Comment on lines +213 to +217
function toGregorian(hy, hm, hd) {
var i = (hy - FIRST_YEAR) * 12 + (hm - 1);
if (i < 0 || i >= MONTH_STARTS.length) return null;
return fromIndex(MONTH_STARTS[i] + hd - 1);
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 minor · Bug · fix: low effort

toGregorian accepts invalid months and days, silently converting them into dates in adjacent months; it also accepts the terminal boundary as a month. Validate the components against the next month start before converting.

Suggested change

Suggested change
function toGregorian(hy, hm, hd) {
var i = (hy - FIRST_YEAR) * 12 + (hm - 1);
if (i < 0 || i >= MONTH_STARTS.length) return null;
return fromIndex(MONTH_STARTS[i] + hd - 1);
}
function toGregorian(hy, hm, hd) {
if (hm < 1 || hm > 12 || hm % 1 !== 0 || hd % 1 !== 0) return null;
var i = (hy - FIRST_YEAR) * 12 + (hm - 1);
if (i < 0 || i >= MONTH_STARTS.length - 1 || i % 1 !== 0) return null;
if (hd < 1 || hd > MONTH_STARTS[i + 1] - MONTH_STARTS[i]) return null;
return fromIndex(MONTH_STARTS[i] + hd - 1);
}

Comment on lines +41 to +50
var g = (357.529 + 0.98560028 * D) % 360;
var q = (280.459 + 0.98564736 * D) % 360;
var L = (q + 1.915 * Math.sin(g * DEG) + 0.020 * Math.sin(2 * g * DEG)) % 360;
var e = 23.439 - 0.00000036 * D;
var ra = Math.atan2(Math.cos(e * DEG) * Math.sin(L * DEG), Math.cos(L * DEG)) / DEG / 15;
var decl = Math.asin(Math.sin(e * DEG) * Math.sin(L * DEG)) / DEG;
return { decl: decl, eqt: q / 15 - ((ra + 24) % 24) };
}

// Hours from solar noon to the given depression angle. null when unsolvable.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 critical · Critical bug · fix: low effort

q wraps at 360 degrees while right ascension wraps at a slightly different point, so their difference can be nearly 24 hours around the March equinox. Normalize the equation of time to prevent prayer times from being assigned to the wrong day.

Suggested change

Suggested change
var g = (357.529 + 0.98560028 * D) % 360;
var q = (280.459 + 0.98564736 * D) % 360;
var L = (q + 1.915 * Math.sin(g * DEG) + 0.020 * Math.sin(2 * g * DEG)) % 360;
var e = 23.439 - 0.00000036 * D;
var ra = Math.atan2(Math.cos(e * DEG) * Math.sin(L * DEG), Math.cos(L * DEG)) / DEG / 15;
var decl = Math.asin(Math.sin(e * DEG) * Math.sin(L * DEG)) / DEG;
return { decl: decl, eqt: q / 15 - ((ra + 24) % 24) };
}
// Hours from solar noon to the given depression angle. null when unsolvable.
function sunPosition(jd) {
var D = jd - 2451545.0;
var g = (357.529 + 0.98560028 * D) % 360;
var q = (280.459 + 0.98564736 * D) % 360;
var L = (q + 1.915 * Math.sin(g * DEG) + 0.020 * Math.sin(2 * g * DEG)) % 360;
var e = 23.439 - 0.00000036 * D;
var ra = Math.atan2(Math.cos(e * DEG) * Math.sin(L * DEG), Math.cos(L * DEG)) / DEG / 15;
var decl = Math.asin(Math.sin(e * DEG) * Math.sin(L * DEG)) / DEG;
var eqt = q / 15 - ((ra + 24) % 24);
eqt = ((eqt + 12) % 24 + 24) % 24 - 12;
return { decl: decl, eqt: eqt };
}

Comment on lines +43 to +45
readonly property var prayerKeys: ["fajr", "sunrise", "dhuhr", "asr", "maghrib", "isha"]
readonly property var prayerLabels: ["Fajr", "Sunrise", "Dhuhr", "Asr", "Maghrib", "Isha"]
readonly property var prayerShortLabels: ["FAJR", "SUNRISE", "DHUHR", "ASR", "MAGHRIB", "ISHA"]

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 minor · Possible Bug · fix: high effort

The service and panel add English-only prayer names, dates, status messages and navigation labels, so translated shell sessions still display these strings in English. Route the user-visible strings through the i18n catalog.

@mysweetpea

Copy link
Copy Markdown
Contributor Author

Pushed 1.4.0 over the top of this branch — a deep accuracy + polish pass rather than more features.

Accuracy (the real find). The engine sampled the sun once at local midnight, like PrayTimes does, which leaves the horizon events up to two minutes late: at the reported sunset the sun was already 0.23° past the angle. Every event is now solved with the sun where it actually is at that moment. Measured against an independent ephemeris, every Fajr/Sunrise/Maghrib/Isha now lands within 45 seconds of the true crossing (it was ~2 minutes). Moscow 7 Nov sunset went 16:40 → 16:38, Stockholm 15:42 → 15:40, matching published times.

Methods and places. Added seasia (Kemenag, JAKIM and MUIS all use 20/18 — verified minute-exact against AlAdhan methods 20/17/11) and russia (16/15), and pointed Jakarta/Kuala Lumpur/Singapore at their national convention instead of MWL. Added nine presets for zones that previously fell back to London: Denver, Phoenix, Vancouver, Mexico City, Buenos Aires, Dublin, Rome, Moscow, Auckland.

Honesty. A timezone no preset covers now says "set your city" instead of silently showing London's times, a date with no twilight says so instead of printing six dashes, and a preset city in another timezone is labelled "shown on your clock".

Panel. The ring carries a second-precision countdown (mono, no jitter), gets a soft halo and an animated sweep, the Qibla line points an arrow at the bearing, tabs cross-fade and the panel height glides, holiday rows get hairlines. The service now rebuilds heavy state only on a day/settings change and ticks cheaply (1s while the panel is open, 30s collapsed).

Tests. 251 assertions: the sky check above, 23 AlAdhan fixtures (adding Jakarta/Moscow/Denver/Auckland and a Stockholm-midsummer high-latitude rule case), and a full walk of all 1,896 months of the Umm al-Qura table. tests/validate-catalogue.sh and ryoku plugin validate are both clean on this head.

@mysweetpea mysweetpea changed the title plugins: add Prayer Times 1.3.0 plugins: add Prayer Times 1.4.0 Nov 8, 2026
…i input, stop treating a timezone as a city

- eqt folded into -12..12h: q and right ascension wrap a day apart around the
  March equinox, which shifted every event by 24h for ~2 days a year
- toHijri/toGregorian reject out-of-range months, days and the table edge instead
  of rolling them into adjacent months
- guess() never reports a timezone hit as an exact city; the zone's namesake wins
- CLI: --city auto means detection; no crash when the zone has no preset
- drop the nested .github workflow: GitHub only reads the repo root, so it was
  inert in the store (it stays in the author repo, where it runs)

@neur0map neur0map left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Changes requested — 0 critical and 2 major issue(s): see the inline comments and the walkthrough for fixes and an agent-ready prompt.

Comment on lines +30 to +35
readonly property var tabs: [
{ value: "today", label: "TODAY" },
{ value: "week", label: "WEEK" },
{ value: "month", label: "MONTH" },
{ value: "holidays", label: "HOLIDAYS" }
]

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 minor · Possible Bug · fix: high effort

The new panel and its service expose raw English strings, including tab labels, warnings, prayer names and holiday descriptions. Users running a translated shell will still see these strings in English; route them through the i18n catalog.

Comment on lines +42 to +43
// ticks once a second instead of once a minute
onActiveChanged: if (service) service.panelOpen = active

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 minor · Possible bug · fix: low effort

This snippet is too short for a valid suggestion.

confidence: 60/100

Suggested change

Suggested change
// ticks once a second instead of once a minute
onActiveChanged: if (service) service.panelOpen = active
onActiveChanged: if (service) service.panelOpen = active
Component.onDestruction: if (service) service.panelOpen = false

Comment thread plugins/prayer-times/lib/praytimes.js Outdated
Comment on lines +127 to +128
// Interval-based Isha (Umm al-Qura): 90 minutes after Maghrib.
if (ishaInterval && out.maghrib !== null) out.isha = out.maghrib + ishaInterval / 60;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 major · Possible Bug · fix: medium effort

Umm al-Qura Isha is fixed at 90 minutes after Maghrib for every day, including Ramadan, when the convention uses 120 minutes. Users selecting Mecca or Medina therefore receive Isha times 30 minutes early throughout Ramadan; choose the interval using the applicable Hijri date.

Comment on lines +33 to +38
"source": "LICENSE",
"destination": "LICENSE",
"sha256": "3a7857c238a5754b5380348616d3d187aff6c2517e23bbf301b0a67581c445f8",
"mode": "0644",
"size": 1066,
"install": false

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 minor · Data Integrity · fix: medium effort

The product manifest excludes LICENSE from installation even though the plugin distributes MIT-licensed code and embeds the MIT-licensed Umm al-Qura table. Installed copies therefore lack the required license notice; install the license and include the upstream table's copyright and permission notice.

Comment thread plugins/prayer-times/service/Main.qml Outdated
Comment on lines +292 to +293
svc.twilightMissing = todayTimes !== null && (todayTimes.fajr === null || todayTimes.maghrib === null)
&& todayTimes.dhuhr !== null;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 minor · Possible Bug · fix: low effort

twilightMissing checks Fajr and Maghrib but not Isha. When Fajr is available and Isha is not, such as near a seasonal twilight boundary, the panel shows --:-- for Isha without the promised explanation; include Isha in the missing-twilight check.

…wording

- bar glyph, holiday chevron and qibla marker use PluginKit GlyphIcon / drawn
  shapes instead of Unicode characters
- chips share one width and 28px targets; ring gets 100px of room
- metadata line separated by a hairline; footer halves get spacing
- hint distinguishes 'auto-detected' from 'no preset for this timezone'

@neur0map neur0map left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 mika reviewed this change; details in the walkthrough comment.

Comment thread plugins/prayer-times/bin/prayer-times Outdated
Comment on lines +124 to +127
const guessed = cityId === null ? C.guess(Intl.DateTimeFormat().resolvedOptions().timeZone) : null;
const city = cityId === null ? (guessed && guessed.city) : C.byId(cityId);
if (!city)
fail(cityId === null ? 'no preset city for this timezone; pass --city <id> (see --cities)'

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 minor · Possible Bug · fix: low effort

C.guess() returns London even when matched is false, so running the CLI without --city in an unsupported timezone such as Asia/Kathmandu silently prints London prayer times instead of the advertised error. Reject an unmatched guess before selecting its fallback city.

"custom/prayer": {
"interval": 60,
"return-type": "json",
"exec": "node ~/.local/share/ryoku/plugins/prayer-times/bin/prayer-times --waybar"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 minor · Possible Bug · fix: medium effort

The Waybar command invokes Node, but the manifest declares only readlink as a command dependency. On an installed system without Node, the documented module exits without producing JSON; declare the dependency or provide a runtime that is available on installed targets.

…lback

- auto timezone no longer throws away a three-segment zone match
- toHijri/toGregorian reject NaN, month 13, day 32, two-digit years and a
  non-numeric adjust before Date.UTC can normalise them
- fmt and nightInfo reject non-finite input instead of printing NaN
- the angle rule does not invent an isha when the method has no isha angle
- CLI: value flags reject a following flag, exact-minute prayers count as next,
  out-of-day high-latitude values are skipped, usage lists every flag
- tests/cli-test.js: 17 assertions over text/json/waybar, DST-free date math,
  the equinox date end to end, auto-detection and argument errors

@neur0map neur0map left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Changes requested — 0 critical and 1 major issue(s): see the inline comments and the walkthrough for fixes and an agent-ready prompt.

Comment on lines +61 to +65
const p = zoneParts(timeZone, new Date(utcNoon));
return (Date.UTC(p.y, p.m - 1, p.d, p.hh, p.mm) - utcNoon) / 3600000;
}

function isoDate(date) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 minor · Bug · fix: low effort

zoneParts() can return hour 24 for midnight, but it normalizes that to 0 without adjusting the date. For zones at UTC+12, this makes zoneOffsetHours() return -12 instead of +12, shifting every CLI prayer time by a day. Request an h23 hour cycle so midnight is formatted as 00.

Suggested change

Suggested change
const p = zoneParts(timeZone, new Date(utcNoon));
return (Date.UTC(p.y, p.m - 1, p.d, p.hh, p.mm) - utcNoon) / 3600000;
}
function isoDate(date) {
function zoneOffsetHours(timeZone, y, m, d) {
const utcNoon = Date.UTC(y, m - 1, d, 12);
const parts = new Intl.DateTimeFormat('en-US', {
timeZone, hourCycle: 'h23',
year: 'numeric', month: '2-digit', day: '2-digit',
hour: '2-digit', minute: '2-digit'
}).formatToParts(new Date(utcNoon));
const get = (type) => Number(parts.find((p) => p.type === type).value);
return (Date.UTC(get('year'), get('month') - 1, get('day'), get('hour'), get('minute')) - utcNoon) / 3600000;
}

Comment on lines +194 to +196
function dayIndex(y, m, d) {
return Math.floor(Date.UTC(y, m - 1, d) / 86400000) + RJD_EPOCH;
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Suggestion (bug)

Reject normalized invalid Gregorian dates

Suggested change

Suggested change
function dayIndex(y, m, d) {
return Math.floor(Date.UTC(y, m - 1, d) / 86400000) + RJD_EPOCH;
}
function dayIndex(y, m, d) {
var t = new Date(Date.UTC(y, m - 1, d));
if (t.getUTCFullYear() !== y || t.getUTCMonth() + 1 !== m || t.getUTCDate() !== d) return NaN;
return Math.floor(t.getTime() / 86400000) + RJD_EPOCH;
}

To apply: click the sparkletip, or ask @mika-agent ask <question>.

Comment thread plugins/prayer-times/lib/praytimes.js Outdated
Comment on lines +131 to +136
var rule = opts.highLat || "none";
if (rule !== "none" && out.sunrise !== null && out.maghrib !== null) {
var night = 24 - (out.maghrib - out.sunrise); // hours between sunset and next sunrise
if (out.fajr === null) {
if (rule === "middle") out.fajr = out.sunrise - night / 2;
else if (rule === "seventh") out.fajr = out.sunrise - night / 7;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 minor · Bug · fix: low effort

High-latitude rules must also cap a computed Fajr that falls earlier than the permitted night portion, not just replace an unsolvable angle. Otherwise, selecting a rule can leave an excessively early Fajr unchanged.

Suggested change

Suggested change
var rule = opts.highLat || "none";
if (rule !== "none" && out.sunrise !== null && out.maghrib !== null) {
var night = 24 - (out.maghrib - out.sunrise); // hours between sunset and next sunrise
if (out.fajr === null) {
if (rule === "middle") out.fajr = out.sunrise - night / 2;
else if (rule === "seventh") out.fajr = out.sunrise - night / 7;
var fajrPortion = rule === "middle" ? night / 2 :
rule === "seventh" ? night / 7 :
rule === "angle" && typeof fajrAngle === "number" ? night * (fajrAngle / 60) : null;
if (fajrPortion !== null && (out.fajr === null || out.fajr < out.sunrise - fajrPortion))
out.fajr = out.sunrise - fajrPortion;

Comment thread plugins/prayer-times/lib/praytimes.js Outdated
Comment on lines +137 to +142
else if (rule === "angle" && typeof fajrAngle === "number")
out.fajr = out.sunrise - night * (fajrAngle / 60);
}
if (out.isha === null) {
if (rule === "middle") out.isha = out.maghrib + night / 2;
else if (rule === "seventh") out.isha = out.maghrib + night / 7;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 minor · Bug · fix: low effort

A solvable Isha can still be later than the selected high-latitude limit. Cap angle-based Isha at that limit while preserving the fixed Umm al-Qura interval.

Suggested change

Suggested change
else if (rule === "angle" && typeof fajrAngle === "number")
out.fajr = out.sunrise - night * (fajrAngle / 60);
}
if (out.isha === null) {
if (rule === "middle") out.isha = out.maghrib + night / 2;
else if (rule === "seventh") out.isha = out.maghrib + night / 7;
var ishaPortion = rule === "middle" ? night / 2 :
rule === "seventh" ? night / 7 :
rule === "angle" && typeof ishaAngle === "number" ? night * (ishaAngle / 60) : null;
if (!ishaInterval && ishaPortion !== null && (out.isha === null || out.isha > out.maghrib + ishaPortion))
out.isha = out.maghrib + ishaPortion;

Comment thread plugins/prayer-times/service/Main.qml Outdated
Comment on lines +85 to +89
var parts = clean.split("/");
var zone = clean;
if (parts.length >= 2)
zone = parts[parts.length - 2] + "/" + parts[parts.length - 1];
detectedZone = zone;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 minor · Bug · fix: low effort

Truncating every zone to two components turns America/Indiana/Indianapolis into Indiana/Indianapolis, breaking matching and mismatch detection for multi-component IANA zones. Strip the zoneinfo path prefix instead, preserving the full zone name.

Suggested change

Suggested change
var parts = clean.split("/");
var zone = clean;
if (parts.length >= 2)
zone = parts[parts.length - 2] + "/" + parts[parts.length - 1];
detectedZone = zone;
var clean = (tz || "").trim();
var marker = "/zoneinfo/";
var markerAt = clean.indexOf(marker);
if (markerAt !== -1)
clean = clean.substring(markerAt + marker.length);
var zone = clean;

Comment thread plugins/prayer-times/tests/cli-test.js Outdated
Comment on lines +50 to +51
var unknown = JSON.parse(run(['--json', 'auto'], { TZ: 'Asia/Kathmandu' }).out);
ok(unknown.city === 'london', 'an unmapped zone falls back visibly, got ' + unknown.city);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 major · Tests · fix: low effort

The unmapped-zone test expects London, but the CLI accepts guessed.city without checking guessed.matched, so it currently does return London; this also contradicts the documented promise that an unmapped timezone is rejected. The test should require a nonzero exit and the CLI should check matched before using the fallback.

Comment on lines +73 to +76
var diff = Math.abs(have - want);
if (diff > 720) diff = 1440 - diff; // midnight wrap
var tol = k === "asr" ? 2 : 1;
ok(diff <= tol, f.name + " " + k + " got " + fmt(have) + " want " + f.expect[k]);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Suggestion (bug)

Fix wrapped fixture differences

Suggested change

Suggested change
var diff = Math.abs(have - want);
if (diff > 720) diff = 1440 - diff; // midnight wrap
var tol = k === "asr" ? 2 : 1;
ok(diff <= tol, f.name + " " + k + " got " + fmt(have) + " want " + f.expect[k]);
var diff = Math.abs(have - want) % 1440;
if (diff > 720) diff = 1440 - diff; // midnight wrap
var tol = k === "asr" ? 2 : 1;
ok(diff <= tol, f.name + " " + k + " got " + fmt(have) + " want " + f.expect[k]);

To apply: click the sparkletip, or ask @mika-agent ask <question>.

Comment on lines +77 to +80
});
});

// ---------------------------------------------------------------- 2. sky checks

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 minor · Bug · fix: low effort

When the difference exceeds 1440 minutes, 1440 - diff becomes negative and the assertion passes even for arbitrarily wrong results. Normalize the difference before calculating the circular distance.

Suggested change

Suggested change
});
});
// ---------------------------------------------------------------- 2. sky checks
var diff = Math.abs(have - want) % 1440;
if (diff > 720) diff = 1440 - diff; // midnight wrap
var tol = k === "asr" ? 2 : 1;
ok(diff <= tol, f.name + " " + k + " got " + fmt(have) + " want " + f.expect[k]);

@mysweetpea

Copy link
Copy Markdown
Contributor Author

Thanks — the equation-of-time finding was real and serious, and the rest were worth taking. Point by point:

🔴 eqt wrap (critical) — confirmed, fixed. On 2026‑03‑21 the raw value is 23.88h where it should be -0.12h: q wraps at 360 while right ascension wraps a day earlier, so for ~2 days around the March equinox every event came back 24h out (Fajr 356 min, Dhuhr −646 min). Folded into -12..12 exactly as you suggested. Regression coverage, because a 24h slip still looks plausible once a clock wraps it:

  • raw-value invariant — no negative minutes and fajr < sunrise < dhuhr < asr < maghrib < isha across 14 days straddling both equinoxes at three latitudes;
  • the sky check now includes 2026‑03‑21 (every event within 45s of the true angle crossing);
  • two AlAdhan fixtures for 21–22 March.

🟠 timezone ≠ city (major) — taken. guess() now returns exact: false on every path, and the zone's namesake wins (America/New_York → New York, not Baltimore). The UI distinguishes the cases: auto users get "Auto-detected · set your city in Settings", an unmapped zone gets "No preset for this timezone · set your city", and a manually chosen city in another zone gets "Shown on your clock" (its times are converted onto the local clock, so the countdown stays correct). This also fixed a self-inflicted bug: the fallback re-guess discarded three-segment zones (America/Argentina/Buenos_Aires), and it now branches on matched.

🟠 nested workflow (major) — removed from the submission. You're right that GitHub only reads the repo root, so it never ran here. It lives at the root of the author repo (mysweetpea/prayer-times) where it does run, and the file carries a header saying it is stripped from the store. The store's own screen + catalogue validation cover this PR.

🟡 Hijri terminal boundary — checked, not a fabrication. MONTH_STARTS has no terminal row: the last entry starts 1500‑12 and the table covers its 30 days (2077‑10‑18 .. 2077‑11‑16), so >= last + 30 was correct rather than producing 1501. I kept the behaviour, named it LAST_MONTH_DAYS with a comment, and pinned it with tests: day 30 valid, day 31 null, toHijri(2077‑11‑17) null.

🟡 toGregorian validation — taken. Months 1–12, integer components, day within the month (the final month uses LAST_MONTH_DAYS), so 1500‑12‑40 no longer rolls into the next month.

🟡 CLI --city auto — fixed, and every value flag now rejects a following flag, so --city --json reports a missing value instead of unknown city: --json.

🟡 C.guess() null deref — made safe. guess() always returns a city (it falls back to London) so the crash was unreachable, but the CLI now checks matched and says which city it fell back to; the silent-London case is visible.

🟡 i18n — blocked upstream, not by choice. Plugins may only import QtQuick*, Quickshell*, Ryoku.PluginKit, Ryoku.PluginKit.Singletons and their own files (R4); I18n lives in Ryoku.Ui.Singletons, which plugins must not touch, and PluginKit.Singletons exports Theme/Scheme/Config/Motion only. The moment PluginKit exposes a tr() hook I'll route the labels through it — that's a one-line change on our side.

Also from an automated review pass on the same code: toHijri/toGregorian now reject NaN, month 13, day 32 and two-digit years before Date.UTC can normalise them; fmt/nightInfo reject non-finite input instead of printing NaN:NaN; the angle rule no longer invents an Isha when the method has no Isha angle; and there is a new tests/cli-test.js (17 assertions: text/json/waybar shapes, the equinox date end to end, auto-detection, argument errors) so the entry point is no longer untested.

Current head 0299cbd: screen green, tests/validate-catalogue.sh and validate-store.py clean, engine 391/391, CLI 17/17, ryoku plugin validate 0 blocking / 0 warnings.

- Umm al-Qura isha 120 min in Ramadan (90 otherwise), wired from the Hijri date
- high-latitude rules cap a solvable Fajr/Isha outside the night portion
- America/Indiana/Indianapolis no longer truncated to Indiana/Indianapolis
- --city auto exits with the zone name instead of printing London
- h23 hour cycle, validated dayIndex, Isha in the twilight check
- fixture comparator wraps before mirroring
- manifest: commands + dependencies.commands (node, readlink)
- drop AGENTS.md/CLAUDE.md/.gitignore from the shipped product
@mysweetpea

Copy link
Copy Markdown
Contributor Author

Second pass taken; two of these were real bugs in code I wrote this session.

🟠 Umm al-Qura Isha in Ramadan (major) — real, fixed. The interval now lives in the method (ishaInterval: 90, ramadanInterval: 120) and both callers pass ramadan from the Hijri date they already compute for the header — so no date logic is duplicated in the engine. Tests: Mecca on a Ramadan day is maghrib + 120, an ordinary day stays +90, and only Isha moves.

🟡 High-latitude rules (2 findings) — real, taken. The rules previously only filled a missing angle. They now also pull back a solvable value that falls outside the night portion the rule permits (adhan-js does the same), with interval methods left alone so Umm al-Qura keeps its fixed distance. Tested at Stockholm midsummer under middle: Fajr never earlier than sunrise − night/2, Isha never later than maghrib + night/2, and the caps never invert the day.

🟡 America/Indiana/Indianapolis truncated — real, fixed. I was keeping the last two path components, which is exactly what turned a three-part zone into Indiana/Indianapolis. Now I strip a zoneinfo/ prefix instead (/etc/localtime symlinks through it, which is why the prefix shows up at all) and match on the full zone name. The truncated fallback is gone.

🟡 --city auto printing London — real, fixed. auto now exits 1 with no city preset for timezone <zone> — pass --city <id> instead of quietly answering for another city. The CLI test asserts the failure rather than the fallback.

🟡 hourCycle: 'h23' — taken. ICU here happens to render 00 for hour12: false, so I could not reproduce it locally, but the option is now explicit rather than left to the platform's ICU version. Pinned with an Auckland July date (UTC+12 puts local midnight at UTC noon).

🟢 dayIndex normalisation — taken, same shape you suggested; toHijri also validates components before it reaches the helper.

🟢 fixture comparator — real. Math.abs(have − want) was unbounded, so a 24 h error produced 1440 − 1452 = −12 and passed as "within 1 minute". Now % 1440 first, then mirror.

🟢 Waybar/Node dependency — declared. Added commands: ["bin/prayer-times"] and dependencies.commands: ["node", "readlink"]. Checked that an unavailable entry does not block ryoku plugin add (it installs anyway), so this documents the requirement rather than gating the widget on a Node install it never uses. Also dropped AGENTS.md, CLAUDE.md and .gitignore from the shipped product — scaffold files nobody should receive.

🟡 LICENSE not installed — that is the packer's policy, not the product's. tools/pack-product.py holds documentation back for the plugins category (DOCS_SHIPPED_CATEGORIES lists barstyles/launchers/docks/osds/notifications/sidebars, not plugins), so README and LICENSE get install: false for every plugin in the store, this one included. The licence travels in manifest.license (MIT) and the README. Making plugins install docs is a one-line change in that tool, upstream of any submission. The Umm al-Qura table has no third-party copyright — it is the calendar's published month starts, reconstructed and verified month by month in tests/engine-test.js.

🟡 twilightMissing without Isha — taken, Isha is part of the check now.

i18n — unchanged from last round: R4 forbids importing Ryoku.Ui.Singletons, where I18n lives, and PluginKit.Singletons exposes Theme/Scheme/Config/Motion only. Give PluginKit a tr() and the labels route through it in one line.

Head c78bfe6: screen green, catalogue + store validators clean, engine 397/397, CLI 19/19, ryoku plugin validate 0 blocking / 0 warnings.

@neur0map neur0map left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Changes requested — 0 critical and 2 major issue(s): see the inline comments and the walkthrough for fixes and an agent-ready prompt.

@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2026 Sweet Pea

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 major · Data Integrity · fix: low effort

The embedded Hijri table is attributed to the MIT-licensed dralshehri/hijri-converter, but this license contains only Sweet Pea's copyright notice. Redistributing the table requires retaining the upstream copyright and permission notice; include the upstream license with the installed distribution.

Comment thread plugins/prayer-times/bin/prayer-times Outdated
Comment on lines +141 to +145
function calculate(day) {
const hijri = H.toHijri(day.getFullYear(), day.getMonth() + 1, day.getDate(), 0);
return PT.times({
// Umm al-Qura holds Isha 120 minutes in Ramadan instead of 90
ramadan: !!hijri && hijri.month === 9,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 minor · Bug · fix: low effort

toHijri returns the month as m, as used by the JSON output below. Checking month always disables the Ramadan Isha interval in the CLI.

Suggested change

Suggested change
function calculate(day) {
const hijri = H.toHijri(day.getFullYear(), day.getMonth() + 1, day.getDate(), 0);
return PT.times({
// Umm al-Qura holds Isha 120 minutes in Ramadan instead of 90
ramadan: !!hijri && hijri.month === 9,
function calculate(day) {
const hijri = H.toHijri(day.getFullYear(), day.getMonth() + 1, day.getDate(), 0);
return PT.times({
// Umm al-Qura holds Isha 120 minutes in Ramadan instead of 90
ramadan: !!hijri && hijri.m === 9,

Comment on lines +278 to +281
required property var modelData
readonly property bool rowNow: root.service !== null && root.service.isNow
&& root.service.nextKey === modelData.key
&& modelData.at === root.service.nextAt

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 minor · Possible bug · fix: low effort

rowNext matches only the time, so after the last prayer it can highlight today's Fajr as tomorrow's next Fajr when their times coincide. Require the row to be at or after the current time.

Suggested change

Suggested change
required property var modelData
readonly property bool rowNow: root.service !== null && root.service.isNow
&& root.service.nextKey === modelData.key
&& modelData.at === root.service.nextAt
readonly property bool rowNext: root.service !== null && !rowNow
&& modelData.at !== null && modelData.at === root.service.nextAt
&& modelData.at >= root.service.now
readonly property bool rowPast: root.service !== null && modelData.at !== null
&& modelData.at < root.service.now && !rowNow

Comment on lines +282 to +285
readonly property bool rowNext: root.service !== null && !rowNow
&& modelData.at !== null && modelData.at === root.service.nextAt
readonly property bool rowPast: root.service !== null && modelData.at !== null
&& modelData.at < root.service.now && !rowNow

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Suggestion (possible bug)

Avoid highlighting past prayers as next

Suggested change

Suggested change
readonly property bool rowNext: root.service !== null && !rowNow
&& modelData.at !== null && modelData.at === root.service.nextAt
readonly property bool rowPast: root.service !== null && modelData.at !== null
&& modelData.at < root.service.now && !rowNow
readonly property bool rowNext: root.service !== null && !rowNow
&& modelData.at !== null && modelData.at === root.service.nextAt
&& modelData.at >= root.service.now
readonly property bool rowPast: root.service !== null && modelData.at !== null
&& modelData.at < root.service.now && !rowNow

To apply: click the sparkletip, or ask @mika-agent ask <question>.

Comment on lines +379 to +381
elide: Text.ElideRight
}
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 minor · Bug · fix: low effort

The two children of this Row use different spacing deductions, so their combined width plus spacing exceeds the parent by 3 * root.s. Use the same deduction for both halves to keep the holiday control inside the panel.

Suggested change

Suggested change
elide: Text.ElideRight
}
}
Item {
width: (parent.width - 14 * root.s) / 2
height: holidayText.implicitHeight

Comment on lines +383 to +385
Item {
width: (parent.width - 8 * root.s) / 2
height: holidayText.implicitHeight

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Suggestion (bug)

Keep the footer within its row

Suggested change

Suggested change
Item {
width: (parent.width - 8 * root.s) / 2
height: holidayText.implicitHeight
Item {
width: (parent.width - 14 * root.s) / 2
height: holidayText.implicitHeight

To apply: click the sparkletip, or ask @mika-agent ask <question>.

Comment thread plugins/prayer-times/service/Main.qml Outdated
Comment on lines +98 to +102
function timesFor(date, lat, lon, method, asr, highLat, offsets) {
var hijri = H.toHijri(date.getFullYear(), date.getMonth() + 1, date.getDate(), hijriAdjust);
return PT.times({
// Umm al-Qura holds Isha 120 minutes in Ramadan instead of 90
ramadan: !!hijri && hijri.month === 9,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 major · Possible Bug · fix: low effort

H.toHijri() returns the month as m, not month, so this condition is always false. Umm al-Qura Isha remains 90 minutes after Maghrib throughout Ramadan instead of 120; the CLI has the same error. Use hijri.m in both callers and add end-to-end Ramadan tests.

…ow guard

- `hijri.month` never existed -> the 120-minute Ramadan interval was dead on both
  call sites; CLI test covers it
- carry the hijri-converter copyright/permission notice in lib/hijri.js, the file
  that actually installs
- Panel rowNext also requires at >= now, so a coincident minute tomorrow cannot
  light up a past row

@neur0map neur0map left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 mika reviewed this change; details in the walkthrough comment.

Comment on lines +106 to +111
fail('--method requires an id');
methodOverride = process.argv[i];
if (!PT.METHODS[methodOverride])
fail(`unknown method: ${methodOverride}`);
} else if (arg === '--high-lat') {
if (++i >= process.argv.length || process.argv[i].startsWith('-'))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 minor · Bug · fix: low effort

Inherited names such as constructor pass this check even though they are not calculation methods. Check for an own property so invalid CLI input cannot reach the engine as a method.

Suggested change

Suggested change
fail('--method requires an id');
methodOverride = process.argv[i];
if (!PT.METHODS[methodOverride])
fail(`unknown method: ${methodOverride}`);
} else if (arg === '--high-lat') {
if (++i >= process.argv.length || process.argv[i].startsWith('-'))
} else if (arg === '--method') {
if (++i >= process.argv.length || process.argv[i].startsWith('-'))
fail('--method requires an id');
methodOverride = process.argv[i];
if (!Object.prototype.hasOwnProperty.call(PT.METHODS, methodOverride))
fail(`unknown method: ${methodOverride}`);

Comment on lines +190 to +195
if (!next) {
const tomorrow = new Date(date.getFullYear(), date.getMonth(), date.getDate() + 1, 12);
const tomorrowTimes = calculate(tomorrow);
for (const key of keys) {
if (tomorrowTimes && tomorrowTimes[key] !== null) {
next = { key, at: tomorrowTimes[key] };

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 minor · Possible Bug · fix: medium effort

Waybar switches to tomorrow's event immediately after the final prayer, but its tooltip still shows today's date and today's times. After Isha, the highlighted text and tooltip therefore describe different schedules; include the next event's date or show tomorrow's schedule when selecting it.

Comment on lines +162 to +167
var fajr = tomorrowFajr;
if (typeof fajr !== "number" || !isFinite(fajr)) fajr = today.fajr;
if (typeof fajr !== "number" || !isFinite(fajr)) return null;
var night = (fajr <= today.maghrib ? fajr + 1440 : fajr) - today.maghrib;
return {
midnight: today.maghrib + Math.round(night / 2),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 minor · Bug · fix: low effort

tomorrowFajr is relative to tomorrow's midnight even when it is greater than today's Maghrib, so the conditional can produce a night that is a day too short. Always add 1440 minutes before subtracting today's Maghrib.

Suggested change

Suggested change
var fajr = tomorrowFajr;
if (typeof fajr !== "number" || !isFinite(fajr)) fajr = today.fajr;
if (typeof fajr !== "number" || !isFinite(fajr)) return null;
var night = (fajr <= today.maghrib ? fajr + 1440 : fajr) - today.maghrib;
return {
midnight: today.maghrib + Math.round(night / 2),
function nightInfo(today, tomorrowFajr) {
if (!today || typeof today.maghrib !== "number" || !isFinite(today.maghrib)) return null;
var fajr = tomorrowFajr;
if (typeof fajr !== "number" || !isFinite(fajr)) fajr = today.fajr;
if (typeof fajr !== "number" || !isFinite(fajr)) return null;
var night = fajr + 1440 - today.maghrib;

@mysweetpea

Copy link
Copy Markdown
Contributor Author

Good catch on the wiring — that one was mine, and my own test missed it because it called PT.times() directly instead of going through a caller.

🟡 hijri.month → hijri.m — fixed, with a test that would have caught it. toHijri returns { y, m, d }, so ramadan was always false and the 120-minute interval never fired on either call site (service and CLI). Both now read hijri.m. The CLI suite gained an end-to-end pair: Mecca under ummalqura gives maghrib + 120 on 2027‑02‑10 (Ramadan 1448) and + 90 on an ordinary day — it fails if the flag ever comes back undefined.

🟠 Upstream MIT notice — taken. The hijri-converter copyright and permission notice now sits at the top of lib/hijri.js, which is the file that actually installs: for the plugins category the packer marks README and LICENSE install: false, so a notice there would never reach a user. The README credit points at it.

🟡 rowNext matching only on time — taken. The highlight now also requires modelData.at >= service.now, so a prayer that shares a minute with tomorrow's next prayer cannot light up a row that has already passed.

Head 519bd57: screen green, catalogue + store validators clean, engine 397/397, CLI 21/21, ryoku plugin validate 0 blocking / 0 warnings.

@mysweetpea

Copy link
Copy Markdown
Contributor Author

The newest pass re-anchors four findings that were already fixed in c78bfe6/519bd57 — quick evidence so nobody re-opens them:

  • Equation of time (critical) — sunPosition(julianDay(2026,3,21) + 0.5).eqt now returns −0.119 h (it was +23.88). Baltimore on 2026‑03‑21: fajr 05:38, sunrise 07:08, dhuhr 13:14, asr 16:40, maghrib 19:20, isha 20:44.
  • guess() marking a zone as exact — cities.js returns exact: false on all three return paths; the widget therefore always shows "Auto-detected · set your city" rather than claiming a city.
  • toGregorian accepting invalid components — 1500‑12‑31, 1500‑13‑1 and 1500‑11‑31 all return null; the valid path checks the day against the next month start.
  • Hijri terminal boundary — the table has 1896 month starts and no terminal row (first 23999 = 1343‑01, last 79960 = 1500‑12). So MONTH_STARTS.length − 1 is 1500‑12, and the suggested guard idx >= MONTH_STARTS[length − 1] would reject the whole final month. toHijri(2077‑11‑16) → 1500‑12‑30, toHijri(2077‑11‑17) → null, which is what the tests pin.
  • C.guess() null deref — guess() always returns a city, and the CLI now exits on matched === false before using the fallback, so --city auto in an unmapped zone reports the zone instead of printing London.

The one genuinely open item is i18n, and it is upstream: R4 forbids importing Ryoku.Ui.Singletons (where I18n lives) and PluginKit.Singletons exports Theme/Scheme/Config/Motion only. Give PluginKit a tr() and every label routes through it in one line.

@neur0map
neur0map merged commit 053896c into Ryoku-dev:main Nov 8, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants