Skip to content

Add fournos-ui dashboard - #97

Merged
kpouget merged 6 commits into
openshift-psap:mainfrom
ashtarkb:add-fournos-ui
Jul 13, 2026
Merged

Add fournos-ui dashboard#97
kpouget merged 6 commits into
openshift-psap:mainfrom
ashtarkb:add-fournos-ui

Conversation

@ashtarkb

@ashtarkb ashtarkb commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

Web dashboard for managing Fournos performance testing jobs. Provides job submission, live monitoring, scheduling, and historical results -- built with FastAPI, HTMX, and PostgreSQL.

Summary by CodeRabbit

  • New Features

    • Added a full dashboard experience for submitting jobs, viewing live status, browsing history, and managing schedules.
    • Introduced live log streaming, job rerun/cancel actions, and schedule trigger/pause/resume controls.
    • Added project lookup, preset selection, and optional version/PR autofill in the submit flow.
  • Bug Fixes

    • Improved job and schedule details with clearer status, progress, and history displays.
    • Added support for archived job records and schedule run history.

Web dashboard for managing Fournos performance testing jobs.
Provides job submission, live monitoring, scheduling, and
historical results -- built with FastAPI, HTMX, and PostgreSQL.

Co-authored-by: Cursor <cursoragent@cursor.com>
@openshift-ci

openshift-ci Bot commented Jul 5, 2026

Copy link
Copy Markdown

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by:
Once this PR has been reviewed and has the lgtm label, please assign tosokin for approval. For more information see the Code Review Process.

The full list of commands accepted by this bot can be found here.

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@coderabbitai

coderabbitai Bot commented Jul 5, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@ashtarkb, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 48 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b8bf8c5a-3cf8-4327-b55b-afaba43f1ba7

📥 Commits

Reviewing files that changed from the base of the PR and between a396a53 and 58407c3.

📒 Files selected for processing (1)
  • fournos-ui/app/main.py
📝 Walkthrough

Walkthrough

This PR introduces a complete new "fournos-ui" FastAPI dashboard application for monitoring, submitting, and scheduling FournosJob custom resources. It includes PostgreSQL persistence, Kubernetes/Tekton API integration, a background watcher, Forge project discovery, HTMX/Jinja templates, static assets, Dockerfile, and Kustomize deployment manifests.

Changes

Fournos Dashboard Application

Layer / File(s) Summary
Project scaffolding and settings
fournos-ui/.dockerignore, fournos-ui/.gitignore, fournos-ui/Dockerfile, fournos-ui/README.md, fournos-ui/app/__init__.py, fournos-ui/app/config.py, fournos-ui/requirements.txt
Adds ignore files, a non-root Docker build, README documentation, package init, requirements, and an environment-driven Settings dataclass covering DB/K8s/Forge/Tekton configuration.
Data models and PostgreSQL persistence
fournos-ui/app/models.py, fournos-ui/app/db.py
Defines ProjectInfo and async SQLAlchemy Job/JobEvent models with engine/session setup and upsert/list/delete/event query helpers.
Forge project discovery
fournos-ui/app/forge_discovery.py
Discovers project metadata from a Forge repo directory or ConfigMap YAML fallback, with caching and lookup helpers.
Kubernetes and Tekton client wrapper
fournos-ui/app/k8s_client.py
Implements FournosJob CRUD/watch, Tekton PipelineRun/TaskRun/stage extraction, pod log streaming, and managed CronJob/resolver ConfigMap operations.
Background job watcher
fournos-ui/app/watcher.py
Runs a daemon watcher that performs full syncs and streams K8s watch events to archive job state/events into PostgreSQL.
FastAPI app and routes
fournos-ui/app/main.py
Defines app lifespan, template helpers, jobs list/detail/cancel/rerun routes, submit form/creation, and schedule CRUD/trigger/delete endpoints.
Jinja dashboard pages
fournos-ui/app/templates/*
Adds base layout, jobs list, job detail, submit form, schedule runs, and schedules templates with HTMX polling and client-side JS interactions.
HTMX SSE and dashboard styling
fournos-ui/app/static/htmx-sse.js, fournos-ui/app/static/style.css
Adds an HTMX SSE extension for streaming updates and a full dark-theme stylesheet.
Kubernetes manifests and overlays
fournos-ui/kustomize/base/*, fournos-ui/kustomize/overlays/ocp/*
Adds namespace, RBAC, deployment, services, PostgreSQL StatefulSet, projects ConfigMap, and an OpenShift overlay example.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Browser
  participant FastAPI
  participant Kubernetes
  participant PostgreSQL
  Browser->>FastAPI: submit job or open job detail
  FastAPI->>Kubernetes: create FournosJob or fetch live job data
  FastAPI->>PostgreSQL: upsert history row on submit
  FastAPI->>Kubernetes: stream logs or resolve PipelineRun data
Loading
sequenceDiagram
  participant Watcher
  participant Kubernetes
  participant PostgreSQL
  Watcher->>Kubernetes: list/watch FournosJob resources
  Watcher->>PostgreSQL: upsert jobs and append job events
  Watcher->>Kubernetes: retry watch after stream errors
Loading

Possibly related PRs

  • openshift-psap/fournos#21: The dashboard's job status/condition rendering displays fields (status.message, conditions) produced by this related PR's operator/CRD changes.
  • openshift-psap/fournos#44: The dashboard's shutdown_fournos_job/cancel path patches spec.shutdown, aligning with this related PR's CRD/operator implementation of the shutdown/Stop mechanism.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: adding the fournos-ui dashboard.
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

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: 17

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

Caution

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

⚠️ Outside diff range comments (1)
fournos-ui/README.md (1)

109-125: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Directory name typo / mismatch, and missing code-fence language.

fournous_UI/ doesn't match the actual project directory (fournos-ui/), which could confuse readers navigating the repo. Also flagged by markdownlint (MD040) for missing fence language.

Fix
-```
+```text
-fournous_UI/
+fournos-ui/
 ├── app/
🤖 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 `@fournos-ui/README.md` around lines 109 - 125, The repository tree in README
has a directory-name typo and the code fence is missing a language annotation.
Update the rendered tree in the README’s project structure section to use the
correct top-level name referenced by the `fournos-ui` project, and change the
fenced block to a language-tagged fence so it satisfies markdownlint MD040; use
the existing README tree snippet as the location to fix.

Source: Linters/SAST tools

🟡 Minor comments (6)
fournos-ui/README.md-58-64 (1)

58-64: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Missing content under "Build and push the dashboard image".

Steps 1, 3, and 4 include concrete commands, but step 2 has no docker build/push example, leaving a gap in the setup instructions.

🤖 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 `@fournos-ui/README.md` around lines 58 - 64, The “Build and push the dashboard
image” step is missing the actual command examples, leaving the setup guide
incomplete. Update the README section under that heading to include a concrete
build and push flow, using the same style as the surrounding steps and
referencing the dashboard image workflow so readers can follow it without
guessing.
fournos-ui/README.md-15-25 (1)

15-25: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Add a language to the fenced code block.

Static analysis (markdownlint MD040) flags this diagram block as missing a language tag.

Fix
-```
+```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 `@fournos-ui/README.md` around lines 15 - 25, The fenced diagram block in
README is missing a language tag, which triggers markdownlint MD040. Update the
existing markdown fence for the architecture diagram by adding an appropriate
language identifier (for example, text) while keeping the diagram content
unchanged, and make sure the fenced block in the README remains valid markdown.

Source: Linters/SAST tools

fournos-ui/app/static/style.css-16-16 (1)

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

Fix the Stylelint value-keyword-case violations.

Stylelint flags the unquoted font-family names and currentColor. Quoting font names preserves intent while satisfying the rule.

Proposed fix
-    --font-sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
+    --font-sans: -apple-system, "BlinkMacSystemFont", 'Segoe UI', "Helvetica", "Arial", sans-serif;
@@
-    background: currentColor;
+    background: currentcolor;

Also applies to: 256-256

🤖 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 `@fournos-ui/app/static/style.css` at line 16, Stylelint is flagging
`value-keyword-case` for the font family values in `style.css`; update the
`--font-sans` declaration and the other affected `font-family` usages in this
file so the font names are quoted consistently. Also review any `currentColor`
occurrences mentioned in the comment and adjust them to satisfy the same rule
while keeping the intended styling intact.

Source: Linters/SAST tools

fournos-ui/app/templates/submit_job.html-98-100 (1)

98-100: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Encode the project name before building the API URL.

Project names containing path/query characters will call the wrong route. Also check resp.ok so failed lookups don’t look like empty preset lists.

Proposed fix
-        const resp = await fetch('/api/project-info/' + projectName);
+        const resp = await fetch('/api/project-info/' + encodeURIComponent(projectName));
+        if (!resp.ok) {
+            throw new Error('Failed to load project info');
+        }
         const data = await resp.json();
🤖 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 `@fournos-ui/app/templates/submit_job.html` around lines 98 - 100, The
project-info fetch in submit_job.html builds the API URL with the raw
projectName, which can break routing for names containing path or query
characters and can treat failed requests as empty results. Update the fetch call
in the submit-job flow to URL-encode projectName before concatenating it into
the /api/project-info/ request, and add a resp.ok check in the same async block
so non-200 responses are handled as failures rather than parsed as empty preset
data.
fournos-ui/app/templates/schedules.html-227-237 (1)

227-237: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Serialize cronjobData with tojson. Jinja/HTML escaping is not valid JavaScript serialization here, so values like O'Brien can be mangled or break the script. Use tojson for the object keys and all embedded fields, including the booleans.

🤖 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 `@fournos-ui/app/templates/schedules.html` around lines 227 - 237,
`cronjobData` is being built with Jinja string interpolation, which can produce
invalid JavaScript and break on quoted values; update the template in the
schedules view to serialize the full object with `tojson` instead of manually
quoting fields. Use the existing `cronjobData` block and ensure every embedded
property, including `has_resolver` and `suspend`, is emitted through proper JSON
serialization so JavaScript receives a valid object literal.
fournos-ui/app/templates/jobs_list.html-45-46 (1)

45-46: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Filter values aren't URL-encoded before insertion into query strings.

filters.owner/filters.cluster are free-text inputs (lines 38-41). If a value contains &, #, %, or spaces, the hx-get URL (line 45) and pagination links (lines 166, 170) will produce a malformed/incorrect query string. Use Jinja's urlencode filter for these values.

🐛 Suggested fix
-<div hx-get="/api/jobs-table?project={{ filters.project }}&cluster={{ filters.cluster }}&status={{ filters.status }}&owner={{ filters.owner }}"
+<div hx-get="/api/jobs-table?project={{ filters.project | urlencode }}&cluster={{ filters.cluster | urlencode }}&status={{ filters.status | urlencode }}&owner={{ filters.owner | urlencode }}"

Apply the same | urlencode treatment to the pagination href values at Line 166 and Line 170.

Also applies to: 166-166, 170-170

🤖 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 `@fournos-ui/app/templates/jobs_list.html` around lines 45 - 46, The job list
template is building query strings from free-text filter values without URL
encoding, so special characters can break the requests. Update the hx-get URL in
jobs_list.html to pass filters.project, filters.cluster, filters.status, and
filters.owner through Jinja’s urlencode filter, and apply the same urlencode
treatment to the pagination hrefs in the jobs table controls. Use the existing
hx-get block and the pagination link markup as the locations to fix.
🧹 Nitpick comments (8)
fournos-ui/app/db.py (2)

140-164: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Consider indexing created_at for paginated history queries.

list_jobs sorts by Job.created_at.desc() with limit/offset pagination, and per the referenced main.py snippet this backs the frequently-hit history tab. Without an index on created_at, this will require a full table scan/sort as the jobs table grows.

⚡ Proposed index
-    created_at = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc))
+    created_at = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), index=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 `@fournos-ui/app/db.py` around lines 140 - 164, The list_jobs query in the Job
history path is ordering by Job.created_at.desc() with limit/offset pagination,
so add an index on created_at to avoid full scans and sorts as the jobs table
grows. Update the Job model/schema with an index on created_at (ideally
alongside any existing filters if appropriate), and ensure the change is
reflected in the database migration so list_jobs continues to return the same
results while using the index.

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

Alembic dependency appears unused.

init_db() notes "use Alembic in production" and alembic is listed in requirements.txt, but there's no visible migration setup (alembic.ini, migrations/ dir) in this batch. Either wire up Alembic migrations for production use or drop the unused dependency to avoid confusion about the actual schema-management strategy.

🤖 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 `@fournos-ui/app/db.py` around lines 85 - 90, The schema-management setup is
inconsistent: init_db() in db.py still creates tables directly, while alembic is
present but there is no visible migration wiring. Either add the missing Alembic
migration setup and switch production schema changes to use it, or remove the
Alembic dependency and update the init_db() docstring/logging to reflect direct
table creation only. Use init_db(), Base.metadata.create_all, and the alembic
dependency reference to locate the related code.
fournos-ui/app/k8s_client.py (1)

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

Only ApiException is caught; other transport errors will propagate.

Every read/list function in this module follows a "catch ApiException, return a safe default" pattern, but transport-level failures (e.g. urllib3.exceptions.MaxRetryError, connection reset) aren't ApiException and will bubble up uncaught, breaking the module's otherwise consistent defensive contract.

Also applies to: 82-100, 196-215, 333-376

🤖 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 `@fournos-ui/app/k8s_client.py` around lines 63 - 79, The list/read helpers in
k8s_client.py only handle ApiException, so transport failures can still escape
and break the module’s safe-default behavior. Update list_fournos_jobs and the
other affected read/list functions referenced in this module to also catch
underlying transport/connection exceptions (for example urllib3/requests errors
used by the Kubernetes client) alongside ApiException, log the failure, and
return the same empty/default value. Keep the existing defensive pattern
consistent across these functions such as list_fournos_jobs and the other
read/list helpers.
fournos-ui/kustomize/overlays/ocp/kustomization.yaml.example (1)

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

Index-based JSON6902 patches are fragile against base changes.

The FOURNOS_NAMESPACE patch targets containers/0/env/6/value by numeric index; any reordering/addition of env vars in the base dashboard-deployment.yaml silently breaks this overlay (wrong var patched) without any error. The image patches (containers/0/image, initContainers/0/image) have the same fragility, though container-index changes are less likely.

Consider a strategic-merge patch instead, which matches array elements by name (the env list's merge key):

-  - target:
-      kind: Deployment
-      name: fournos-dashboard
-    patch: |
-      - op: replace
-        path: /spec/template/spec/containers/0/env/6/value
-        value: YOUR_FOURNOS_NAMESPACE
+patches:
+  - path: fournos-namespace-patch.yaml
# fournos-namespace-patch.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: fournos-dashboard
spec:
  template:
    spec:
      containers:
        - name: dashboard
          env:
            - name: FOURNOS_NAMESPACE
              value: YOUR_FOURNOS_NAMESPACE
🤖 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 `@fournos-ui/kustomize/overlays/ocp/kustomization.yaml.example` around lines 26
- 50, The JSON6902 patches in kustomization.yaml.example are index-based and can
silently drift when the base Deployment changes. Update the overlay to use a
strategic-merge patch (or name-keyed patching) for fournos-dashboard so the env
var FOURNOS_NAMESPACE is matched by name instead of env/6, and apply the same
approach for the dashboard and init container image overrides using container
names rather than containers/0 and initContainers/0.
fournos-ui/kustomize/base/postgresql-statefulset.yaml (1)

21-54: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Harden container securityContext.

Only pod-level runAsNonRoot: true is set; the postgresql container lacks allowPrivilegeEscalation: false, readOnlyRootFilesystem: true, and dropped capabilities, as flagged by static analysis. Note that enabling readOnlyRootFilesystem for the official postgres image will additionally require emptyDir mounts for paths like /tmp and /var/run/postgresql.

🔒️ Proposed container securityContext hardening
       containers:
         - name: postgresql
           image: postgres:15-alpine
+          securityContext:
+            allowPrivilegeEscalation: false
+            readOnlyRootFilesystem: true
+            capabilities:
+              drop:
+                - ALL
           ports:
             - containerPort: 5432
               name: postgresql
               protocol: TCP
+          volumeMounts:
+            - name: postgresql-data
+              mountPath: /var/lib/postgresql/data
+            - name: tmp
+              mountPath: /tmp
+            - name: run
+              mountPath: /var/run/postgresql
🤖 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 `@fournos-ui/kustomize/base/postgresql-statefulset.yaml` around lines 21 - 54,
The postgresql container in the StatefulSet only has a pod-level non-root
setting, so harden the container-level securityContext for the postgresql
container by adding allowPrivilegeEscalation: false and dropping all
capabilities, and enable readOnlyRootFilesystem only if you also add the
required emptyDir mounts for writable paths such as /tmp and
/var/run/postgresql. Update the container spec under the postgresql container
block to include these securityContext fields.

Source: Linters/SAST tools

fournos-ui/app/templates/components/jobs_table_body.html (2)

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

Actions-menu SVG markup duplicated with jobs_list.html.

The kebab-trigger button and "Run Again" icon SVGs here are duplicated verbatim in jobs_list.html (history tab, lines 131-142). Consider extracting a shared macro/include for the actions-menu (trigger + stop/rerun/delete buttons) to avoid divergence between the live-table and history-table rendering.

🤖 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 `@fournos-ui/app/templates/components/jobs_table_body.html` around lines 56 -
76, The actions menu markup in jobs_table_body.html is duplicated in
jobs_list.html, including the kebab trigger and the Run Again SVG, so extract
the shared actions-menu UI into a reusable macro/include and have both templates
use it. Use the existing stopJob, rerunJob, and toggleActions hooks as the
stable integration points when consolidating the duplicated button markup.

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

Progress-bar markup duplicated with job_detail_dynamic.html — already drifting.

This progress bar (segments + tooltip) is nearly identical to the one in components/job_detail_dynamic.html (lines 41-53), except the tooltip there also includes "Skipped" while this one doesn't. Extracting a shared macro (e.g. {% macro progress_bar(progress) %}) would keep both in sync going forward.

🤖 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 `@fournos-ui/app/templates/components/jobs_table_body.html` around lines 32 -
42, The progress-bar markup in jobs_table_body.html is duplicated from
job_detail_dynamic.html and has already diverged, so extract the shared
rendering into a reusable Jinja macro such as progress_bar(progress). Update
both templates to call the same macro and make sure the tooltip and segment
logic stay consistent, including the Skipped count shown in
job_detail_dynamic.html.
fournos-ui/app/templates/components/job_detail_dynamic.html (1)

11-13: 📐 Maintainability & Code Quality | 🔵 Trivial

Prefer namespace() over list-append side-effect trick.

Using {% if finally_started.append(1) %} purely for its (falsy) side effect works but is non-idiomatic and duplicated twice in this file. Jinja's namespace() object is the standard way to mutate state across a loop.

♻️ Suggested refactor
-{% set finally_started = [] %}
+{% set ns = namespace(started=false) %}
 {% for s in tl %}
-{% if s['finally'] and not finally_started %}{% if finally_started.append(1) %}{% endif %}<div class="ptl-sep"></div>{% endif %}
+{% if s['finally'] and not ns.started %}{% set ns.started = true %}<div class="ptl-sep"></div>{% endif %}

Apply the equivalent change to the finally_started2 block below.

Also applies to: 22-24

🤖 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 `@fournos-ui/app/templates/components/job_detail_dynamic.html` around lines 11
- 13, The loop state tracking in the `finally_started` and `finally_started2`
blocks uses a list-append side effect inside an `if`, which is non-idiomatic
Jinja. Refactor `job_detail_dynamic.html` to use a `namespace()` object for the
cross-loop flag in both blocks, and update the conditionals to set and check
that namespace value instead of relying on `{% if ...append(1) %}`. Keep the
existing separator rendering logic intact while applying the same change to both
occurrences.
🤖 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 `@fournos-ui/app/forge_discovery.py`:
- Around line 92-103: The project parsing loop in discover_projects() should not
assume every item in data["projects"] is a mapping, because a malformed entry
can raise AttributeError and break all routes that depend on it. Update the loop
around the ProjectInfo construction to validate each proj before calling
proj.get(...), and skip or safely ignore non-dict entries so one bad ConfigMap
item does not abort discovery.

In `@fournos-ui/app/k8s_client.py`:
- Around line 26-50: The K8s client setup in _ensure_loaded() only initializes
shared API objects and does not limit how long sync calls can block inside async
FastAPI handlers. Update the actual Kubernetes API call sites used through
_custom_api, _core_api, and _batch_api to pass a request timeout (for example
via _request_timeout), or add a central wrapper around those client methods so
every request is bounded; do not rely on retries alone.

In `@fournos-ui/app/main.py`:
- Around line 40-44: Disable Jinja template caching in production by removing
the hardcoded cache_size=0 from _jinja_env in main.py and letting Environment
use a positive cache size or a config-driven value. Update the _jinja_env setup
so get_template calls reuse compiled templates, while preserving any desired
no-cache behavior only for local development if needed.
- Around line 292-350: The jobs_list handler applies pagination only in the
history branch, so the live tab still renders every filtered job. Update
jobs_list to slice the filtered live jobs by page and per_page using the
existing page parameter, and keep total as the full filtered count so the
pagination UI remains correct. Make sure the change is localized around
jobs_list and preserves the current filters, clusters, and current_steps
behavior for the live tab.
- Around line 577-604: The submission flow in the job-creation handler catches
both Kubernetes creation and DB persistence failures together, so a successful
create_fournos_job followed by a db.upsert_job error is wrongly treated as a
failed submission. Split the error handling around create_fournos_job and the
db.async_session/db.upsert_job block in the main submission path so that once
the K8s job is created, any later DB failure does not re-render submit_job.html
as an error; instead preserve the created_name and return the success redirect
(or an equivalent non-error response) while logging the DB sync failure
separately.
- Around line 658-678: The schedule edit flow in main.py deletes the existing
CronJob before confirming the replacement is created, so a failed create can
leave the schedule missing. Update the edit path around edit_target,
k8s_client.delete_cronjob, and k8s_client.create_cronjob so the new CronJob is
created first (or otherwise validated) and only then remove/replace the old one,
with rollback if creation fails.
- Around line 483-497: The stream_logs SSE handler is iterating a synchronous
follow loop directly on the async event loop, which can block the worker. Update
stream_logs and its inner generate() to offload k8s_client.read_pod_log(...,
follow=True) to a thread/executor, then asynchronously yield each line back to
StreamingResponse without blocking; keep the pod validation and SSE formatting
in place.
- Around line 224-268: The async job routes are still doing blocking Kubernetes
calls, and _compute_current_steps also triggers per-job step lookups that can
stall the event loop under polling load. Move the synchronous k8s_client access
used by jobs_list, jobs_table_partial, job_detail, and job_detail_partial off
the async path by wrapping the relevant calls in a threadpool/async offload, and
keep _compute_current_steps from fanning out synchronously. Also add request
timeouts in k8s_client so list_fournos_jobs, get_current_step_for_job, and
related client methods fail fast instead of blocking.

In `@fournos-ui/app/static/htmx-sse.js`:
- Around line 253-258: The retry backoff in ensureEventSourceOnElement is using
the XOR operator instead of exponentiation, which can produce incorrect delays
and reconnect loops. Update the timeout calculation to use proper
power/exponential backoff with retryCount in the CLOSED readyState branch,
keeping the existing Math.random() jitter and retryCount increment logic intact.
- Around line 190-199: The `hx-trigger="sse:*"` branch in `htmx-sse.js` creates
`listener` but never registers it, so the trigger never fires. In the code path
that defines `listener` for the SSE event handling, make sure
`source.addEventListener(...)` is called for the computed SSE event name and
that the htmx request is triggered as intended when the event arrives. Use the
surrounding SSE setup logic in this listener block to wire the handler
consistently with the other trigger paths.
- Around line 239-267: The ensureEventSource() logic always creates a new
EventSource and overwrites api.getInternalData(elt).sseEventSource, which can
leave duplicate live SSE connections on reprocessing. Update ensureEventSource()
to first check for an existing sseEventSource and reuse it when its readyState
is not CLOSED, or explicitly close the old source before assigning a new one;
keep the change localized to ensureEventSource() and its use of
api.getInternalData(elt).sseEventSource.

In `@fournos-ui/app/templates/job_detail.html`:
- Around line 10-26: Keep the live job header synced with polling: the phase
badge, duration badge, Cancel Job button, and artifact links in the job detail
view are rendered outside the only fragment that refreshes periodically. Update
the `job_detail.html` template so the dynamic header controls are either moved
into `#job-dynamic` or sent as out-of-band swaps with `hx-swap-oob`, and make
sure the same `phase`, `is_history`, and `job.metadata.name` conditions are
applied in the polled update path.

In `@fournos-ui/app/watcher.py`:
- Around line 103-124: `_archive_job` is still creating duplicate `JobEvent`
rows on every sync even when the job’s `status` and `message` haven’t changed.
Update the logic around `_archive_job` and the `db.add_job_event` call to
compare the incoming forge fields against the existing job state before
inserting, or add deduping inside `add_job_event`, so unchanged watch/full-sync
passes only upsert the job and skip event creation.

In `@fournos-ui/Dockerfile`:
- Around line 19-23: The current uvicorn ENTRYPOINT is running multiple web
workers while watcher.start_watcher() is launched from the FastAPI lifespan,
which duplicates watcher threads and downstream sync/write work. Fix this by
ensuring the watcher runs in a separate single-replica process or by changing
the app to a single worker, and keep the watcher startup isolated from the
multi-worker web process in the app lifespan/ENTRYPOINT setup.

In `@fournos-ui/kustomize/base/dashboard-clusterrole.yaml`:
- Around line 1-28: The ClusterRole for the dashboard is granting cluster-wide
write access for ConfigMaps, CronJobs, and Jobs even though the dashboard only
operates in FOURNOS_NAMESPACE and its own namespace. Refactor the RBAC in the
dashboard ClusterRole manifest by moving the namespace-specific rules into Role
objects and binding them with RoleBinding, and only keep permissions in
ClusterRole/ClusterRoleBinding if a true cross-namespace need exists. Use the
existing fournos-dashboard-reader role definition and the rules for configmaps,
cronjobs, and jobs as the symbols to split and scope correctly.

In `@fournos-ui/kustomize/base/dashboard-deployment.yaml`:
- Around line 19-103: The dashboard Deployment is missing restrictive container
security settings on both the dashboard container and the wait-for-postgresql
initContainer. Add a securityContext to each container with
readOnlyRootFilesystem enabled and allowPrivilegeEscalation disabled, and mirror
the same settings on the initContainer as well. If the dashboard process needs
writable storage, use an emptyDir mount for that specific path while keeping the
root filesystem read-only.

In `@fournos-ui/requirements.txt`:
- Around line 1-11: The dashboard dependencies in requirements.txt are all
unpinned, which makes builds non-reproducible and can introduce unexpected
upgrades. Update the dependency list used by the Dockerfile install path to
fixed versions, or switch the install flow to a lockfile/constraints file so
rebuilds resolve the same package set. Make sure every package listed in this
file is versioned consistently and kept in sync with the Docker image build.

---

Outside diff comments:
In `@fournos-ui/README.md`:
- Around line 109-125: The repository tree in README has a directory-name typo
and the code fence is missing a language annotation. Update the rendered tree in
the README’s project structure section to use the correct top-level name
referenced by the `fournos-ui` project, and change the fenced block to a
language-tagged fence so it satisfies markdownlint MD040; use the existing
README tree snippet as the location to fix.

---

Minor comments:
In `@fournos-ui/app/static/style.css`:
- Line 16: Stylelint is flagging `value-keyword-case` for the font family values
in `style.css`; update the `--font-sans` declaration and the other affected
`font-family` usages in this file so the font names are quoted consistently.
Also review any `currentColor` occurrences mentioned in the comment and adjust
them to satisfy the same rule while keeping the intended styling intact.

In `@fournos-ui/app/templates/jobs_list.html`:
- Around line 45-46: The job list template is building query strings from
free-text filter values without URL encoding, so special characters can break
the requests. Update the hx-get URL in jobs_list.html to pass filters.project,
filters.cluster, filters.status, and filters.owner through Jinja’s urlencode
filter, and apply the same urlencode treatment to the pagination hrefs in the
jobs table controls. Use the existing hx-get block and the pagination link
markup as the locations to fix.

In `@fournos-ui/app/templates/schedules.html`:
- Around line 227-237: `cronjobData` is being built with Jinja string
interpolation, which can produce invalid JavaScript and break on quoted values;
update the template in the schedules view to serialize the full object with
`tojson` instead of manually quoting fields. Use the existing `cronjobData`
block and ensure every embedded property, including `has_resolver` and
`suspend`, is emitted through proper JSON serialization so JavaScript receives a
valid object literal.

In `@fournos-ui/app/templates/submit_job.html`:
- Around line 98-100: The project-info fetch in submit_job.html builds the API
URL with the raw projectName, which can break routing for names containing path
or query characters and can treat failed requests as empty results. Update the
fetch call in the submit-job flow to URL-encode projectName before concatenating
it into the /api/project-info/ request, and add a resp.ok check in the same
async block so non-200 responses are handled as failures rather than parsed as
empty preset data.

In `@fournos-ui/README.md`:
- Around line 58-64: The “Build and push the dashboard image” step is missing
the actual command examples, leaving the setup guide incomplete. Update the
README section under that heading to include a concrete build and push flow,
using the same style as the surrounding steps and referencing the dashboard
image workflow so readers can follow it without guessing.
- Around line 15-25: The fenced diagram block in README is missing a language
tag, which triggers markdownlint MD040. Update the existing markdown fence for
the architecture diagram by adding an appropriate language identifier (for
example, text) while keeping the diagram content unchanged, and make sure the
fenced block in the README remains valid markdown.

---

Nitpick comments:
In `@fournos-ui/app/db.py`:
- Around line 140-164: The list_jobs query in the Job history path is ordering
by Job.created_at.desc() with limit/offset pagination, so add an index on
created_at to avoid full scans and sorts as the jobs table grows. Update the Job
model/schema with an index on created_at (ideally alongside any existing filters
if appropriate), and ensure the change is reflected in the database migration so
list_jobs continues to return the same results while using the index.
- Around line 85-90: The schema-management setup is inconsistent: init_db() in
db.py still creates tables directly, while alembic is present but there is no
visible migration wiring. Either add the missing Alembic migration setup and
switch production schema changes to use it, or remove the Alembic dependency and
update the init_db() docstring/logging to reflect direct table creation only.
Use init_db(), Base.metadata.create_all, and the alembic dependency reference to
locate the related code.

In `@fournos-ui/app/k8s_client.py`:
- Around line 63-79: The list/read helpers in k8s_client.py only handle
ApiException, so transport failures can still escape and break the module’s
safe-default behavior. Update list_fournos_jobs and the other affected read/list
functions referenced in this module to also catch underlying
transport/connection exceptions (for example urllib3/requests errors used by the
Kubernetes client) alongside ApiException, log the failure, and return the same
empty/default value. Keep the existing defensive pattern consistent across these
functions such as list_fournos_jobs and the other read/list helpers.

In `@fournos-ui/app/templates/components/job_detail_dynamic.html`:
- Around line 11-13: The loop state tracking in the `finally_started` and
`finally_started2` blocks uses a list-append side effect inside an `if`, which
is non-idiomatic Jinja. Refactor `job_detail_dynamic.html` to use a
`namespace()` object for the cross-loop flag in both blocks, and update the
conditionals to set and check that namespace value instead of relying on `{% if
...append(1) %}`. Keep the existing separator rendering logic intact while
applying the same change to both occurrences.

In `@fournos-ui/app/templates/components/jobs_table_body.html`:
- Around line 56-76: The actions menu markup in jobs_table_body.html is
duplicated in jobs_list.html, including the kebab trigger and the Run Again SVG,
so extract the shared actions-menu UI into a reusable macro/include and have
both templates use it. Use the existing stopJob, rerunJob, and toggleActions
hooks as the stable integration points when consolidating the duplicated button
markup.
- Around line 32-42: The progress-bar markup in jobs_table_body.html is
duplicated from job_detail_dynamic.html and has already diverged, so extract the
shared rendering into a reusable Jinja macro such as progress_bar(progress).
Update both templates to call the same macro and make sure the tooltip and
segment logic stay consistent, including the Skipped count shown in
job_detail_dynamic.html.

In `@fournos-ui/kustomize/base/postgresql-statefulset.yaml`:
- Around line 21-54: The postgresql container in the StatefulSet only has a
pod-level non-root setting, so harden the container-level securityContext for
the postgresql container by adding allowPrivilegeEscalation: false and dropping
all capabilities, and enable readOnlyRootFilesystem only if you also add the
required emptyDir mounts for writable paths such as /tmp and
/var/run/postgresql. Update the container spec under the postgresql container
block to include these securityContext fields.

In `@fournos-ui/kustomize/overlays/ocp/kustomization.yaml.example`:
- Around line 26-50: The JSON6902 patches in kustomization.yaml.example are
index-based and can silently drift when the base Deployment changes. Update the
overlay to use a strategic-merge patch (or name-keyed patching) for
fournos-dashboard so the env var FOURNOS_NAMESPACE is matched by name instead of
env/6, and apply the same approach for the dashboard and init container image
overrides using container names rather than containers/0 and initContainers/0.
🪄 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: a912f49c-385d-45bf-910a-bd65bf6acf2a

📥 Commits

Reviewing files that changed from the base of the PR and between 216e292 and 72aa751.

⛔ Files ignored due to path filters (1)
  • fournos-ui/app/static/htmx.min.js is excluded by !**/*.min.js
📒 Files selected for processing (37)
  • fournos-ui/.dockerignore
  • fournos-ui/.gitignore
  • fournos-ui/Dockerfile
  • fournos-ui/README.md
  • fournos-ui/app/__init__.py
  • fournos-ui/app/config.py
  • fournos-ui/app/db.py
  • fournos-ui/app/forge_discovery.py
  • fournos-ui/app/k8s_client.py
  • fournos-ui/app/main.py
  • fournos-ui/app/models.py
  • fournos-ui/app/static/htmx-sse.js
  • fournos-ui/app/static/style.css
  • fournos-ui/app/templates/base.html
  • fournos-ui/app/templates/components/job_detail_dynamic.html
  • fournos-ui/app/templates/components/jobs_table_body.html
  • fournos-ui/app/templates/job_detail.html
  • fournos-ui/app/templates/jobs_list.html
  • fournos-ui/app/templates/schedule_runs.html
  • fournos-ui/app/templates/schedules.html
  • fournos-ui/app/templates/submit_job.html
  • fournos-ui/app/watcher.py
  • fournos-ui/kustomize/base/dashboard-clusterrole.yaml
  • fournos-ui/kustomize/base/dashboard-clusterrolebinding.yaml
  • fournos-ui/kustomize/base/dashboard-deployment.yaml
  • fournos-ui/kustomize/base/dashboard-projects-configmap.yaml
  • fournos-ui/kustomize/base/dashboard-service.yaml
  • fournos-ui/kustomize/base/dashboard-serviceaccount.yaml
  • fournos-ui/kustomize/base/kustomization.yaml
  • fournos-ui/kustomize/base/namespace.yaml
  • fournos-ui/kustomize/base/postgresql-secret.env.example
  • fournos-ui/kustomize/base/postgresql-service.yaml
  • fournos-ui/kustomize/base/postgresql-statefulset.yaml
  • fournos-ui/kustomize/overlays/ocp/kustomization.yaml.example
  • fournos-ui/kustomize/overlays/ocp/params.env.example
  • fournos-ui/kustomize/overlays/ocp/projects.yaml.example
  • fournos-ui/requirements.txt

Comment thread fournos-ui/app/forge_discovery.py
Comment thread fournos-ui/app/k8s_client.py Outdated
Comment thread fournos-ui/app/main.py
Comment thread fournos-ui/app/main.py Outdated
Comment thread fournos-ui/app/main.py
Comment thread fournos-ui/app/watcher.py
Comment thread fournos-ui/Dockerfile
Comment thread fournos-ui/kustomize/base/dashboard-clusterrole.yaml
Comment thread fournos-ui/kustomize/base/dashboard-deployment.yaml
Comment thread fournos-ui/requirements.txt Outdated
ashtarkb and others added 2 commits July 5, 2026 14:45
- Switch ClusterRoleBinding to namespace-scoped RoleBinding for least-privilege access
- Add securityContext to init and dashboard containers (drop capabilities, read-only root)
- Wrap blocking K8s API calls with asyncio.to_thread to avoid event-loop starvation
- Add configurable K8s request timeout (K8S_REQUEST_TIMEOUT env var)
- Pin dependency versions in requirements.txt
- Use kustomize configMapGenerator for projects ConfigMap instead of static manifest
- Add OOB HTMX swaps for live header status, completion banner, and MLflow links
- Fix htmx-sse.js exponential backoff (Math.pow instead of bitwise XOR)
- Deduplicate watcher archive events when phase/message unchanged
- Add resilience to malformed project entries in forge_discovery
- Reduce Dockerfile workers to 1 and disable Jinja2 cache_size=0 in favour of auto_reload=False
- Update README with corrected deploy instructions and project structure

Co-authored-by: Cursor <cursoragent@cursor.com>
- Add optional PULL_PULL_SHA field to submit form, injected into
  spec.env so the resolve job checks out Forge code from a specific PR
- Show the MCP Gateway version field only when mcp_gateway project is
  selected; hide and clear it for all other projects

Co-authored-by: Cursor <cursoragent@cursor.com>

@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

Caution

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

⚠️ Outside diff range comments (2)
fournos-ui/kustomize/base/kustomization.yaml (1)

6-13: 🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win

The RoleBinding is never deployed — RBAC will not take effect. The resources list includes dashboard-clusterrole.yaml but omits dashboard-clusterrolebinding.yaml. Without the binding, the fournos-dashboard ServiceAccount receives none of the ClusterRole permissions, and all Kubernetes API calls (fournosjobs, configmaps, cronjobs, jobs, pods, logs) will fail with Forbidden at runtime.

🔧 Add the binding to the resources list
   - dashboard-service.yaml
   - dashboard-clusterrole.yaml
+  - dashboard-clusterrolebinding.yaml
🤖 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 `@fournos-ui/kustomize/base/kustomization.yaml` around lines 6 - 13, The RBAC
resources are incomplete because the Kustomize base includes
dashboard-clusterrole.yaml but not dashboard-clusterrolebinding.yaml, so the
forunos-dashboard ServiceAccount never gets the ClusterRole permissions. Update
the resources list in kustomization.yaml to include the missing RoleBinding
manifest alongside dashboard-clusterrole.yaml, keeping the
dashboard-serviceaccount and dashboard-clusterrole names consistent so the
binding is actually applied.
fournos-ui/kustomize/base/dashboard-clusterrolebinding.yaml (1)

2-15: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Add a RoleBinding in the target jobs namespace when FOURNOS_NAMESPACE differs. This binding only applies in the dashboard namespace; the client sends namespaced calls to settings.fournos_namespace, so a separate RoleBinding (or a ClusterRoleBinding) is needed there to avoid Forbidden errors.

🤖 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 `@fournos-ui/kustomize/base/dashboard-clusterrolebinding.yaml` around lines 2 -
15, The current RoleBinding only exists in the dashboard namespace, so
namespaced calls from the client can still fail with Forbidden when
FOURNOS_NAMESPACE points elsewhere. Update the dashboard-clusterrolebinding
manifest and related overlay logic so a RoleBinding is also created in the
target jobs namespace when FOURNOS_NAMESPACE differs, or switch to a
ClusterRoleBinding if that is the intended access model; reference the existing
fournos-dashboard binding and its roleRef/subjects to keep the new binding
aligned.
🤖 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 `@fournos-ui/app/main.py`:
- Around line 330-332: The cluster filter is being derived from the paginated
`jobs` slice in `main.py`, so `_collect_clusters(jobs)` only sees the current
page instead of all filtered results. Update the job listing flow so clusters
are collected from the full filtered job set before applying
`offset`/`per_page`, using the same `jobs` data path around `total`, slicing,
and `_collect_clusters`. Then remove the later unconditional cluster collection
from the paginated slice so the live tab dropdown includes clusters from every
matching job.
- Around line 508-526: The SSE generator in generate() should stop the
background log reader when the client disconnects and avoid dropping lines via
put_nowait. Add a disconnect-aware stop signal to the _reader loop, use a
blocking handoff into the asyncio.Queue instead of
call_soon_threadsafe(queue.put_nowait, ...), and ensure the executor task is
torn down when StreamingResponse finishes. If kubernetes log streaming exposes a
close/cleanup path in k8s_client.read_pod_log, invoke it during teardown.

In `@fournos-ui/kustomize/base/dashboard-clusterrolebinding.yaml`:
- Line 2: The manifest name and resource kind are mismatched:
dashboard-clusterrolebinding.yaml defines a RoleBinding, so rename it to
dashboard-rolebinding.yaml and update the corresponding kustomization reference
to match. Make the change in the RoleBinding manifest and any Kustomize entry
that points to dashboard-clusterrolebinding so the filename accurately reflects
the resource.

---

Outside diff comments:
In `@fournos-ui/kustomize/base/dashboard-clusterrolebinding.yaml`:
- Around line 2-15: The current RoleBinding only exists in the dashboard
namespace, so namespaced calls from the client can still fail with Forbidden
when FOURNOS_NAMESPACE points elsewhere. Update the dashboard-clusterrolebinding
manifest and related overlay logic so a RoleBinding is also created in the
target jobs namespace when FOURNOS_NAMESPACE differs, or switch to a
ClusterRoleBinding if that is the intended access model; reference the existing
fournos-dashboard binding and its roleRef/subjects to keep the new binding
aligned.

In `@fournos-ui/kustomize/base/kustomization.yaml`:
- Around line 6-13: The RBAC resources are incomplete because the Kustomize base
includes dashboard-clusterrole.yaml but not dashboard-clusterrolebinding.yaml,
so the forunos-dashboard ServiceAccount never gets the ClusterRole permissions.
Update the resources list in kustomization.yaml to include the missing
RoleBinding manifest alongside dashboard-clusterrole.yaml, keeping the
dashboard-serviceaccount and dashboard-clusterrole names consistent so the
binding is actually applied.
🪄 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: 229d8215-c7c9-4626-9f0b-1ecde3974f3a

📥 Commits

Reviewing files that changed from the base of the PR and between 72aa751 and 1a375cb.

📒 Files selected for processing (19)
  • fournos-ui/.gitignore
  • fournos-ui/Dockerfile
  • fournos-ui/README.md
  • fournos-ui/app/config.py
  • fournos-ui/app/forge_discovery.py
  • fournos-ui/app/k8s_client.py
  • fournos-ui/app/main.py
  • fournos-ui/app/static/htmx-sse.js
  • fournos-ui/app/templates/components/job_detail_dynamic.html
  • fournos-ui/app/templates/job_detail.html
  • fournos-ui/app/templates/submit_job.html
  • fournos-ui/app/watcher.py
  • fournos-ui/kustomize/base/dashboard-clusterrole.yaml
  • fournos-ui/kustomize/base/dashboard-clusterrolebinding.yaml
  • fournos-ui/kustomize/base/dashboard-deployment.yaml
  • fournos-ui/kustomize/base/dashboard-projects-configmap-data.yaml
  • fournos-ui/kustomize/base/kustomization.yaml
  • fournos-ui/kustomize/overlays/ocp/kustomization.yaml.example
  • fournos-ui/requirements.txt
✅ Files skipped from review due to trivial changes (3)
  • fournos-ui/kustomize/base/dashboard-projects-configmap-data.yaml
  • fournos-ui/README.md
  • fournos-ui/.gitignore
🚧 Files skipped from review as they are similar to previous changes (11)
  • fournos-ui/Dockerfile
  • fournos-ui/kustomize/overlays/ocp/kustomization.yaml.example
  • fournos-ui/app/templates/components/job_detail_dynamic.html
  • fournos-ui/kustomize/base/dashboard-deployment.yaml
  • fournos-ui/app/config.py
  • fournos-ui/app/templates/submit_job.html
  • fournos-ui/app/forge_discovery.py
  • fournos-ui/app/watcher.py
  • fournos-ui/app/templates/job_detail.html
  • fournos-ui/app/static/htmx-sse.js
  • fournos-ui/app/k8s_client.py

Comment thread fournos-ui/app/main.py
Comment thread fournos-ui/app/main.py
Comment on lines +508 to +526
async def generate():
queue: asyncio.Queue[str | None] = asyncio.Queue(maxsize=64)
loop = asyncio.get_event_loop()

def _reader():
try:
for line in k8s_client.read_pod_log(pod_name, follow=True):
loop.call_soon_threadsafe(queue.put_nowait, line)
finally:
loop.call_soon_threadsafe(queue.put_nowait, None)

asyncio.get_event_loop().run_in_executor(None, _reader)
while True:
line = await queue.get()
if line is None:
break
yield f"data: {line}\n\n"

return StreamingResponse(generate(), media_type="text/event-stream")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🔴 Critical | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate and inspect the relevant files.
git ls-files | rg '(^|/)app/main\.py$|(^|/)k8s_client|(^|/)k8s.*client|(^|/)client.*k8s' || true

# Show nearby context in fournos-ui/app/main.py around the cited lines.
if [ -f fournos-ui/app/main.py ]; then
  nl -ba fournos-ui/app/main.py | sed -n '470,560p'
fi

# Search for read_pod_log definitions/usages.
rg -n "read_pod_log|read_namespaced_pod_log|StreamingResponse|async def generate|run_in_executor|call_soon_threadsafe|QueueFull" fournos-ui -S

Repository: openshift-psap/fournos

Length of output: 252


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- fournos-ui/app/main.py (around cited lines) ---'
sed -n '490,545p' fournos-ui/app/main.py

echo
echo '--- fournos-ui/app/k8s_client.py ---'
sed -n '1,260p' fournos-ui/app/k8s_client.py

echo
echo '--- search for any disconnect/cancellation handling around pod log streaming ---'
rg -n "disconnect|cancel|CancelledError|QueueFull|put_nowait|run_coroutine_threadsafe|read_pod_log|read_namespaced_pod_log|stream\(" fournos-ui/app -S

Repository: openshift-psap/fournos

Length of output: 50378


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- fournos-ui/app/k8s_client.py (read_pod_log region) ---'
sed -n '360,440p' fournos-ui/app/k8s_client.py

echo
echo '--- asyncio queue/threadsafe backpressure probe ---'
python3 - <<'PY'
import asyncio
import threading

async def main():
    q = asyncio.Queue(maxsize=1)
    loop = asyncio.get_running_loop()
    q.put_nowait("first")

    done = threading.Event()
    errs = []

    def cb():
        try:
            q.put_nowait("second")
        except Exception as e:
            errs.append(repr(e))
        finally:
            done.set()

    loop.call_soon_threadsafe(cb)
    for _ in range(50):
        if done.is_set():
            break
        await asyncio.sleep(0.01)

    print("queue size:", q.qsize())
    print("errors:", errs)
    print("get:", await q.get())

asyncio.run(main())
PY

Repository: openshift-psap/fournos

Length of output: 3142


Stop the SSE reader on disconnect and avoid put_nowait here. fournos-ui/app/main.py:508-526

  • The background reader keeps running after generate() closes, so abandoned streams keep an executor worker busy until the pod log stream ends.
  • queue.put_nowait scheduled via call_soon_threadsafe can raise QueueFull, which drops log lines instead of applying backpressure.

Use a disconnect-aware stop signal and a blocking handoff into the queue; if possible, close the underlying Kubernetes log response during teardown too.

🤖 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 `@fournos-ui/app/main.py` around lines 508 - 526, The SSE generator in
generate() should stop the background log reader when the client disconnects and
avoid dropping lines via put_nowait. Add a disconnect-aware stop signal to the
_reader loop, use a blocking handoff into the asyncio.Queue instead of
call_soon_threadsafe(queue.put_nowait, ...), and ensure the executor task is
torn down when StreamingResponse finishes. If kubernetes log streaming exposes a
close/cleanup path in k8s_client.read_pod_log, invoke it during teardown.

Comment thread fournos-ui/kustomize/base/dashboard-rolebinding.yaml
ashtarkb and others added 2 commits July 5, 2026 17:21
- Stop the background log reader thread on client disconnect via
  asyncio.Event; catch QueueFull to avoid dropping the executor thread
- Collect cluster filter options from the full filtered job set before
  paginating so the dropdown includes all clusters, not just the
  current page
- Rename dashboard-clusterrolebinding.yaml to dashboard-rolebinding.yaml
  to match the actual RoleBinding kind inside

Co-authored-by: Cursor <cursoragent@cursor.com>
Replace the manual SHA input with a searchable dropdown that lists
open PRs from the Forge repo (public API, no token needed). Users
can filter by PR number, title or author and the HEAD SHA is filled
automatically. Also sort pods by creation timestamp so they appear
in execution order.

Co-authored-by: Cursor <cursoragent@cursor.com>

@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: 2

🤖 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 `@fournos-ui/app/main.py`:
- Around line 573-577: The GitHub fetch in github_open_prs is still using
synchronous urllib.request.urlopen inside an async path, which can block the
event loop. Update the request/response handling in github_open_prs to run the
blocking network call via asyncio.to_thread or switch to an async HTTP client,
and keep the existing HTTPException error handling around the fetch and JSON
parsing.

In `@fournos-ui/app/templates/submit_job.html`:
- Around line 154-169: The selectPR() helper is inserting API-sourced data into
pr-hint via innerHTML, which can create an XSS risk. Update the pr branch in
selectPR() to build the hint using textContent and/or DOM nodes instead of
string concatenation, and keep pr.head_sha as plain text when rendering the HEAD
SHA label. Use the existing selectPR, pr.head_sha, and pr-hint elements to
locate the change.
🪄 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: f24370cf-3c28-4432-a903-f4a6b403bed3

📥 Commits

Reviewing files that changed from the base of the PR and between 1a375cb and a396a53.

📒 Files selected for processing (6)
  • fournos-ui/README.md
  • fournos-ui/app/config.py
  • fournos-ui/app/k8s_client.py
  • fournos-ui/app/main.py
  • fournos-ui/app/templates/submit_job.html
  • fournos-ui/kustomize/base/dashboard-rolebinding.yaml
💤 Files with no reviewable changes (1)
  • fournos-ui/kustomize/base/dashboard-rolebinding.yaml
✅ Files skipped from review due to trivial changes (1)
  • fournos-ui/README.md
🚧 Files skipped from review as they are similar to previous changes (2)
  • fournos-ui/app/config.py
  • fournos-ui/app/k8s_client.py

Comment thread fournos-ui/app/main.py Outdated
Comment on lines +154 to +169
function selectPR(pr) {
const search = document.getElementById('pr_search');
const shaInput = document.getElementById('pull_sha');
const hint = document.getElementById('pr-hint');
const dd = document.getElementById('pr_dropdown');
dd.style.display = 'none';
if (pr) {
search.value = _prLabel(pr);
shaInput.value = pr.head_sha;
hint.innerHTML = 'HEAD SHA: <code style="font-family:var(--font-mono);font-size:12px;background:var(--bg-tertiary,#21262d);padding:1px 6px;border-radius:3px">' + pr.head_sha + '</code>';
} else {
search.value = '';
shaInput.value = '';
hint.textContent = 'Forge will build from this commit instead of the default image.';
}
}

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 | 🟡 Minor | ⚡ Quick win

Avoid innerHTML with API-sourced data.

hint.innerHTML is built by concatenating pr.head_sha from the GitHub API response. Static analysis flags this as a potential XSS vector (CWE-79). Even though GitHub SHAs are normally hex-only, this is untrusted external data reflected without escaping — safer to build the hint with textContent/DOM nodes instead of innerHTML.

🛡️ Proposed fix using DOM nodes instead of innerHTML
     if (pr) {
         search.value = _prLabel(pr);
         shaInput.value = pr.head_sha;
-        hint.innerHTML = 'HEAD SHA: <code style="font-family:var(--font-mono);font-size:12px;background:var(--bg-tertiary,`#21262d`);padding:1px 6px;border-radius:3px">' + pr.head_sha + '</code>';
+        hint.textContent = '';
+        hint.append('HEAD SHA: ');
+        const code = document.createElement('code');
+        code.style.cssText = 'font-family:var(--font-mono);font-size:12px;background:var(--bg-tertiary,`#21262d`);padding:1px 6px;border-radius:3px';
+        code.textContent = pr.head_sha;
+        hint.append(code);
     } else {
📝 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 selectPR(pr) {
const search = document.getElementById('pr_search');
const shaInput = document.getElementById('pull_sha');
const hint = document.getElementById('pr-hint');
const dd = document.getElementById('pr_dropdown');
dd.style.display = 'none';
if (pr) {
search.value = _prLabel(pr);
shaInput.value = pr.head_sha;
hint.innerHTML = 'HEAD SHA: <code style="font-family:var(--font-mono);font-size:12px;background:var(--bg-tertiary,#21262d);padding:1px 6px;border-radius:3px">' + pr.head_sha + '</code>';
} else {
search.value = '';
shaInput.value = '';
hint.textContent = 'Forge will build from this commit instead of the default image.';
}
}
function selectPR(pr) {
const search = document.getElementById('pr_search');
const shaInput = document.getElementById('pull_sha');
const hint = document.getElementById('pr-hint');
const dd = document.getElementById('pr_dropdown');
dd.style.display = 'none';
if (pr) {
search.value = _prLabel(pr);
shaInput.value = pr.head_sha;
hint.textContent = '';
hint.append('HEAD SHA: ');
const code = document.createElement('code');
code.style.cssText = 'font-family:var(--font-mono);font-size:12px;background:var(--bg-tertiary,`#21262d`);padding:1px 6px;border-radius:3px';
code.textContent = pr.head_sha;
hint.append(code);
} else {
search.value = '';
shaInput.value = '';
hint.textContent = 'Forge will build from this commit instead of the default image.';
}
}
🧰 Tools
🪛 ast-grep (0.44.1)

[warning] 162-162: Avoid assigning untrusted data to innerHTML/outerHTML or document.write
Context: hint.innerHTML = 'HEAD SHA: ' + pr.head_sha + ''
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting').

(inner-outer-html)

🤖 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 `@fournos-ui/app/templates/submit_job.html` around lines 154 - 169, The
selectPR() helper is inserting API-sourced data into pr-hint via innerHTML,
which can create an XSS risk. Update the pr branch in selectPR() to build the
hint using textContent and/or DOM nodes instead of string concatenation, and
keep pr.head_sha as plain text when rendering the HEAD SHA label. Use the
existing selectPR, pr.head_sha, and pr-hint elements to locate the change.

Source: Linters/SAST tools

@kpouget

kpouget commented Jul 13, 2026

Copy link
Copy Markdown
Collaborator

@ashtarkb should we merge that?

@ashtarkb

Copy link
Copy Markdown
Contributor Author

@kpouget I think we can merge it and I will improve it with time and with new features.
It's not relying nor affecting any fournos functionality

@kpouget
kpouget merged commit 8b3628e into openshift-psap:main Jul 13, 2026
4 of 5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants