feat(floware): change to OAuth flow from DWD for cron job#291
feat(floware): change to OAuth flow from DWD for cron job#291vishnurk6247 wants to merge 4 commits into
Conversation
📝 WalkthroughWalkthroughGmail 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. ChangesGmail OAuth Migration
Scheduled Job XLSX + Recipient RLS
Sequence DiagramsequenceDiagram
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
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 winPreserve existing payload fields on update.
This update path reconstructs
payloadfrom a subset of form fields, so editing an existing job drops persisted keys likefilter,offset, andlimit.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
⛔ Files ignored due to path filters (1)
wavefront/server/uv.lockis excluded by!**/*.lock
📒 Files selected for processing (6)
wavefront/client/src/pages/apps/[appId]/datasources/ScheduleEmailAlertDialog.tsxwavefront/client/src/types/scheduled-job.tswavefront/server/apps/floware/floware/di/application_container.pywavefront/server/apps/floware/floware/server.pywavefront/server/apps/floware/floware/services/scheduled_job_service.pywavefront/server/apps/floware/pyproject.toml
| 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; | ||
| } |
There was a problem hiding this comment.
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.
| 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 |
There was a problem hiding this comment.
🧩 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:
- 1: https://www.mr22.com.tw/vertic328/openpyxl-colors-must-be-argb-hex-values1
- 2: https://wenku.csdn.net/answer/2hbbhvqaymi1
- 3: https://stackoverflow.com/questions/76017371/changing-background-color-in-excel-results-in-a-valueerror
- 4: https://stackoverflow.com/questions/30484220/fill-cells-with-colors-using-openpyxl
🏁 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]}")
PYRepository: 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.pyRepository: 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" -nRepository: 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}")
PYRepository: 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]}")
PYRepository: 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]}")
PYRepository: 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]}")
PYRepository: 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)
PYRepository: 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:
- 1: https://openpyxl.readthedocs.io/en/stable/api/openpyxl.styles.fills.html?highlight=patternfill
- 2: https://openpyxl.readthedocs.io/en/stable/_modules/openpyxl/styles/fills.html
- 3: https://openpyxl.readthedocs.io/en/3.1/styles.html
- 4: https://openpyxl.readthedocs.io/en/3.1/%5Fmodules/openpyxl/styles/colors.html
- 5: https://stackoverflow.com/questions/76017371/changing-background-color-in-excel-results-in-a-valueerror
- 6: https://stackoverflow.com/questions/30484220/fill-cells-with-colors-using-openpyxl
🌐 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:
- 1: https://openpyxl.readthedocs.io/en/stable/api/openpyxl.styles.fills.html?highlight=patternfill
- 2: https://openpyxl.readthedocs.io/en/3.1/api/openpyxl.styles.fills.html
- 3: https://openpyxl.readthedocs.io/en/3.1/styles.html
- 4: https://blog.csdn.net/spencer_tseng/article/details/132776834
- 5: https://openpyxl.readthedocs.io/en/stable/%5Fmodules/openpyxl/styles/fills.html
- 6: https://stackoverflow.com/questions/40926715/what-is-a-start-color-and-end-color-in-openpyxl
🌐 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:
- 1: https://openpyxl.readthedocs.io/en/stable/_modules/openpyxl/styles/colors.html
- 2: https://openpyxl.readthedocs.io/en/latest/_modules/openpyxl/styles/colors.html
- 3: https://openpyxl.readthedocs.io/en/3.1/%5Fmodules/openpyxl/styles/colors.html
- 4: https://openpyxl.readthedocs.io/en/3.1/styles.html
- 5: https://dev.to/danielledvina/road-to-senior-how-computers-think-in-numbers-1mec
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).
| 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 |
There was a problem hiding this comment.
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)
+ continueNote: even with isolation,
send_emailis 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).
There was a problem hiding this comment.
🧹 Nitpick comments (1)
wavefront/client/src/api/app-user-service.ts (1)
25-27: ⚡ Quick win
listAppUsers()’s:appIdplaceholder is already substituted by the Axios request interceptor
wavefront/client/src/api/app-user-service.tssends/v1/:appId/floware/v1/users, andwavefront/client/src/lib/axios.tsreplaces:appIdinconfig.urlusingwindow.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
📒 Files selected for processing (9)
wavefront/client/src/api/app-user-service.tswavefront/client/src/hooks/data/fetch-hooks.tswavefront/client/src/hooks/data/mutation-hooks.tswavefront/client/src/hooks/data/query-functions.tswavefront/client/src/hooks/data/query-keys.tswavefront/client/src/pages/apps/[appId]/datasources/ScheduleEmailAlertDialog.tsxwavefront/client/src/pages/apps/[appId]/datasources/[datasourceId].tsxwavefront/client/src/pages/apps/users/index.tsxwavefront/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
Summary by CodeRabbit
Configuration Changes
New Features
Behavior Changes
UI