Skip to content

Develop - #101

Merged
Billos merged 9 commits into
mainfrom
develop
Jul 8, 2026
Merged

Develop#101
Billos merged 9 commits into
mainfrom
develop

Conversation

@Billos

@Billos Billos commented Jul 7, 2026

Copy link
Copy Markdown
Owner

Summary by CodeRabbit

  • New Features
    • Added multilingual UI support (English and French) via in-app translations, including localized labels and the cron “Update” action.
    • Introduced a Notifiers section to configure Discord and Gotify, including editable Gotify fields.
    • Standardized action button layout across key blocks (About, controls, accounts, categories, roles, schedules).
  • Bug Fixes
    • Improved budget visibility/selection indicators for schedules and budget roles for more consistent hidden-state display.
  • Refactor
    • Updated notification handling so sending/deleting occurs only when notifier settings are present.

@Billos Billos self-assigned this Jul 7, 2026
@coderabbitai

coderabbitai Bot commented Jul 7, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds localization support, updates existing front-end blocks to use translated labels and shared button layout, and introduces notifier configuration state, endpoints, runtime resolution, and UI for selecting and editing notifier settings.

Changes

Localization, shared UI, and existing block updates

Layer / File(s) Summary
i18n bootstrap and shared UI
package.json, frontend/locales/en.json, frontend/locales/fr-FR.json, frontend/main.js, frontend/molecules/BlockContainer.vue
Adds vue-i18n, locale message files, i18n app bootstrap, and responsive container styling changes.
Translated blocks and button layout
frontend/App.vue, frontend/molecules/CronInput.vue, frontend/organisms/AboutBlock.vue, frontend/organisms/CategoryBlock.vue, frontend/organisms/ControlBlock.vue, frontend/organisms/CurrentAccountsBlock.vue, frontend/organisms/RolesBlock.vue, frontend/organisms/SchedulesBlock.vue
Replaces hardcoded UI strings with translation keys, wraps action buttons in ButtonList, and changes selection/visibility indicators to right icons in the schedule and role views.

Notifier configuration plumbing, runtime, and UI

Layer / File(s) Summary
Notifier config contracts and API
src/modules/notifiers/types.ts, src/modules/config/dynamic.ts, src/endpoints/config.ts, src/endpoints/setNotifier.ts, src/endpoints/setNotifierField.ts, src/server.ts
Extends config types and dynamic keys for notifier settings, exposes notifier data in the config response, and registers endpoints for selecting a notifier and updating notifier fields.
Notifier runtime resolution
src/config.ts, src/modules/notifiers/discord.ts, src/modules/notifiers/gotify.ts, src/modules/notifiers/index.ts, src/queues/index.ts, src/queues/jobs/BaseJob.ts, src/queues/jobs/checkBudgetLimit.ts, src/queues/jobs/removeTransactionMessages.ts, src/queues/jobs/setBudgetForTransaction.ts, src/queues/jobs/setCategoryForTransaction.ts, src/queues/jobs/unbudgetedTransactions.ts, src/queues/jobs/uncategorizedTransactions.ts
Moves notifier construction to runtime config lookup, injects notifier settings into notifier classes, and updates queues and jobs to resolve and guard notifier use dynamically.
Notifier selection and fields
frontend/molecules/TextInput.vue, frontend/organisms/GotifyBlock.vue, frontend/organisms/NotifiersBlock.vue
Adds the notifier settings UI, including notifier selection buttons, Gotify-specific fields, and a reusable text input control that emits config updates.

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related PRs

  • Billos/Sparkleft#18: Both PRs modify the checkBudgetLimit queue flow in src/queues/jobs/checkBudgetLimit.ts.
  • Billos/Sparkleft#68: Both PRs modify notifier implementation files in src/modules/notifiers/discord.ts and src/modules/notifiers/gotify.ts.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Title check ❓ Inconclusive The title is too generic and does not describe the actual changes in the pull request. Use a concise title that names the main change, such as adding notifier configuration and i18n UI updates.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch develop

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (3)
frontend/molecules/ButtonList.vue (1)

4-4: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Simplify static class binding.

The class list is static, so wrapping it in an array via :class="[...]" is unnecessary; a plain class attribute is simpler and avoids an unneeded reactive binding.

♻️ Proposed simplification
-  <div :class="['flex flex-1 flex-row flex-wrap gap-2 justify-center items-start']">
+  <div class="flex flex-1 flex-row flex-wrap gap-2 justify-center items-start">
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/molecules/ButtonList.vue` at line 4, The class binding in
ButtonList.vue is static, so remove the unnecessary Vue binding from the root
<div> and use a plain class attribute instead. Update the template where the
flex layout classes are defined so the ButtonList component no longer creates an
unneeded reactive :class array.
frontend/organisms/AboutBlock.vue (1)

15-19: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Overly generic i18n keys risk collisions.

name, version, author, license, repository are single common-word keys, unlike the descriptive/namespaced convention used elsewhere in this PR (title_about, desc_show_hide_categories, label_current_account, etc.). Generic keys like these are likely to be reused/overwritten by unrelated features needing a translation for "name" or "version" in a different context.

♻️ Suggested namespacing
-        <LabeledValue :label="$t('name')" :value="props.about.name" />
-        <LabeledValue :label="$t('version')" :value="props.about.version" />
-        <LabeledValue :label="$t('author')" :value="props.about.author" />
-        <LabeledValue :label="$t('license')" :value="props.about.license" />
-        <LabeledValue :label="$t('repository')" :value="props.about.repository" :link="true" />
+        <LabeledValue :label="$t('about_name')" :value="props.about.name" />
+        <LabeledValue :label="$t('about_version')" :value="props.about.version" />
+        <LabeledValue :label="$t('about_author')" :value="props.about.author" />
+        <LabeledValue :label="$t('about_license')" :value="props.about.license" />
+        <LabeledValue :label="$t('about_repository')" :value="props.about.repository" :link="true" />
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/organisms/AboutBlock.vue` around lines 15 - 19, The AboutBlock.vue
labels are using overly generic translation keys that can collide with other
features. Update the LabeledValue bindings in AboutBlock to use descriptive,
namespaced i18n keys consistent with the rest of the PR, and then add or rename
the corresponding entries in the translation files so the keys remain unique and
context-specific.
frontend/organisms/SchedulesBlock.vue (1)

19-39: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Remove the unused helper and avoid shadowing isHidden

  • label() is unused in this file; drop it if nothing else references it.
  • Rename the local isHidden inside background() or call the helper directly to avoid the collision.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/organisms/SchedulesBlock.vue` around lines 19 - 39, The
SchedulesBlock.vue helpers include dead code and a name collision: remove the
unused label() helper if nothing references it, and in background() avoid
shadowing the existing isHidden(name) helper by renaming the local variable or
using the helper directly. Keep the hidden-budget check behavior the same while
making the function names clear and non-conflicting.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@frontend/locales/fr-FR.json`:
- Line 21: The desc_schedules entry in the French locale does not preserve the
English meaning about the cron schedule format. Update the translation in the
fr-FR JSON locale so it explicitly describes the cron pattern guidance shown to
users, matching the intent of the source string rather than only saying to
manage scheduled actions.

In `@frontend/main.js`:
- Around line 5-17: The i18n setup in createI18n currently only registers French
messages under fr-FR, so navigator.language values like fr or fr-CA fall back to
English. Update the locale mapping in main.js by either adding fr to the
messages object with the same fr_FR bundle or normalizing navigator.language to
fr-FR before passing it into createI18n, so French variants resolve correctly.

In `@frontend/organisms/CategoryBlock.vue`:
- Around line 39-55: The ActionButton label in CategoryBlock is now empty for
categories missing attributes.name, so restore a fallback label in the v-for
over props.config.categories. Update the :label binding to use a translated
placeholder when category.attributes?.name is absent, keeping it consistent with
the existing background(category) guard and the category.id-based action/key
usage.

In `@frontend/organisms/SchedulesBlock.vue`:
- Around line 64-79: The budget list in SchedulesBlock.vue is using a non-unique
Vue key based on value.attributes.name, which can collide when names repeat.
Update the v-for key on the ActionButton to use value.id instead, since it is
already available and unique, and keep the rest of the budget rendering logic
unchanged.

---

Nitpick comments:
In `@frontend/molecules/ButtonList.vue`:
- Line 4: The class binding in ButtonList.vue is static, so remove the
unnecessary Vue binding from the root <div> and use a plain class attribute
instead. Update the template where the flex layout classes are defined so the
ButtonList component no longer creates an unneeded reactive :class array.

In `@frontend/organisms/AboutBlock.vue`:
- Around line 15-19: The AboutBlock.vue labels are using overly generic
translation keys that can collide with other features. Update the LabeledValue
bindings in AboutBlock to use descriptive, namespaced i18n keys consistent with
the rest of the PR, and then add or rename the corresponding entries in the
translation files so the keys remain unique and context-specific.

In `@frontend/organisms/SchedulesBlock.vue`:
- Around line 19-39: The SchedulesBlock.vue helpers include dead code and a name
collision: remove the unused label() helper if nothing references it, and in
background() avoid shadowing the existing isHidden(name) helper by renaming the
local variable or using the helper directly. Keep the hidden-budget check
behavior the same while making the function names clear and non-conflicting.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f1280904-3f74-4ca4-8076-9810880cb5ad

📥 Commits

Reviewing files that changed from the base of the PR and between 35aede4 and e7a6118.

⛔ Files ignored due to path filters (1)
  • yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (12)
  • frontend/locales/en.json
  • frontend/locales/fr-FR.json
  • frontend/main.js
  • frontend/molecules/ButtonList.vue
  • frontend/molecules/CronInput.vue
  • frontend/organisms/AboutBlock.vue
  • frontend/organisms/CategoryBlock.vue
  • frontend/organisms/ControlBlock.vue
  • frontend/organisms/CurrentAccountsBlock.vue
  • frontend/organisms/RolesBlock.vue
  • frontend/organisms/SchedulesBlock.vue
  • package.json

Comment thread frontend/locales/fr-FR.json
Comment thread frontend/main.js
Comment on lines +5 to +17
import en from "./locales/en.json"
import fr_FR from "./locales/fr-FR.json"

createApp(App).mount("#app")
const locale = navigator.language
// const locale = "fr"
const i18n = createI18n({
locale,
fallbackLocale: "en",
messages: {
en,
"fr-FR": fr_FR,
},
})

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

Does vue-i18n implicit locale fallback work from base language to a territory-specific variant (e.g. "fr" resolving messages registered only under "fr-FR")?

💡 Result:

No, the implicit fallback in Vue I18n works in the opposite direction of what you described [1][2]. Vue I18n's implicit fallback mechanism automatically resolves from a territory-specific locale to its base language, not from a base language to a specific territory [1][2]. For example, if your locale is set to "fr-FR" and a translation key is missing, Vue I18n will automatically attempt to fall back to "fr" [2]. It does not automatically resolve from a base language (like "fr") to a territory-specific variant (like "fr-FR") because "fr" is considered more general than "fr-FR" [2]. If you require "fr" to access messages registered under "fr-FR", you must explicitly configure a fallback chain using the fallbackLocale option [2]. You can define a decision map to specify exactly how locales should chain together: const i18n = createI18n({ locale: 'fr', fallbackLocale: { 'fr': ['fr-FR'], 'default': ['en'] }, //... }) In this configuration, when a key is missing in "fr", Vue I18n will look for it in "fr-FR" before proceeding to the default fallback [2].

Citations:


🌐 Web query:

vue-i18n locale fallback chain base language territory-specific variant fr fr-FR documentation

💡 Result:

In vue-i18n, when a locale string includes a territory (e.g., fr-FR), the library automatically activates an implicit fallback mechanism [1][2]. The fallback behavior for a locale with a territory follows a specific chain: 1. Implicit Fallback: If you set the locale to fr-FR, vue-i18n will automatically attempt to resolve keys by first checking fr-FR, then falling back to the base language, fr [1][2]. 2. Explicit Fallback: If the key is not found in the base language (fr), the system will then proceed to the locale(s) defined in your fallbackLocale configuration [1]. For example, if locale is set to fr-FR and fallbackLocale is set to en, the full lookup chain is: fr-FR -> fr -> en [1]. Key technical details: - Automatic Implicit Fallback: This process occurs automatically for any locale string containing a hyphen-separated territory or dialect [1][2]. - Suppressing Implicit Fallback: You can disable this implicit fallback behavior for a specific locale by appending an exclamation mark (!) to the locale string (e.g., fr-FR!) [1]. - Fallback Configuration: The fallbackLocale option can accept a single string, an array of strings (for multiple fallback priorities), or a complex decision map for advanced routing [3][1]. The underlying implementation uses a fallback chain algorithm that parses the locale string by splitting it at hyphens and iteratively stripping the last segment to generate the fallback sequence [4].

Citations:


🌐 Web query:

site:vue-i18n.intlify.dev locale fallback chain territory language base locale fr-FR fr documentation

💡 Result:

In the context of software localization (specifically frameworks like Vue I18n), the locale fallback chain is a mechanism used to resolve missing translations by moving from a specific locale to more general ones [1]. For a locale string like fr-FR (where fr is the language and FR is the territory), the automatic fallback chain typically proceeds as follows: 1. fr-FR (Specific locale) 2. fr (Base locale/Language) When a translation key is missing in the primary locale (e.g., fr-FR), the system automatically attempts to resolve it using the base language (fr) before eventually falling back to a globally configured fallback locale (such as en) if provided [1]. This process is often referred to as an implicit fallback [1][2][3]. If a locale string contains both a territory and a dialect (e.g., de-DE-bavarian), the chain extends further: de-DE-bavarian > de-DE > de [1]. To stop this automatic implicit fallback, developers can often append an exclamation mark (e.g., fr-FR!) to the locale string [1]. The fallback behavior can also be explicitly customized by providing an array of locales or a decision map in the configuration [1].

Citations:


🌐 Web query:

site:vue-i18n.intlify.dev fallbackLocale locale chain 'fr-FR' 'fr' vue-i18n

💡 Result:

In vue-i18n, the locale fallback mechanism is designed to handle hierarchical or chained fallbacks automatically [1][2]. For a locale like 'fr-FR', vue-i18n includes an implicit fallback mechanism [3][4]. If a translation key is missing for 'fr-FR', the system will automatically attempt to resolve it by stripping the region/dialect information, effectively falling back to 'fr' [3][4]. To explicitly define a fallback chain, you can configure the fallbackLocale option in your createI18n instance [5][6]. 1. Implicit Fallback: If you set your locale to 'fr-FR', vue-i18n natively attempts to resolve keys using the 'fr' locale if they are not found in 'fr-FR' [3][4]. 2. Explicit Fallback Chain: If you need to specify a more complex or custom chain (e.g., if 'fr' is also missing, you want to fall back to 'en'), you can provide an array to fallbackLocale [2]: const i18n = createI18n({ locale: 'fr-FR', fallbackLocale: ['fr', 'en'], // Try 'fr', then 'en' if 'fr' is missing messages: { 'fr-FR': {... }, 'fr': {... }, 'en': {... } } }) The default behavior, known as the fallbackWithLocaleChain algorithm, ensures that the system traverses from the specific locale to the base language and then to the specified fallback locales [1][2].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail
printf 'frontend/main.js\n'
if [ -f frontend/main.js ]; then
  cat -n frontend/main.js | sed -n '1,40p'
fi

Repository: Billos/Sparkleft

Length of output: 721


Map French messages to fr as well as fr-FR. navigator.language can be fr or fr-CA, and vue-i18n won’t resolve those to fr-FR; they’ll fall through to English here. Register fr: fr_FR or normalize the locale before creating i18n.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/main.js` around lines 5 - 17, The i18n setup in createI18n currently
only registers French messages under fr-FR, so navigator.language values like fr
or fr-CA fall back to English. Update the locale mapping in main.js by either
adding fr to the messages object with the same fr_FR bundle or normalizing
navigator.language to fr-FR before passing it into createI18n, so French
variants resolve correctly.

Comment thread frontend/organisms/CategoryBlock.vue
Comment thread frontend/organisms/SchedulesBlock.vue

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (4)
frontend/organisms/GotifyBlock.vue (1)

17-49: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Repetitive TextInput blocks could be data-driven.

The four TextInput usages differ only by label/placeholder/action/model path. Consider iterating over a small array of field descriptors to reduce duplication, since more Gotify fields (or other providers) will likely follow the same shape.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/organisms/GotifyBlock.vue` around lines 17 - 49, The GotifyBlock
component repeats four nearly identical TextInput blocks; make this data-driven
by defining a small field-descriptor array and rendering it with a loop in
GotifyBlock.vue. Keep the existing bindings to props.config.token,
props.config.notifierConfig, and the update:config emit, but move the varying
label, placeholder, action, and config key into the descriptor so the template
is easier to extend for future Gotify fields.
frontend/organisms/NotifiersBlock.vue (1)

42-42: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Hardcoded "Not implemented yet" string.

Inconsistent with the rest of this component, which uses $t(...) for header/subtitle text.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/organisms/NotifiersBlock.vue` at line 42, The fallback text in
NotifiersBlock.vue is hardcoded and should use localization like the rest of the
component. Update the v-else branch in NotifiersBlock to replace the literal
“Not implemented yet” with a $t(...) lookup, matching the existing translation
pattern used for the header and subtitle text.
frontend/molecules/TextInput.vue (2)

17-30: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Label not programmatically associated with the input.

The label is a plain <div> with no for/id linkage to the <input>, reducing screen-reader usability for this reusable field.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/molecules/TextInput.vue` around lines 17 - 30, The TextInput
component’s label is only rendered as a plain div, so it is not programmatically
tied to the input for assistive tech. Update the TextInput.vue template to use a
real label element (or equivalent accessible association) connected to the input
via matching for/id values, and ensure the component exposes/uses a stable
unique id alongside the existing label and model bindings.

35-35: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Hardcoded "Save" label breaks i18n consistency.

Every other new label in this cohort (GotifyBlock, NotifiersBlock) uses $t(...). This shared, reused component hardcodes "Save" in English for all consumers.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/molecules/TextInput.vue` at line 35, The TextInput.vue component has
a hardcoded English label "Save" on the label prop which breaks
internationalization consistency. Replace the hardcoded string "Save" with the
appropriate `$t(...)` translation function call (using a suitable translation
key like "save" or similar) to match the i18n pattern used in other components
like GotifyBlock and NotifiersBlock, ensuring the label respects the user's
locale settings.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/endpoints/config.ts`:
- Around line 129-136: The config read response in the config endpoint is
echoing notifier secrets in cleartext. Update the notifierConfig object returned
by the config read flow so that `discordWebhook`, `gotifyToken`, and
`gotifyUserToken` are masked or replaced with a configured boolean, and keep raw
values only in the write path handled by `setNotifierField`; use the existing
`notifierConfig`/`notifier` response assembly to locate and adjust this
behavior.
- Around line 50-51: The config response shape is too narrow because
`configEndpoint` can return a null notifier from
`DynamicConfig.get(VConfig.Notifier)` while `Config.notifier` is typed as
`Notifiers`. Update the `Config` contract and any related mapping in
`configEndpoint` to allow `notifier` to be `Notifiers | null`, or ensure it is
always initialized before the endpoint returns. Keep the change aligned with the
existing `configEndpoint` and `DynamicConfig.get(VConfig.Notifier)` flow so the
API type matches the runtime value.

In `@src/endpoints/setNotifierField.ts`:
- Around line 17-23: The route param in setNotifierField is being typed too
narrowly as Notifiers even though it arrives as an untrusted string and is later
narrowed by isNotifierField before calling DynamicConfig.set(). Update the
Request generic in setNotifierField to use string for field, then keep the
existing runtime validation/narrowing flow so only validated values reach
DynamicConfig.set().

---

Nitpick comments:
In `@frontend/molecules/TextInput.vue`:
- Around line 17-30: The TextInput component’s label is only rendered as a plain
div, so it is not programmatically tied to the input for assistive tech. Update
the TextInput.vue template to use a real label element (or equivalent accessible
association) connected to the input via matching for/id values, and ensure the
component exposes/uses a stable unique id alongside the existing label and model
bindings.
- Line 35: The TextInput.vue component has a hardcoded English label "Save" on
the label prop which breaks internationalization consistency. Replace the
hardcoded string "Save" with the appropriate `$t(...)` translation function call
(using a suitable translation key like "save" or similar) to match the i18n
pattern used in other components like GotifyBlock and NotifiersBlock, ensuring
the label respects the user's locale settings.

In `@frontend/organisms/GotifyBlock.vue`:
- Around line 17-49: The GotifyBlock component repeats four nearly identical
TextInput blocks; make this data-driven by defining a small field-descriptor
array and rendering it with a loop in GotifyBlock.vue. Keep the existing
bindings to props.config.token, props.config.notifierConfig, and the
update:config emit, but move the varying label, placeholder, action, and config
key into the descriptor so the template is easier to extend for future Gotify
fields.

In `@frontend/organisms/NotifiersBlock.vue`:
- Line 42: The fallback text in NotifiersBlock.vue is hardcoded and should use
localization like the rest of the component. Update the v-else branch in
NotifiersBlock to replace the literal “Not implemented yet” with a $t(...)
lookup, matching the existing translation pattern used for the header and
subtitle text.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: cfde74d4-d121-4de0-8f4f-547d9150c500

📥 Commits

Reviewing files that changed from the base of the PR and between d385bbe and 70bfccb.

⛔ Files ignored due to path filters (1)
  • yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (12)
  • frontend/App.vue
  • frontend/locales/en.json
  • frontend/locales/fr-FR.json
  • frontend/molecules/TextInput.vue
  • frontend/organisms/GotifyBlock.vue
  • frontend/organisms/NotifiersBlock.vue
  • src/endpoints/config.ts
  • src/endpoints/setNotifier.ts
  • src/endpoints/setNotifierField.ts
  • src/modules/config/dynamic.ts
  • src/modules/notifiers/types.ts
  • src/server.ts
✅ Files skipped from review due to trivial changes (2)
  • src/modules/notifiers/types.ts
  • frontend/locales/en.json
🚧 Files skipped from review as they are similar to previous changes (2)
  • frontend/locales/fr-FR.json
  • frontend/App.vue

Comment thread src/endpoints/config.ts
Comment on lines +50 to +51
notifier: Notifiers
notifierConfig: NotifierConfig

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify DynamicConfig.get() return type
fd -a 'dynamic.ts' src/modules/config --exec cat -n {}

Repository: Billos/Sparkleft

Length of output: 2567


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the config endpoint and notifier-related types/usages
ast-grep outline src/endpoints/config.ts --view expanded
printf '\n--- FILE: src/endpoints/config.ts (relevant range) ---\n'
cat -n src/endpoints/config.ts | sed -n '1,220p'

printf '\n--- SEARCH: Notifiers definition/usages ---\n'
rg -n "export (enum|type|interface) Notifiers|Notifiers" src -g '!**/*.map'

Repository: Billos/Sparkleft

Length of output: 7247


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n--- FILE: src/modules/notifiers/types.ts ---\n'
cat -n src/modules/notifiers/types.ts

printf '\n--- FILE: src/endpoints/setNotifier.ts ---\n'
cat -n src/endpoints/setNotifier.ts

printf '\n--- SEARCH: writes to VConfig.Notifier ---\n'
rg -n "VConfig\.Notifier|\"notifier\"" src -g '!**/*.map'

Repository: Billos/Sparkleft

Length of output: 2407


Make notifier nullable in the config response
DynamicConfig.get(VConfig.Notifier) can return null, and configEndpoint currently casts that to Notifiers. Expose Config.notifier as Notifiers | null (or initialize it before serving this endpoint) so the API contract matches the runtime value.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/endpoints/config.ts` around lines 50 - 51, The config response shape is
too narrow because `configEndpoint` can return a null notifier from
`DynamicConfig.get(VConfig.Notifier)` while `Config.notifier` is typed as
`Notifiers`. Update the `Config` contract and any related mapping in
`configEndpoint` to allow `notifier` to be `Notifiers | null`, or ensure it is
always initialized before the endpoint returns. Keep the change aligned with the
existing `configEndpoint` and `DynamicConfig.get(VConfig.Notifier)` flow so the
API type matches the runtime value.

Comment thread src/endpoints/config.ts
Comment on lines +129 to +136
notifier: notifier as Notifiers,
notifierConfig: {
discordWebhook: notifierDiscordWebhook,
gotifyUrl: notifierGotifyUrl,
gotifyToken: notifierGotifyToken,
gotifyUserToken: notifierGotifyUserToken,
gotifyApplicationId: notifierGotifyApplicationId,
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

Secrets returned in cleartext via the config read endpoint.

discordWebhook, gotifyToken, and gotifyUserToken are sent back verbatim to any client calling this endpoint. Since this is a persistent config-read surface (not just a one-time write ack), consider returning only a "configured" boolean/masked value and letting the write endpoints (setNotifierField) remain the only place raw values are transmitted (write-only, never echoed back).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/endpoints/config.ts` around lines 129 - 136, The config read response in
the config endpoint is echoing notifier secrets in cleartext. Update the
notifierConfig object returned by the config read flow so that `discordWebhook`,
`gotifyToken`, and `gotifyUserToken` are masked or replaced with a configured
boolean, and keep raw values only in the write path handled by
`setNotifierField`; use the existing `notifierConfig`/`notifier` response
assembly to locate and adjust this behavior.

Comment on lines +17 to +23
function isNotifierField(value: string): value is VConfig {
return valid.includes(value as VConfig)
}

export async function setNotifierField(req: Request<{ field: Notifiers }, unknown, { value: string }>, res: Response) {
const { field } = req.params
const { value } = req.body

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
fd -a 'types.ts' src/modules/notifiers --exec cat -n {}

Repository: Billos/Sparkleft

Length of output: 252


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n## setNotifierField.ts\n'
cat -n src/endpoints/setNotifierField.ts

printf '\n## VConfig references\n'
rg -n "enum VConfig|type VConfig|interface VConfig|valid.includes|set\\(" src -S

printf '\n## notifier types\n'
fd -a 'types.ts' src | xargs -r sed -n '1,120p'

Repository: Billos/Sparkleft

Length of output: 2710


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n## src/endpoints/setNotifierField.ts\n'
cat -n src/endpoints/setNotifierField.ts

printf '\n## VConfig definition and uses\n'
rg -n "enum VConfig|type VConfig|interface VConfig|valid =|valid\\.includes\\(|DynamicConfig\\.set|setNotifierField" src -S

printf '\n## all config enum definitions\n'
rg -n "enum .*Config|type .*Config" src -S

Repository: Billos/Sparkleft

Length of output: 3277


🏁 Script executed:

#!/bin/bash
set -euo pipefail

cat -n src/modules/config/dynamic.ts

Repository: Billos/Sparkleft

Length of output: 2567


field should be typed as string, not Notifiers.

field is a raw route param that gets narrowed to VConfig before DynamicConfig.set(). Declaring it as Notifiers is misleading and can hide real type mismatches.

🔧 Proposed fix
-export async function setNotifierField(req: Request<{ field: Notifiers }, unknown, { value: string }>, res: Response) {
+export async function setNotifierField(req: Request<{ field: string }, unknown, { value: string }>, res: Response) {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function isNotifierField(value: string): value is VConfig {
return valid.includes(value as VConfig)
}
export async function setNotifierField(req: Request<{ field: Notifiers }, unknown, { value: string }>, res: Response) {
const { field } = req.params
const { value } = req.body
function isNotifierField(value: string): value is VConfig {
return valid.includes(value as VConfig)
}
export async function setNotifierField(req: Request<{ field: string }, unknown, { value: string }>, res: Response) {
const { field } = req.params
const { value } = req.body
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/endpoints/setNotifierField.ts` around lines 17 - 23, The route param in
setNotifierField is being typed too narrowly as Notifiers even though it arrives
as an untrusted string and is later narrowed by isNotifierField before calling
DynamicConfig.set(). Update the Request generic in setNotifierField to use
string for field, then keep the existing runtime validation/narrowing flow so
only validated values reach DynamicConfig.set().

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/queues/jobs/uncategorizedTransactions.ts (1)

113-131: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Init log messages say "UnbudgetedTransactions" instead of "UncategorizedTransactions" and hardcode count as 0.

Both log messages in init() are copy-pasted from UnbudgetedTransactionsJob and incorrectly say "UnbudgetedTransactions". The count is also hardcoded to 0 regardless of actual transactions processed.

🔧 Proposed fix
   override async init(): Promise<void> {
-    logger.info("Initializing UnbudgetedTransactions jobs for all unbudgeted transactions")
+    logger.info("Initializing UncategorizedTransactions jobs for all uncategorized transactions")
     const notifier = await getNotifier()
+    let count = 0
     if (notifier) {
       const startDate = Temporal.Now.zonedDateTimeISO(env.timezone).subtract({ months: 3 }).startOfDay()
       const start = startDate.toPlainDate().toString()
       const endDate = Temporal.Now.zonedDateTimeISO(env.timezone)
       const end = endDate.toPlainDate().toString()
       if (!end) {
         logger.error("Failed to get current date in ISO format")
         return
       }
       const uncategorizedTransactionsList = await getUncategorizedTransactions(start, end)
+      count = uncategorizedTransactionsList.length
       for (const { id: transactionId } of uncategorizedTransactionsList) {
         await addTransactionJobToQueue(this, transactionId)
       }
     }
-    logger.info("Initialized UnbudgetedTransactions jobs for %d transactions", 0)
+    logger.info("Initialized UncategorizedTransactions jobs for %d transactions", count)
   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/queues/jobs/uncategorizedTransactions.ts` around lines 113 - 131, The
init logging in UncategorizedTransactionsJob is using copied
UnbudgetedTransactions wording and a hardcoded transaction count. Update the
logger.info messages in init() to reference “UncategorizedTransactions”
consistently, and replace the final 0 with the actual number of transactions
processed from getUncategorizedTransactions. Use the init() method,
uncategorizedTransactionsList, and addTransactionJobToQueue as the main points
to adjust.
src/queues/jobs/unbudgetedTransactions.ts (1)

89-99: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Hardcoded 0 in init log always reports zero transactions.

Line 98 logs 0 regardless of how many transactions were actually queued. When a notifier is configured and transactions are processed, the log is misleading.

🔧 Proposed fix
   override async init(): Promise<void> {
     logger.info("Initializing UnbudgetedTransactions jobs for all unbudgeted transactions")
     const notifier = await getNotifier()
+    let count = 0
     if (notifier) {
       const { data } = await BudgetsService.listTransactionWithoutBudget({ client, query: { page: 1, limit: 50 } })
+      count = data.length
       for (const { id: transactionId } of data) {
         await addTransactionJobToQueue(this, transactionId)
       }
     }
-    logger.info("Initialized UnbudgetedTransactions jobs for %d transactions", 0)
+    logger.info("Initialized UnbudgetedTransactions jobs for %d transactions", count)
   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/queues/jobs/unbudgetedTransactions.ts` around lines 89 - 99, The init log
in UnbudgetedTransactions always reports 0 transactions, which makes the success
message misleading. In init() of UnbudgetedTransactions, track how many
transaction IDs are actually queued while iterating over
BudgetsService.listTransactionWithoutBudget() and pass that count to the final
logger.info call instead of a hardcoded value. Keep the existing notifier check
and use the same addTransactionJobToQueue flow so the log reflects the real
number processed.
🧹 Nitpick comments (1)
src/queues/index.ts (1)

176-180: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Unawaited sendMessage inside async handler.

notifier.sendMessage(...) is not awaited, so a rejection becomes an unhandled promise rejection (the surrounding try/logging won't catch it). Await it, or attach a .catch to log failures.

♻️ Proposed fix
     if (notifier) {
-      notifier.sendMessage(
-        "Job Failed",
-        `Job **${job.data.job}** (${job.id}) failed with error ${err.message} and data ${JSON.stringify(job.data)}`,
-      )
+      await notifier.sendMessage(
+        "Job Failed",
+        `Job **${job.data.job}** (${job.id}) failed with error ${err.message} and data ${JSON.stringify(job.data)}`,
+      )
     } else {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/queues/index.ts` around lines 176 - 180, The notifier call in the async
job-failure handler is fire-and-forget, so any rejection from
notifier.sendMessage in the job failure path can escape as an unhandled promise
rejection. Update the failure-notification logic in the queue handler that uses
job.data, job.id, and notifier.sendMessage to either await the sendMessage
promise or attach a .catch that logs the notification failure, keeping it inside
the surrounding error handling flow.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/queues/index.ts`:
- Around line 156-167: The `worker.on("completed")` handler in
`src/queues/index.ts` returns early when `getNotifier()` is null, which prevents
`logJobDuration(true, id ?? "unknown", name ?? "unknown")` from running for
delayed jobs. Update the `completed` callback so the notifier lookup and
`deleteMessage` path are only conditional, but `logJobDuration` always executes
after the delayed-message branch, using the existing `worker.on("completed")`
and `logJobDuration` symbols to keep timing/log cleanup consistent.

In `@src/queues/jobs/BaseJob.ts`:
- Around line 95-99: The auto-import test mock for the notifiers module is
missing the getNotifier export used by BaseJob, so importing the job module
fails. Update the ../modules/notifiers mock in src/__tests__/autoImport.test.ts
to include getNotifier alongside the existing notifier mock, keeping the mock
shape aligned with BaseJob’s getNotifier usage so
import("../queues/jobs/autoImport.js") succeeds.

In `@src/queues/jobs/setBudgetForTransaction.ts`:
- Line 22: The `setBudgetForTransaction` change now calls `getNotifier()`, but
the `src/__tests__/jobs/setBudgetForTransaction.test.ts` mock for
`../../modules/notifiers` still only exposes the old `notifier` value. Update
the test mock to include a `getNotifier` export that returns a mock resolved
notifier (or `null` when needed) so the `setBudgetForTransaction` job can be
exercised without missing-export failures.

In `@src/queues/jobs/setCategoryForTransaction.ts`:
- Around line 22-34: The notifier cleanup in setCategoryForTransaction is
swallowing the real failure and logging a misleading “No notifier message to
delete” message. Update the try/catch around getMessageId,
unbindTransactionToNotification, and notifier.deleteMessage to capture the
thrown error, and log that error with context in the catch block instead of
implying the message is simply missing. Keep the existing logger, notifier, and
unbindTransactionToNotification flow, but make the failure message reflect an
actual delete/unbind error for the transaction id.

---

Outside diff comments:
In `@src/queues/jobs/unbudgetedTransactions.ts`:
- Around line 89-99: The init log in UnbudgetedTransactions always reports 0
transactions, which makes the success message misleading. In init() of
UnbudgetedTransactions, track how many transaction IDs are actually queued while
iterating over BudgetsService.listTransactionWithoutBudget() and pass that count
to the final logger.info call instead of a hardcoded value. Keep the existing
notifier check and use the same addTransactionJobToQueue flow so the log
reflects the real number processed.

In `@src/queues/jobs/uncategorizedTransactions.ts`:
- Around line 113-131: The init logging in UncategorizedTransactionsJob is using
copied UnbudgetedTransactions wording and a hardcoded transaction count. Update
the logger.info messages in init() to reference “UncategorizedTransactions”
consistently, and replace the final 0 with the actual number of transactions
processed from getUncategorizedTransactions. Use the init() method,
uncategorizedTransactionsList, and addTransactionJobToQueue as the main points
to adjust.

---

Nitpick comments:
In `@src/queues/index.ts`:
- Around line 176-180: The notifier call in the async job-failure handler is
fire-and-forget, so any rejection from notifier.sendMessage in the job failure
path can escape as an unhandled promise rejection. Update the
failure-notification logic in the queue handler that uses job.data, job.id, and
notifier.sendMessage to either await the sendMessage promise or attach a .catch
that logs the notification failure, keeping it inside the surrounding error
handling flow.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 65b4769c-3712-49c7-bcaa-9539e7119954

📥 Commits

Reviewing files that changed from the base of the PR and between 70bfccb and bdc1891.

📒 Files selected for processing (13)
  • src/config.ts
  • src/modules/notifiers/discord.ts
  • src/modules/notifiers/gotify.ts
  • src/modules/notifiers/index.ts
  • src/queues/index.ts
  • src/queues/jobs/BaseJob.ts
  • src/queues/jobs/checkBudgetLimit.ts
  • src/queues/jobs/removeTransactionMessages.ts
  • src/queues/jobs/setBudgetForTransaction.ts
  • src/queues/jobs/setCategoryForTransaction.ts
  • src/queues/jobs/unbudgetedTransactions.ts
  • src/queues/jobs/uncategorizedTransactions.ts
  • tsconfig.json
💤 Files with no reviewable changes (1)
  • src/config.ts

Comment thread src/queues/index.ts
Comment on lines 156 to 167
worker.on("completed", async ({ id, name, data }) => {
if (data.delayedMessageId) {
logger.info("Deleting delayed message %s for job %s (%s)", data.delayedMessageId, id, name)
const notifier = await getNotifier()
if (!notifier) {
logger.warn("No notifier configured, skipping message deletion for job %s (%s)", id, name)
return
}
await notifier.deleteMessage(data.delayedMessageId)
}
logJobDuration(true, id ?? "unknown", name ?? "unknown")
})

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

completed handler skips logJobDuration when no notifier is configured.

When data.delayedMessageId is set but getNotifier() returns null, the early return at Line 162 bypasses logJobDuration(true, ...) at Line 166, so completed delayed jobs won't be timed/logged (and startedAt won't be cleaned up). Fold the notifier guard into the branch so the duration log always runs.

🐛 Proposed fix
   worker.on("completed", async ({ id, name, data }) => {
     if (data.delayedMessageId) {
       logger.info("Deleting delayed message %s for job %s (%s)", data.delayedMessageId, id, name)
       const notifier = await getNotifier()
-      if (!notifier) {
-        logger.warn("No notifier configured, skipping message deletion for job %s (%s)", id, name)
-        return
-      }
-      await notifier.deleteMessage(data.delayedMessageId)
+      if (notifier) {
+        await notifier.deleteMessage(data.delayedMessageId)
+      } else {
+        logger.warn("No notifier configured, skipping message deletion for job %s (%s)", id, name)
+      }
     }
     logJobDuration(true, id ?? "unknown", name ?? "unknown")
   })
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
worker.on("completed", async ({ id, name, data }) => {
if (data.delayedMessageId) {
logger.info("Deleting delayed message %s for job %s (%s)", data.delayedMessageId, id, name)
const notifier = await getNotifier()
if (!notifier) {
logger.warn("No notifier configured, skipping message deletion for job %s (%s)", id, name)
return
}
await notifier.deleteMessage(data.delayedMessageId)
}
logJobDuration(true, id ?? "unknown", name ?? "unknown")
})
worker.on("completed", async ({ id, name, data }) => {
if (data.delayedMessageId) {
logger.info("Deleting delayed message %s for job %s (%s)", data.delayedMessageId, id, name)
const notifier = await getNotifier()
if (notifier) {
await notifier.deleteMessage(data.delayedMessageId)
} else {
logger.warn("No notifier configured, skipping message deletion for job %s (%s)", id, name)
}
}
logJobDuration(true, id ?? "unknown", name ?? "unknown")
})
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/queues/index.ts` around lines 156 - 167, The `worker.on("completed")`
handler in `src/queues/index.ts` returns early when `getNotifier()` is null,
which prevents `logJobDuration(true, id ?? "unknown", name ?? "unknown")` from
running for delayed jobs. Update the `completed` callback so the notifier lookup
and `deleteMessage` path are only conditional, but `logJobDuration` always
executes after the delayed-message branch, using the existing
`worker.on("completed")` and `logJobDuration` symbols to keep timing/log cleanup
consistent.

Comment thread src/queues/jobs/BaseJob.ts
Comment thread src/queues/jobs/setBudgetForTransaction.ts
Comment on lines +22 to 34
const notifier = await getNotifier()
if (notifier) {
try {
logger.info("Deleting notifier message")
const messageId = await notifier.getMessageId("CategoryMessageId", id)
await unbindTransactionToNotification(id, "CategoryMessageId", messageId)
await notifier.deleteMessage(messageId)
} catch {
logger.error("No notifier message to delete for transaction %s", id)
}
} else {
logger.warn("No notifier configured, skipping message removal for transaction %s", id)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Catch block discards error details and uses misleading log message.

The catch block doesn't capture the error, so the actual failure reason (network error, API failure, etc.) is lost. The log message "No notifier message to delete" is misleading — the failure may not be a missing message.

🔧 Proposed fix
       } catch {
-        logger.error("No notifier message to delete for transaction %s", id)
+      } catch (err) {
+        logger.error({ err }, "Failed to delete notifier message for transaction %s", id)
       }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const notifier = await getNotifier()
if (notifier) {
try {
logger.info("Deleting notifier message")
const messageId = await notifier.getMessageId("CategoryMessageId", id)
await unbindTransactionToNotification(id, "CategoryMessageId", messageId)
await notifier.deleteMessage(messageId)
} catch {
logger.error("No notifier message to delete for transaction %s", id)
}
} else {
logger.warn("No notifier configured, skipping message removal for transaction %s", id)
}
const notifier = await getNotifier()
if (notifier) {
try {
logger.info("Deleting notifier message")
const messageId = await notifier.getMessageId("CategoryMessageId", id)
await unbindTransactionToNotification(id, "CategoryMessageId", messageId)
await notifier.deleteMessage(messageId)
} catch (err) {
logger.error({ err }, "Failed to delete notifier message for transaction %s", id)
}
} else {
logger.warn("No notifier configured, skipping message removal for transaction %s", id)
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/queues/jobs/setCategoryForTransaction.ts` around lines 22 - 34, The
notifier cleanup in setCategoryForTransaction is swallowing the real failure and
logging a misleading “No notifier message to delete” message. Update the
try/catch around getMessageId, unbindTransactionToNotification, and
notifier.deleteMessage to capture the thrown error, and log that error with
context in the catch block instead of implying the message is simply missing.
Keep the existing logger, notifier, and unbindTransactionToNotification flow,
but make the failure message reflect an actual delete/unbind error for the
transaction id.

Copilot finished work on behalf of Billos July 8, 2026 09:04
@Billos
Billos merged commit 7e81875 into main Jul 8, 2026
5 checks passed
@coderabbitai coderabbitai Bot mentioned this pull request Jul 11, 2026
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