Skip to content

feat(floware): change to OAuth flow from DWD for cron job#291

Open
vishnurk6247 wants to merge 4 commits into
developfrom
feat/gmail-cron
Open

feat(floware): change to OAuth flow from DWD for cron job#291
vishnurk6247 wants to merge 4 commits into
developfrom
feat/gmail-cron

Conversation

@vishnurk6247
Copy link
Copy Markdown
Member

@vishnurk6247 vishnurk6247 commented May 26, 2026

Summary by CodeRabbit

  • Configuration Changes

    • Gmail auth switched to OAuth 2.0 (client ID/secret + refresh token); update deployment config accordingly.
  • New Features

    • Scheduled email alerts: choose recipients by user, multi-select UI, recipient labels shown.
    • Scheduled reports now support per-column styling rules and are generated as Excel files.
  • Behavior Changes

    • Reports personalized per recipient with row-level filtering; large reports use secure download links; per-recipient delivery failures handled without aborting all deliveries.
  • UI

    • Schedule dialog reorganized into “Schedule” and “Email” tabs with improved validation.

Review Change Stack

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 26, 2026

📝 Walkthrough

Walkthrough

Gmail auth switched from service-account delegation to OAuth (client_id/secret + refresh_token); ScheduledJobService now generates per-recipient XLSX reports with optional column styling and enforces per-recipient RLS, sending individualized attachments or presigned links; client types/hooks/UI and DI updated; openpyxl added.

Changes

Gmail OAuth Migration

Layer / File(s) Summary
Config schema migration
wavefront/server/apps/floware/floware/config.ini
Gmail configuration parameters replaced: service_account_file and delegate_user removed; client_id, client_secret, and refresh_token added while email_sender is retained.
GmailEmailService OAuth implementation
wavefront/server/modules/user_management_module/user_management_module/services/email_service.py
GmailEmailService constructor signature changed to accept client_id, client_secret, refresh_token; imports updated for OAuth credential handling; credential refresh and Gmail API service initialization added; send_email uses new service builder.
UserContainer DI wiring
wavefront/server/modules/user_management_module/user_management_module/user_container.py
UserContainer email_service provider for Gmail updated to inject client_id, client_secret, refresh_token to GmailEmailService instead of service_account_b64 and delegate_user.

Scheduled Job XLSX + Recipient RLS

Layer / File(s) Summary
Client types, hooks & UI
wavefront/client/src/types/scheduled-job.ts, wavefront/client/src/pages/apps/[appId]/datasources/ScheduleEmailAlertDialog.tsx, wavefront/client/src/api/app-user-service.ts, wavefront/client/src/hooks/data/*
Adds ColumnStyleRule/ColumnStyleConfig types; ScheduleEmailAlertDialog updated for recipient-user multi-select, column_styles JSON, and tabbed schedule/email UI; client hooks/query-keys split into app-scoped and console-scoped variants; create/update payloads use recipient_user_ids.
DI wiring
wavefront/server/apps/floware/floware/di/application_container.py, wavefront/server/apps/floware/floware/server.py
ApplicationContainer adds user_service, role_repository, user_role_repository dependencies and scheduled_job_service wiring now passes these into ScheduledJobService; server constructs ApplicationContainer with the new dependencies.
Dependency update
wavefront/server/apps/floware/pyproject.toml
Adds openpyxl dependency for XLSX generation.
ScheduledJobService XLSX & per-recipient RLS
wavefront/server/apps/floware/floware/services/scheduled_job_service.py
Refactors scheduled dynamic-query email execution: removes CSV, adds openpyxl XLSX generation with conditional column styling, normalizes recipient_user_ids, enforces per-recipient RLS (admin detection via role/user_role lookups), fetches rows per recipient, chooses attachment vs presigned link by size, sends individualized emails, and logs/tracks per-recipient failures.

Sequence Diagram

sequenceDiagram
  participant DI as ApplicationContainer
  participant Scheduler as ScheduledJobService
  participant UserRepo as UserRepository
  participant RoleRepo as RoleRepository
  participant DB as DynamicQueryEngine
  participant Storage as CloudStorage
  participant EmailSvc as EmailService

  DI->>Scheduler: provide user_service, role_repository, user_role_repository, email_service
  Scheduler->>UserRepo: load recipient user and app associations
  Scheduler->>RoleRepo: check admin roles for recipient
  Scheduler->>DB: fetch rows with per-recipient RLS filter
  DB->>Scheduler: return rows
  Scheduler->>Scheduler: generate XLSX bytes (openpyxl) and apply column styles
  Scheduler->>Storage: upload bytes when large
  Storage->>Scheduler: return presigned URL
  Scheduler->>EmailSvc: send individualized email with attachment or link
  EmailSvc->>Scheduler: return delivery result
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • vizsatiz

Poem

🐰 I nibble tokens, stitch the seams,
OAuth springs and spreadsheet dreams.
Users chosen, styles applied,
Reports in bytes across the tide.
A rabbit hops — "Code, now beam!"

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: migrating from service-account-based Gmail authentication (DWD/delegation) to OAuth2 credentials flow for cron job email scheduling.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/gmail-cron

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 and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

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

⚠️ Outside diff range comments (1)
wavefront/client/src/pages/apps/[appId]/datasources/ScheduleEmailAlertDialog.tsx (1)

286-301: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Preserve existing payload fields on update.

This update path reconstructs payload from a subset of form fields, so editing an existing job drops persisted keys like filter, offset, and limit. ScheduledJobService._execute_email_dynamic_query_job() still consumes those fields, so a no-op edit can widen the exported dataset or change the row count.

Suggested fix
       if (editingJobId) {
+        const existingPayload = ((jobs.find((job) => job.id === editingJobId)?.payload ??
+          {}) as Record<string, unknown>);
         await floConsoleService.scheduledJobService.updateScheduledJob(editingJobId, {
           cron_expr: cronExpr.trim(),
           timezone: timezone.trim(),
           max_retries: retries,
           payload: {
+            ...existingPayload,
             datasource_id: datasourceId,
             query_id: queryId,
             recipient_user_ids: selectedRecipientUserIds,
             subject: subject.trim() || undefined,
             email_content: emailContent.trim() || undefined,
🤖 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
`@wavefront/client/src/pages/apps/`[appId]/datasources/ScheduleEmailAlertDialog.tsx
around lines 286 - 301, The update path for
floConsoleService.scheduledJobService.updateScheduledJob(editingJobId,
{...payload...}) rebuilds payload from form fields and drops persisted keys like
filter/offset/limit; fetch the existing scheduled job (or its payload) first,
shallow-merge its payload with the new payload object so any fields not present
in the form are preserved, and then call updateScheduledJob with the merged
payload (i.e., keep existingPayload = await
floConsoleService.scheduledJobService.getScheduledJob(editingJobId) or similar,
mergedPayload = {...existingPayload.payload, ...newPayloadFields}, and pass
mergedPayload to updateScheduledJob) to ensure
_execute_email_dynamic_query_job() still receives filter/offset/limit and other
persisted keys.
🤖 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
`@wavefront/client/src/pages/apps/`[appId]/datasources/ScheduleEmailAlertDialog.tsx:
- Around line 265-279: The code currently accepts any JSON array as
parsedColumnStyles (in ScheduleEmailAlertDialog) and casts it to
ColumnStyleConfig[]; instead validate each entry before accepting: iterate
parsedColumnStyles (after JSON.parse) and ensure rule.operator is one of
'eq','neq','lt','lte','gt','gte','between' and for non-'between' rules
rule.value is a finite number and for 'between' rules rule.min and rule.max are
finite numbers (and min<=max); if any entry fails, call setError('Column styles
contain invalid rules'), setActiveTab('email') and return so malformed rules are
rejected instead of silently ignored. Ensure validation happens where
parsedColumnStyles is assigned/used and before any save/submit logic that sends
ColumnStyleConfig to the backend.

In `@wavefront/server/apps/floware/floware/services/scheduled_job_service.py`:
- Around line 737-817: The current try/except only wraps RLS and row fetch so
downstream failures (XLSX generation, cloud upload, presigned URL, send_email)
escape and cause full-job retry/duplicate sends; wrap the entire per-recipient
processing (start at calling _rls_filter_for_user and including
_fetch_dynamic_query_rows, _rows_to_xlsx_bytes,
cloud_storage_manager.save_small_file,
cloud_storage_manager.generate_presigned_url, building body/attachments, and
email_service.send_email) in a single try/except that catches Exception as exc,
logs the user-specific failure (including user_id and query_id), appends to
failed_recipient_user_ids, and continues the loop without re-raising so the job
only fails/raises downstream when every recipient is in
failed_recipient_user_ids (preserve existing delivered_count increment on
success).
- Around line 534-545: _in _resolve_fill_styles, the branch that handles
'#'‑prefixed colors currently strips the '#' into bg_hex without validating;
update that branch to validate the stripped value (bg_hex) is exactly 6 hex
characters (0-9A-F/a-f) before uppercasing and assigning, and if it fails
validation return (None, None) like the other invalid branch; ensure this same
validation logic is used consistently with the bare‑6-char branch so PatternFill
creation won't receive invalid bg_hex/font_hex values (references: function
_resolve_fill_styles, variables bg_hex and font_hex, and constants
COLUMN_FILL_COLORS / COLUMN_FILL_FONT_COLORS).

---

Outside diff comments:
In
`@wavefront/client/src/pages/apps/`[appId]/datasources/ScheduleEmailAlertDialog.tsx:
- Around line 286-301: The update path for
floConsoleService.scheduledJobService.updateScheduledJob(editingJobId,
{...payload...}) rebuilds payload from form fields and drops persisted keys like
filter/offset/limit; fetch the existing scheduled job (or its payload) first,
shallow-merge its payload with the new payload object so any fields not present
in the form are preserved, and then call updateScheduledJob with the merged
payload (i.e., keep existingPayload = await
floConsoleService.scheduledJobService.getScheduledJob(editingJobId) or similar,
mergedPayload = {...existingPayload.payload, ...newPayloadFields}, and pass
mergedPayload to updateScheduledJob) to ensure
_execute_email_dynamic_query_job() still receives filter/offset/limit and other
persisted keys.
🪄 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: d585fd44-f1cb-447c-a5d7-5cad441f9248

📥 Commits

Reviewing files that changed from the base of the PR and between b28cbb1 and 255ca14.

⛔ Files ignored due to path filters (1)
  • wavefront/server/uv.lock is excluded by !**/*.lock
📒 Files selected for processing (6)
  • wavefront/client/src/pages/apps/[appId]/datasources/ScheduleEmailAlertDialog.tsx
  • wavefront/client/src/types/scheduled-job.ts
  • wavefront/server/apps/floware/floware/di/application_container.py
  • wavefront/server/apps/floware/floware/server.py
  • wavefront/server/apps/floware/floware/services/scheduled_job_service.py
  • wavefront/server/apps/floware/pyproject.toml

Comment on lines +265 to 279
let parsedColumnStyles: ColumnStyleConfig[] | undefined;
if (columnStylesJson.trim()) {
try {
const value = JSON.parse(columnStylesJson);
if (!Array.isArray(value)) {
setError('Column styles must be a JSON array');
setActiveTab('email');
return;
}
parsedColumnStyles = value as ColumnStyleConfig[];
} catch {
setError('Column styles must be valid JSON (array)');
setActiveTab('email');
return;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Reject malformed column-style rules before save.

Any JSON array is cast to ColumnStyleConfig[] here. The backend only accepts eq/neq/lt/lte/gt/gte rules with numeric value and between rules with numeric min/max; invalid entries are silently skipped, so the dialog can report success for styles that never apply.

🤖 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
`@wavefront/client/src/pages/apps/`[appId]/datasources/ScheduleEmailAlertDialog.tsx
around lines 265 - 279, The code currently accepts any JSON array as
parsedColumnStyles (in ScheduleEmailAlertDialog) and casts it to
ColumnStyleConfig[]; instead validate each entry before accepting: iterate
parsedColumnStyles (after JSON.parse) and ensure rule.operator is one of
'eq','neq','lt','lte','gt','gte','between' and for non-'between' rules
rule.value is a finite number and for 'between' rules rule.min and rule.max are
finite numbers (and min<=max); if any entry fails, call setError('Column styles
contain invalid rules'), setActiveTab('email') and return so malformed rules are
rejected instead of silently ignored. Ensure validation happens where
parsedColumnStyles is assigned/used and before any save/submit logic that sends
ColumnStyleConfig to the backend.

Comment on lines +534 to +545
if normalized.startswith('#'):
bg_hex = normalized[1:].upper()
elif normalized.casefold() in COLUMN_FILL_COLORS:
key = normalized.casefold()
bg_hex = COLUMN_FILL_COLORS[key]
font_hex = COLUMN_FILL_FONT_COLORS.get(key)
elif len(normalized) == 6 and all(
ch in '0123456789ABCDEFabcdef' for ch in normalized
):
bg_hex = normalized.upper()
else:
return None, None
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

openpyxl PatternFill ValueError invalid hex color "Colors must be aRGB hex values"

💡 Result:

The ValueError "Colors must be aRGB hex values" in openpyxl occurs because the library strictly requires colors to be defined in aRGB hexadecimal format [1][2]. Standard 6-character hex codes (often used in web design) are rejected if they include the "#" prefix or lack the alpha (transparency) channel component [3][1]. To resolve this error, follow these requirements: 1. Remove the "#" prefix: Standard CSS-style hex codes (e.g., "#FF0000") must be stripped of the hash symbol [3]. 2. Use aRGB format: The color value must be an 8-character string representing Alpha, Red, Green, and Blue (e.g., "FFFF0000" instead of "FF0000") [1][4]. - The first two characters represent the Alpha channel (transparency). "FF" indicates full opacity (no transparency) [1][4]. Example of correct usage: from openpyxl.styles import PatternFill # Correct: 8-character hex string without '#' redFill = PatternFill(start_color="FFFF0000", end_color="FFFF0000", fill_type="solid") ws['A1'].fill = redFill Common pitfalls include: - Including the '#' character [3]. - Providing only 6 characters (RGB) instead of 8 (aRGB) [1]. - Using named colors (e.g., "red", "blue") directly in parameters where a hex string is expected [1]. If you have a standard 6-character RGB hex code (e.g., "RRGGBB"), you can convert it to the required aRGB format by prepending "FF" (e.g., "FFRRGGBB") [1][4].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="wavefront/server/apps/floware/floware/services/scheduled_job_service.py"

python3 - <<'PY'
import itertools, pathlib
p = pathlib.Path("wavefront/server/apps/floware/floware/services/scheduled_job_service.py")
lines = p.read_text().splitlines()
start, end = 500, 620
for i in range(start, end):
    if 1 <= i <= len(lines):
        print(f"{i:4d}: {lines[i-1]}")
PY

Repository: rootflo/wavefront

Length of output: 5200


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find where PatternFill is constructed / bg_hex is consumed.
rg -n "PatternFill|bg_hex" wavefront/server/apps/floware/floware/services/scheduled_job_service.py

Repository: rootflo/wavefront

Length of output: 430


🏁 Script executed:

#!/bin/bash
set -euo pipefail
FILE="wavefront/server/apps/floware/floware/services/scheduled_job_service.py"

# Find call sites of _apply_cell_style and _resolve_fill_styles plus surrounding try/except
rg -n "_apply_cell_style\(|_resolve_fill_styles\(|PatternFill\(" "$FILE"

# Also find try/except in vicinity of _rows_to_xlsx_bytes
rg -n "def _rows_to_xlsx_bytes|try:|except " "$FILE" -n

Repository: rootflo/wavefront

Length of output: 990


🏁 Script executed:

#!/bin/bash
set -euo pipefail
FILE="wavefront/server/apps/floware/floware/services/scheduled_job_service.py"

# Print the full _rows_to_xlsx_bytes method area
python3 - <<'PY'
import pathlib, re
p=pathlib.Path("wavefront/server/apps/floware/floware/services/scheduled_job_service.py")
lines=p.read_text().splitlines()
# find start line for _rows_to_xlsx_bytes
for idx,l in enumerate(lines, start=1):
    if "def _rows_to_xlsx_bytes" in l:
        start=idx
        break
else:
    raise SystemExit("not found")
# print next 220 lines or until next def at same indent
out=[]
indent=len(lines[start-1]) - len(lines[start-1].lstrip())
for j in range(start, min(start+260,len(lines))+1):
    out.append((j,lines[j-1]))
# just output with line numbers but limit length
for j,l in out[:260]:
    print(f"{j:4d}: {l}")
PY

Repository: rootflo/wavefront

Length of output: 12651


🏁 Script executed:

#!/bin/bash
set -euo pipefail
FILE="wavefront/server/apps/floware/floware/services/scheduled_job_service.py"
# Locate COLUMN_FILL_COLORS constant definitions and check format
rg -n "COLUMN_FILL_COLORS|COLUMN_FILL_FONT_COLORS" "$FILE"
python3 - <<'PY'
import pathlib
p=pathlib.Path("wavefront/server/apps/floware/floware/services/scheduled_job_service.py")
lines=p.read_text().splitlines()
for i,l in enumerate(lines, start=1):
    if "COLUMN_FILL_COLORS" in l:
        start=i-5
        break
else:
    raise SystemExit
for j in range(start, start+120):
    if 1<=j<=len(lines):
        print(f"{j:4d}: {lines[j-1]}")
PY

Repository: rootflo/wavefront

Length of output: 5991


🏁 Script executed:

#!/bin/bash
set -euo pipefail
FILE="wavefront/server/apps/floware/floware/services/scheduled_job_service.py"

# Find call sites of _apply_cell_style and _resolve_fill_styles plus surrounding PatternFill usage
rg -n "_apply_cell_style\(|_resolve_fill_styles\(|PatternFill\(" "$FILE"

Repository: rootflo/wavefront

Length of output: 405


🏁 Script executed:

#!/bin/bash
set -euo pipefail
FILE="wavefront/server/apps/floware/floware/services/scheduled_job_service.py"

# Print the area covering _rows_to_xlsx_bytes to check exception handling
python3 - <<'PY'
import pathlib
p=pathlib.Path("wavefront/server/apps/floware/floware/services/scheduled_job_service.py")
lines=p.read_text().splitlines()

start=None
for idx,l in enumerate(lines, start=1):
    if "def _rows_to_xlsx_bytes" in l:
        start=idx
        break
if start is None:
    raise SystemExit("not found")

for j in range(start, min(start+260, len(lines))+1):
    print(f"{j:4d}: {lines[j-1]}")
PY

Repository: rootflo/wavefront

Length of output: 12697


🏁 Script executed:

#!/bin/bash
set -euo pipefail
FILE="wavefront/server/apps/floware/floware/services/scheduled_job_service.py"

# Locate COLUMN_FILL_COLORS / COLUMN_FILL_FONT_COLORS definitions to see expected color format
rg -n "COLUMN_FILL_COLORS|COLUMN_FILL_FONT_COLORS" "$FILE"

python3 - <<'PY'
import pathlib
p=pathlib.Path("wavefront/server/apps/floware/floware/services/scheduled_job_service.py")
lines=p.read_text().splitlines()

hits=[]
for i,l in enumerate(lines, start=1):
    if "COLUMN_FILL_COLORS" in l or "COLUMN_FILL_FONT_COLORS" in l:
        hits.append(i)

print("hits:", hits)
if hits:
    start=max(1, min(hits)-10)
    end=min(len(lines), max(hits)+140)
    for j in range(start, end+1):
        print(f"{j:4d}: {lines[j-1]}")
PY

Repository: rootflo/wavefront

Length of output: 29214


🏁 Script executed:

#!/bin/bash
set -euo pipefail
python3 - <<'PY'
try:
    from openpyxl.styles import PatternFill
except Exception as e:
    print("openpyxl import failed:", e)
    raise

def try_fill(val):
    try:
        pf = PatternFill(start_color=val, end_color=val, fill_type="solid")
        # openpyxl stores normalized values internally; just prove no exception
        print("OK ", val, "->", getattr(pf, "start_color", None))
    except Exception as e:
        print("ERR", val, "->", type(e).__name__, str(e)[:200])

tests = [
    "FFFF0000",  # ARGB 8
    "FF0000",    # RGB 6
    "FFF",       # RGB shorthand 3
    "GGGGGG",    # invalid
    "12345G",    # invalid char
    "00FF00",    # RGB 6
    "C6EFCE",    # matches COLUMN_FILL_COLORS (6)
    "006100",    # matches COLUMN_FILL_COLORS (6)
    "FFFFFFFF",  # ARGB 8
]
for t in tests:
    try_fill(t)
PY

Repository: rootflo/wavefront

Length of output: 251


🌐 Web query:

openpyxl PatternFill start_color "Colors must be aRGB hex values" accepts RRGGBB length 6

💡 Result:

In openpyxl, the PatternFill start_color and bgColor attributes require aRGB hex values [1][2][3]. While the library technically validates that the input string matches a regex for either 6 or 8 hexadecimal characters, it handles 6-character strings (RRGGBB) by automatically prepending "00" to them [4][3]. Key technical points: 1. Format Requirements: The library expects aRGB hex strings. If you provide a standard 6-character hex code (e.g., "RRGGBB"), openpyxl treats it as an aRGB value where the alpha channel defaults to "00" (resulting in "00RRGGBB") [4][3]. 2. Troubleshooting "ValueError": This error often occurs when users include a hash symbol (#) in their hex string [5]. The value provided must be a clean string of hexadecimal characters only (e.g., use "FFFFFF" instead of "#FFFFFF") [5]. 3. Alpha Channel: While the format is technically aRGB, the alpha channel value is generally not relevant for basic cell styles in Excel; the prepended "00" is handled internally [3]. Example of correct usage: from openpyxl.styles import PatternFill # Correct: No '#' prefix, 6 or 8 chars are acceptable fill = PatternFill(start_color="FF0000", fill_type="solid") [6]

Citations:


🌐 Web query:

openpyxl Color rgb "FFF" PatternFill start_color

💡 Result:

In openpyxl, the PatternFill class is used to apply background patterns or solid colors to cells [1][2]. When specifying a color such as white ("FFF"), you typically provide it as a hexadecimal string to the start_color argument [3]. Key points regarding PatternFill and color usage: 1. Color Format: Colors are represented as 6-character hexadecimal RGB strings (e.g., "FFFFFF" for white) [3][4]. While "FFF" is often accepted in some web contexts, it is best practice to use the full 6-character format ("FFFFFF") to ensure compatibility with openpyxl's color handling [3]. 2. start_color vs. fgColor: In the PatternFill constructor, start_color is an alias for fgColor [5]. Both parameters achieve the same result. 3. Required Parameters: For a color to be visible, you must explicitly set a fill_type (e.g., "solid") [6][1]. If fill_type is not provided or set to None, the color attributes will have no effect [1][2]. Example usage: from openpyxl.styles import PatternFill # Using 'solid' fill type with white color (FFFFFF) fill = PatternFill(start_color="FFFFFF", fill_type="solid") cell.fill = fill Note: If you are using a pattern other than "solid", the start_color typically defines the foreground color of the pattern, and end_color (an alias for bgColor) defines the background color [6][5].

Citations:


🌐 Web query:

openpyxl styles colors validate hex length regex 6 8 3

💡 Result:

In openpyxl, color validation is handled internally by the library, primarily through the RGB descriptor found in openpyxl.styles.colors [1][2][3]. The validation logic uses the following regular expression: ^([A-Fa-f0-9]{8}|[A-Fa-f0-9]{6})$ This means openpyxl explicitly supports and validates hexadecimal color strings of length 6 (RGB) and 8 (aRGB) [1][2][3]. Hexadecimal strings of length 3 (e.g., #FFF) are not natively supported by this regex and will trigger a ValueError if passed directly as an aRGB value [1][2][3]. Key behavior details: 1. Regex Validation: If a value does not match the 6 or 8 character requirement, a ValueError is raised with the message "Colors must be aRGB hex values" [1][2][3]. 2. Automatic Alpha Handling: If a 6-character hex string is provided, openpyxl automatically prepends "00" to it, converting it to an 8-character aRGB string (where "00" represents the alpha/transparency channel) [1][4][2][3]. 3. No native 3-character support: Unlike some web-based CSS color parsers, openpyxl does not automatically expand or parse 3-character shorthand hex codes [1][2][3]. You must convert 3-character hex codes to their 6-character equivalents (e.g., doubling each digit) before assigning them to styles [4][5].

Citations:


Validate #-prefixed fill colors before building PatternFill.

_resolve_fill_styles currently strips # and uppercases without checking length or hex characters (unlike the bare 6-char branch). That lets inputs like #FFF (3 chars) or #GGGGGG through, and PatternFill(start_color=bg_hex, ...) will raise ValueError: Colors must be aRGB hex values, aborting the scheduled report generation for the job/user.

🛡️ Proposed validation
-        if normalized.startswith('#'):
-            bg_hex = normalized[1:].upper()
+        if normalized.startswith('#'):
+            candidate = normalized[1:].upper()
+            if len(candidate) in (6, 8) and all(
+                ch in '0123456789ABCDEF' for ch in candidate
+            ):
+                bg_hex = candidate
+            else:
+                return None, None
         elif normalized.casefold() in COLUMN_FILL_COLORS:
             key = normalized.casefold()
             bg_hex = COLUMN_FILL_COLORS[key]
             font_hex = COLUMN_FILL_FONT_COLORS.get(key)
         elif len(normalized) == 6 and all(
             ch in '0123456789ABCDEFabcdef' for ch in normalized
         ):
             bg_hex = normalized.upper()
         else:
             return None, None
🤖 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 `@wavefront/server/apps/floware/floware/services/scheduled_job_service.py`
around lines 534 - 545, _in _resolve_fill_styles, the branch that handles
'#'‑prefixed colors currently strips the '#' into bg_hex without validating;
update that branch to validate the stripped value (bg_hex) is exactly 6 hex
characters (0-9A-F/a-f) before uppercasing and assigning, and if it fails
validation return (None, None) like the other invalid branch; ensure this same
validation logic is used consistently with the bare‑6-char branch so PatternFill
creation won't receive invalid bg_hex/font_hex values (references: function
_resolve_fill_styles, variables bg_hex and font_hex, and constants
COLUMN_FILL_COLORS / COLUMN_FILL_FONT_COLORS).

Comment on lines +737 to +817
try:
rls_filter_str = await self._rls_filter_for_user(user_id)
rows = await self._fetch_dynamic_query_rows(
datasource_plugin,
yaml_query,
query_id,
rls_filter_str,
filter_expr,
offset,
limit,
params,
)
except Exception as exc:
logger.error(
f'Scheduled report failed for user_id={user_id}, query_id={query_id}: {exc}'
)
failed_recipient_user_ids.append(user_id)
continue

if len(rows) == 0:
start_key = str(payload.get('start_date_param', 'start_date'))
end_key = str(payload.get('end_date_param', 'end_date'))
applied_start = (
params.get(start_key) if isinstance(params, dict) else None
)
applied_end = params.get(end_key) if isinstance(params, dict) else None
logger.info(
f'No records for user_id={user_id}, query_id={query_id}; '
f'range {applied_start}..{applied_end} (keys: {start_key}, {end_key}). '
'Skipping email.'
)
continue

report_bytes, _ = self._rows_to_xlsx_bytes(
rows, column_styles=column_styles
)
report_size = len(report_bytes)
report_filename = f'{query_id}_{user_id}_{run_timestamp}_report.xlsx'
report_url: str | None = None
use_attachment = report_size <= MAX_EMAIL_ATTACHMENT_BYTES
if not use_attachment:
report_key = f'scheduled_query_reports/{query_id}/{report_filename}'
self.cloud_storage_manager.save_small_file(
file_content=report_bytes,
bucket_name=self.bucket_name,
key=report_key,
content_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
)
report_url = self.cloud_storage_manager.generate_presigned_url(
bucket_name=self.bucket_name,
key=report_key,
type='GET',
expiresIn=SIGNED_URL_EXPIRY_SECONDS,
)

body = self._build_report_email_body(
report_name=yaml_name or query_id,
email_content=email_content,
report_size=report_size,
use_attachment=use_attachment,
report_url=report_url,
)
attachments = None
if use_attachment:
attachments = [
{
'filename': report_filename,
'content_bytes': csv_bytes,
'mime_type': 'text/csv',
'content_bytes': report_bytes,
'mime_type': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
}
]
is_sent = self.email_service.send_email(
subject,
body,
recipient,
user.email,
attachments=attachments,
)
if not is_sent:
failed_recipients.append(recipient)
failed_recipient_user_ids.append(user_id)
else:
delivered_count += 1
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Per-recipient try/except is too narrow — delivery failures fail the whole job and cause duplicate emails on retry.

The except only covers RLS resolution and the row fetch (Lines 737-754). XLSX generation (_rows_to_xlsx_bytes), the cloud upload (save_small_file), presigned-URL generation, and send_email all run unguarded. If any of these throws for one recipient, the exception escapes the loop and _run_job marks the job failed and reschedules it. On retry the loop restarts from the first recipient, re-sending to everyone who already received their report. This also contradicts the intended "raise only when all recipients fail" semantic. Wrap the entire per-recipient body so a single failure is isolated.

🛠️ Isolate per-recipient delivery failures
             try:
                 rls_filter_str = await self._rls_filter_for_user(user_id)
                 rows = await self._fetch_dynamic_query_rows(
                     datasource_plugin,
                     yaml_query,
                     query_id,
                     rls_filter_str,
                     filter_expr,
                     offset,
                     limit,
                     params,
                 )
-            except Exception as exc:
-                logger.error(
-                    f'Scheduled report failed for user_id={user_id}, query_id={query_id}: {exc}'
-                )
-                failed_recipient_user_ids.append(user_id)
-                continue

-            if len(rows) == 0:
-                ...
-                continue
-
-            report_bytes, _ = self._rows_to_xlsx_bytes(
-                rows, column_styles=column_styles
-            )
-            ...
-            is_sent = self.email_service.send_email(
-                subject,
-                body,
-                user.email,
-                attachments=attachments,
-            )
-            if not is_sent:
-                failed_recipient_user_ids.append(user_id)
-            else:
-                delivered_count += 1
+                if len(rows) == 0:
+                    ...
+                    continue
+
+                report_bytes, _ = self._rows_to_xlsx_bytes(
+                    rows, column_styles=column_styles
+                )
+                ...
+                is_sent = self.email_service.send_email(
+                    subject,
+                    body,
+                    user.email,
+                    attachments=attachments,
+                )
+                if not is_sent:
+                    failed_recipient_user_ids.append(user_id)
+                else:
+                    delivered_count += 1
+            except Exception as exc:
+                logger.error(
+                    f'Scheduled report failed for user_id={user_id}, query_id={query_id}: {exc}'
+                )
+                failed_recipient_user_ids.append(user_id)
+                continue

Note: even with isolation, send_email is not idempotent, so a recipient who fails after a successful send (e.g., a transient post-send error) could still be re-delivered if the whole-job path is ever hit. Keeping all per-recipient failures non-propagating (so the job only raises when every recipient fails) is what prevents the duplicate-send retry loop.

🧰 Tools
🪛 Ruff (0.15.14)

[warning] 749-749: Do not catch blind exception: Exception

(BLE001)

🤖 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 `@wavefront/server/apps/floware/floware/services/scheduled_job_service.py`
around lines 737 - 817, The current try/except only wraps RLS and row fetch so
downstream failures (XLSX generation, cloud upload, presigned URL, send_email)
escape and cause full-job retry/duplicate sends; wrap the entire per-recipient
processing (start at calling _rls_filter_for_user and including
_fetch_dynamic_query_rows, _rows_to_xlsx_bytes,
cloud_storage_manager.save_small_file,
cloud_storage_manager.generate_presigned_url, building body/attachments, and
email_service.send_email) in a single try/except that catches Exception as exc,
logs the user-specific failure (including user_id and query_id), appends to
failed_recipient_user_ids, and continues the loop without re-raising so the job
only fails/raises downstream when every recipient is in
failed_recipient_user_ids (preserve existing delivered_count increment on
success).

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
wavefront/client/src/api/app-user-service.ts (1)

25-27: ⚡ Quick win

listAppUsers()’s :appId placeholder is already substituted by the Axios request interceptor

  • wavefront/client/src/api/app-user-service.ts sends /v1/:appId/floware/v1/users, and wavefront/client/src/lib/axios.ts replaces :appId in config.url using window.location.pathname.split('/')[2] before the request is issued.
  • Interceptor behavior depends on the current path matching /apps/:appId/... (otherwise it may replace with '').
🤖 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 `@wavefront/client/src/api/app-user-service.ts` around lines 25 - 27, The
request URL in listAppUsers() relies on an Axios interceptor in lib/axios.ts to
replace the :appId token, which is fragile if the current path doesn't match
/apps/:appId/; make listAppUsers() build the URL explicitly by extracting appId
= window.location.pathname.split('/')[2] (with a safe fallback) and using
`/v1/${appId}/floware/v1/users` so the service no longer depends on the
interceptor's runtime replacement; update listAppUsers() accordingly and keep
the interceptor as-is for other callers.
🤖 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.

Nitpick comments:
In `@wavefront/client/src/api/app-user-service.ts`:
- Around line 25-27: The request URL in listAppUsers() relies on an Axios
interceptor in lib/axios.ts to replace the :appId token, which is fragile if the
current path doesn't match /apps/:appId/; make listAppUsers() build the URL
explicitly by extracting appId = window.location.pathname.split('/')[2] (with a
safe fallback) and using `/v1/${appId}/floware/v1/users` so the service no
longer depends on the interceptor's runtime replacement; update listAppUsers()
accordingly and keep the interceptor as-is for other callers.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: cc7abf0b-d69a-4f7d-b22d-54d7df93f71c

📥 Commits

Reviewing files that changed from the base of the PR and between 255ca14 and 2ee9ae6.

📒 Files selected for processing (9)
  • wavefront/client/src/api/app-user-service.ts
  • wavefront/client/src/hooks/data/fetch-hooks.ts
  • wavefront/client/src/hooks/data/mutation-hooks.ts
  • wavefront/client/src/hooks/data/query-functions.ts
  • wavefront/client/src/hooks/data/query-keys.ts
  • wavefront/client/src/pages/apps/[appId]/datasources/ScheduleEmailAlertDialog.tsx
  • wavefront/client/src/pages/apps/[appId]/datasources/[datasourceId].tsx
  • wavefront/client/src/pages/apps/users/index.tsx
  • wavefront/server/apps/floware/floware/services/scheduled_job_service.py
🚧 Files skipped from review as they are similar to previous changes (2)
  • wavefront/server/apps/floware/floware/services/scheduled_job_service.py
  • wavefront/client/src/pages/apps/[appId]/datasources/ScheduleEmailAlertDialog.tsx

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.

1 participant