Skip to content

Add opinion monitor quickstart - #5

Closed
2233admin wants to merge 2 commits into
backend-fleet-v1.1.0from
codex/opinion-monitor-quickstart
Closed

Add opinion monitor quickstart#5
2233admin wants to merge 2 commits into
backend-fleet-v1.1.0from
codex/opinion-monitor-quickstart

Conversation

@2233admin

Copy link
Copy Markdown
Owner

Summary

  • add an opinion-monitor quickstart preset that creates OpenCLI sources, schedules, and a Feishu notification rule
  • add a real dashboard projection for records, AI enrichment, tags/sentiment, and Feishu send evidence
  • render AI enrichment fields in Feishu templates and document the practical flow
  • add a fleet acceptance script that proves collection routes through the selected WS agent

Verification

  • .\.venv\Scripts\python.exe -m ruff check backend/api/v1/presets.py backend/api/v1/dashboard.py backend/notifiers/feishu_notifier.py backend/pipeline/runner.py backend/agent_server.py backend/ws_agent_manager.py tests/integration/test_presets_api.py tests/integration/test_dashboard_api.py tests/unit/test_messaging_notifiers.py
  • .\.venv\Scripts\python.exe -m pytest -q --no-cov tests/integration/test_presets_api.py tests/integration/test_dashboard_api.py tests/unit/test_messaging_notifiers.py tests/integration/test_workflow_fleet_api.py tests/integration/test_opencli_channel_api.py::test_collect_agent_mode_prefers_site_bound_agent tests/unit/channels/test_opencli_channel.py
  • npm run typecheck:frontend
  • npm run build:frontend
  • Invoke-SentruxAgentTool.ps1 check_rules C:\c\Users\Administrator\projects\opencli-admin-backend
  • PowerShell parser check for scripts/acceptance/fleet-acceptance.ps1

Notes

  • pushing to lunnynight/opencli-admin was rejected with permission denied, so this PR is opened on the accessible 2233admin/opencli-admin fork branch.

@coderabbitai

coderabbitai Bot commented Jul 5, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: b9e18352-7acb-40ff-9dde-bcbe5994e91e

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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.

@repowise-bot

repowise-bot Bot commented Jul 5, 2026

Copy link
Copy Markdown

✅ Health: 8.0
5 hotspots · 5 hidden couplings · 6 dead-code findings

🚨 Change risk: 9.9/10 (high)
This change's risk is driven by:

  • more lines added than baseline
  • more scattered than baseline
🔥 Hotspots touched (5)
  • tests/unit/test_messaging_notifiers.py — 2 commits/90d, 0 dependents · primary owner: xujinghua (100%)
  • backend/ws_agent_manager.py — 3 commits/90d, 2 dependents · primary owner: xujinghua (100%)
  • backend/pipeline/runner.py — 6 commits/90d, 7 dependents · primary owner: xujinghua (100%)
2 more
  • .../v1/dashboard.py — 4 commits/90d, 1 dependents · primary owner: xujinghua (100%)
  • backend/agent_server.py — 18 commits/90d, 0 dependents · primary owner: xujinghua (100%)
🔗 Hidden coupling (2 files)
  • backend/agent_server.py co-changes with backend/channels/opencli_channel.py (10× — 🟡 notable) — not in this PR.
  • backend/pipeline/runner.py co-changes with these files (not in this PR):
    • .../api/endpoints.ts (6× — 🟢 routine)
    • .../api/types.ts (6× — 🟢 routine)
    • backend/models/task.py (4× — 🟢 routine)
    • backend/models/__init__.py (4× — 🟢 routine)
💀 Dead code (6 findings)
  • 💀 backend/ws_agent_manager.py unregister_connection (confidence 1.00)
  • 💀 backend/ws_agent_manager.py is_connected (confidence 1.00)
  • 💀 backend/ws_agent_manager.py list_connected (confidence 1.00)
3 more
  • 💀 backend/ws_agent_manager.py dispatch_collect (confidence 1.00)
  • 💀 backend/ws_agent_manager.py resolve_response (confidence 1.00)
  • 💀 backend/agent_server.py (file-level) (confidence 0.40)

👀 Suggested reviewers @xujinghua


📊 Full report · ⭐ Star Repowise · 📥 Install bot · Last updated 2026-07-06 19:27 UTC
Silence on a single PR with [skip repowise] in the title · Per-repo toggle on repowise.dev/settings?tab=bot

@gemini-code-assist gemini-code-assist 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.

Code Review

This pull request introduces an 'Opinion Monitor' feature, adding backend endpoints for dashboard projection and preset application, frontend UI components for real-time visualization, and a PowerShell acceptance script. The code review identified several critical issues: a missing database commit in the preset application endpoint, hardcoded absolute paths in the acceptance script, a logic flaw where invalid cron expressions block data source creation, potential crashes in dashboard helpers due to missing type checks on AI enrichment data, potential type mismatches with record IDs, and duplicate React keys in the frontend.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread backend/api/v1/presets.py
Comment on lines +227 to +229
db.add(rule)
await db.flush()
await db.refresh(rule)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

The endpoint apply_opinion_monitor_preset creates sources, schedules, and a notification rule, but never calls await db.commit(). In production, when the request finishes and the session is closed, all these newly created records will be rolled back and lost. Please ensure await db.commit() is called to persist the changes.

Suggested change
db.add(rule)
await db.flush()
await db.refresh(rule)
db.add(rule)
await db.flush()
await db.refresh(rule)
await db.commit()

Comment on lines +723 to +743
Run-RegressionGate -Gate "regression-sentrux" -Label "sentrux-check-rules" -FilePath $PwshExe -Arguments @(
"-NoProfile", "-ExecutionPolicy", "Bypass",
"-File", "C:\c\Users\Administrator\projects\code-intel-pipeline\Invoke-SentruxAgentTool.ps1",
"check_rules", $RepoRoot
) -TimeoutSeconds $RegressionTimeoutSeconds -Hard $true | Out-Null

if (-not $SkipCodeIntel) {
Run-RegressionGate -Gate "regression-code-intel-doctor" -Label "code-intel-doctor" -FilePath $PwshExe -Arguments @(
"-NoProfile", "-ExecutionPolicy", "Bypass",
"-File", "C:\c\Users\Administrator\projects\code-intel-pipeline\check-code-intel-tools.ps1",
"-RepoPath", $RepoRoot,
"-RequireRepowise",
"-Json"
) -TimeoutSeconds $RegressionTimeoutSeconds -Hard $true | Out-Null

Run-RegressionGate -Gate "regression-code-intel-normal" -Label "code-intel-normal" -FilePath $PwshExe -Arguments @(
"-NoProfile", "-ExecutionPolicy", "Bypass",
"-File", "C:\c\Users\Administrator\projects\code-intel-pipeline\invoke-code-intel.ps1",
"-RepoPath", $RepoRoot,
"-Mode", "normal"
) -TimeoutSeconds $RegressionTimeoutSeconds -Hard $true -KnownDebtRegex "graph_missing|Understand graph: False|baseline_missing|rules_missing|known debt|known_debt|Sentrux fail|sentrux_fail|Sentrux gate|Blocking Sentrux debt|worsened_debt|god_files|Quality degraded during this session" | Out-Null

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

The script hardcodes absolute paths to a specific user's directory (C:\c\Users\Administrator\projects\code-intel-pipeline\...) for executing regression tools. This makes the script non-portable, and it will fail on any other developer's machine or standard CI environment. Consider using an environment variable with a fallback to make it configurable.

    $PipelinePath = $env:CODE_INTEL_PIPELINE_PATH
    if (-not $PipelinePath) {
        $PipelinePath = "C:\\c\\Users\\Administrator\\projects\\code-intel-pipeline"
    }
    Run-RegressionGate -Gate "regression-sentrux" -Label "sentrux-check-rules" -FilePath $PwshExe -Arguments @(
        "-NoProfile", "-ExecutionPolicy", "Bypass",
        "-File", (Join-Path $PipelinePath "Invoke-SentruxAgentTool.ps1"),
        "check_rules", $RepoRoot
    ) -TimeoutSeconds $RegressionTimeoutSeconds -Hard $true | Out-Null

    if (-not $SkipCodeIntel) {
        Run-RegressionGate -Gate "regression-code-intel-doctor" -Label "code-intel-doctor" -FilePath $PwshExe -Arguments @(
            "-NoProfile", "-ExecutionPolicy", "Bypass",
            "-File", (Join-Path $PipelinePath "check-code-intel-tools.ps1"),
            "-RepoPath", $RepoRoot,
            "-RequireRepowise",
            "-Json"
        ) -TimeoutSeconds $RegressionTimeoutSeconds -Hard $true | Out-Null

        Run-RegressionGate -Gate "regression-code-intel-normal" -Label "code-intel-normal" -FilePath $PwshExe -Arguments @(
            "-NoProfile", "-ExecutionPolicy", "Bypass",
            "-File", (Join-Path $PipelinePath "invoke-code-intel.ps1"),
            "-RepoPath", $RepoRoot,
            "-Mode", "normal"
        ) -TimeoutSeconds $RegressionTimeoutSeconds -Hard $true -KnownDebtRegex "graph_missing|Understand graph: False|baseline_missing|rules_missing|known debt|known_debt|Sentrux fail|sentrux_fail|Sentrux gate|Blocking Sentrux debt|worsened_debt|god_files|Quality degraded during this session" | Out-Null

Comment thread backend/api/v1/presets.py
Comment on lines +153 to +198
for slot in body.account_slots:
if body.create_schedules and not schedule_service.validate_cron_expression(
slot.cron_expression
):
warnings.append(
{
"slot": slot.label,
"warning": "invalid_cron_expression",
"cron_expression": slot.cron_expression,
}
)
continue

source_data = DataSourceCreate(**_opinion_source_payload(body, slot))
source = await source_service.create_source(db, source_data)
created_sources.append(
{
"id": source.id,
"name": source.name,
"site": slot.site,
"command": slot.command,
"account_label": slot.label,
}
)

if body.create_schedules:
schedule = await schedule_service.create_schedule(
db,
CronScheduleCreate(
source_id=source.id,
name=f"{source.name} · 定时采集",
cron_expression=slot.cron_expression,
timezone=slot.timezone,
parameters={"limit": slot.limit},
enabled=body.schedule_enabled,
),
)
created_schedules.append(
{
"id": schedule.id,
"source_id": source.id,
"cron_expression": schedule.cron_expression,
"timezone": schedule.timezone,
"enabled": schedule.enabled,
}
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

If body.create_schedules is True and a slot has an invalid cron expression, the loop continues, which skips creating the DataSource for that slot entirely. An invalid cron expression should only prevent the creation of the schedule, not the data source itself. Consider creating the source anyway and appending a warning about the skipped schedule.

    for slot in body.account_slots:
        source_data = DataSourceCreate(**_opinion_source_payload(body, slot))
        source = await source_service.create_source(db, source_data)
        created_sources.append(
            {
                "id": source.id,
                "name": source.name,
                "site": slot.site,
                "command": slot.command,
                "account_label": slot.label,
            }
        )

        if body.create_schedules:
            if not schedule_service.validate_cron_expression(slot.cron_expression):
                warnings.append(
                    {
                        "slot": slot.label,
                        "warning": "invalid_cron_expression",
                        "cron_expression": slot.cron_expression,
                        "detail": f"Source '{source.name}' was created, but its schedule was skipped due to an invalid cron expression.",
                    }
                )
            else:
                schedule = await schedule_service.create_schedule(
                    db,
                    CronScheduleCreate(
                        source_id=source.id,
                        name=f"{source.name} · 定时采集",
                        cron_expression=slot.cron_expression,
                        timezone=slot.timezone,
                        parameters={"limit": slot.limit},
                        enabled=body.schedule_enabled,
                    ),
                )
                created_schedules.append(
                    {
                        "id": schedule.id,
                        "source_id": source.id,
                        "cron_expression": schedule.cron_expression,
                        "timezone": schedule.timezone,
                        "enabled": schedule.enabled,
                    }
                )

Comment on lines +46 to +48
def _summary_from_ai(ai: dict[str, Any] | None) -> str:
if not ai:
return ""

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The helper function _summary_from_ai assumes that ai is always a dictionary or None. However, ai_enrichment is a JSON column and could contain other types (like a raw string if the LLM output failed to parse as JSON). Calling .get() on a non-dict object will raise an AttributeError and crash the dashboard. Adding a type check ensures robust and defensive handling.

Suggested change
def _summary_from_ai(ai: dict[str, Any] | None) -> str:
if not ai:
return ""
def _summary_from_ai(ai: dict[str, Any] | None) -> str:
if not isinstance(ai, dict):
return ""

Comment on lines +55 to +57
def _tags_from_ai(ai: dict[str, Any] | None) -> list[str]:
if not ai:
return []

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The helper function _tags_from_ai assumes that ai is always a dictionary or None. However, ai_enrichment is a JSON column and could contain other types (like a raw string if the LLM output failed to parse as JSON). Calling .get() on a non-dict object will raise an AttributeError and crash the dashboard. Adding a type check ensures robust and defensive handling.

Suggested change
def _tags_from_ai(ai: dict[str, Any] | None) -> list[str]:
if not ai:
return []
def _tags_from_ai(ai: dict[str, Any] | None) -> list[str]:
if not isinstance(ai, dict):
return []

Comment on lines +66 to +68
def _sentiment_from_ai(ai: dict[str, Any] | None) -> str:
if not ai:
return "unknown"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The helper function _sentiment_from_ai assumes that ai is always a dictionary or None. However, ai_enrichment is a JSON column and could contain other types (like a raw string if the LLM output failed to parse as JSON). Calling .get() on a non-dict object will raise an AttributeError and crash the dashboard. Adding a type check ensures robust and defensive handling.

Suggested change
def _sentiment_from_ai(ai: dict[str, Any] | None) -> str:
if not ai:
return "unknown"
def _sentiment_from_ai(ai: dict[str, Any] | None) -> str:
if not isinstance(ai, dict):
return "unknown"

Comment on lines +375 to +377
for record_id, status, notifier_type in notification_rows.all():
if record_id and notifier_type == "feishu":
notification_by_record[record_id][status] += 1

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

notification_by_record uses record_id as keys, which might be UUID objects or integers depending on the database dialect and model definition. Later, it queries this dictionary using record.id (which might be a string). To prevent type mismatches (e.g., UUID object vs. string), all keys should be consistently converted to strings using str().

Suggested change
for record_id, status, notifier_type in notification_rows.all():
if record_id and notifier_type == "feishu":
notification_by_record[record_id][status] += 1
for record_id, status, notifier_type in notification_rows.all():
if record_id and notifier_type == "feishu":
notification_by_record[str(record_id)][status] += 1

if record.ai_enrichment:
source_bucket["ai_processed"] += 1

notify_counts = notification_by_record.get(record.id, {})

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

To ensure consistency with the string keys used in notification_by_record, convert record.id to a string when retrieving notification counts.

Suggested change
notify_counts = notification_by_record.get(record.id, {})
notify_counts = notification_by_record.get(str(record.id), {})

Comment on lines +109 to +113
{[...topTags, ...topSentiment].slice(0, 7).map((item) => (
<Badge key={`${item.label}-${item.count}`} variant="secondary">
{item.label} · {item.count}
</Badge>
))}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The combined list [...topTags, ...topSentiment] is mapped to render badges using key={${item.label}-${item.count}}. Since a tag and a sentiment can have the same label (e.g., "positive") and count, this can easily result in duplicate keys, causing React rendering warnings and potential UI bugs. Appending the index to the key guarantees uniqueness.

Suggested change
{[...topTags, ...topSentiment].slice(0, 7).map((item) => (
<Badge key={`${item.label}-${item.count}`} variant="secondary">
{item.label} · {item.count}
</Badge>
))}
{[...topTags, ...topSentiment].slice(0, 7).map((item, index) => (
<Badge key={`${item.label}-${item.count}-${index}`} variant="secondary">
{item.label} · {item.count}
</Badge>
))}

@2233admin 2233admin closed this Jul 6, 2026
@2233admin
2233admin deleted the codex/opinion-monitor-quickstart branch July 6, 2026 19:32
2233admin added a commit that referenced this pull request Jul 8, 2026
…ckManifest schema (PR-A)

Vendor github.com/browser-act/skills @a23131e solutions/ verbatim into
backend/browser_act_packs/ (78 packs, MIT, LICENSE + VENDOR.md attribution).
Packs = upstream SKILL.md + scripts/*.py, byte-unchanged; channel.manifest.json
is our addition (PR-D).

- backend/browser_act_packs/catalog.py: PackCatalog scans <category>/<pack>/SKILL.md,
  parses YAML frontmatter (name/description), derives domain=category /
  capability=pack-dir; reads utf-8-sig (one pack ships a BOM); missing/broken
  frontmatter skipped with a warning, never crashes.
- backend/browser_act_packs/manifest.py: PackManifest schema (param_schema/steps/
  pagination/success per GOAL-7 #5) + load_manifest. Schema only; no manifest
  content yet.
- tests/unit/browser_act_packs/: 18 tests (catalog scan >=20, BOM/bad-frontmatter
  skip, get_pack, manifest validate/reject).

Isolated from the DB-backed backend/skills subsystem (different concept).
1430 -> 1448 passed, zero regression.
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