Persist digest preferences and history
This commit is contained in:
+8
-3
@@ -2,7 +2,7 @@
|
||||
|
||||
## Current Goal
|
||||
|
||||
Automation execution-layer PXX remains closed and its known scope stays frozen. Under explicit user direction, the active product phase is a dense `Decision Assistant` operator workspace with grouped recall, broader replay baselines, note templates, quick-save shortcuts, read-only market context, and a continuous operator cockpit. This run closed C3/G20/H26 as one internal batch: watchlist now exposes a focus matrix and next-focus prompt, review entry now exposes a prioritized review-action queue and handoff summary, and the main `assistant_loop_queue` now carries watchlist focus alongside review-entry actions. Local validation passed, the current working tree was deployed, and remote smoke confirmed the latest Decision Assistant fields on the live host.
|
||||
Automation execution-layer PXX remains closed and its known scope stays frozen. Under explicit user direction, the active product phase is a dense `Decision Assistant` operator workspace with grouped recall, broader replay baselines, note templates, quick-save shortcuts, read-only market context, and a continuous operator cockpit. This run closed F26/H27 as a real backend batch: digest preferences now persist on the server, digest snapshots archive into a history table, cockpit reads back the persisted mode/history archive, and the front-end writes preference changes back through a dedicated API instead of local-only storage. Local validation passed, remote migration `044_create_decision_assistant_digest_state.sql` was applied, the current working tree was deployed, and remote smoke confirmed the latest Decision Assistant fields on the live host.
|
||||
|
||||
## Current Branch
|
||||
|
||||
@@ -129,6 +129,8 @@ Automation execution-layer PXX remains closed and its known scope stays frozen.
|
||||
- latest_decision_assistant_watchlist_focus_slice: watchlist_focus_matrix
|
||||
- latest_decision_assistant_review_action_slice: review_entry_action_queue
|
||||
- latest_decision_assistant_review_handoff_slice: review_entry_handoff_summary
|
||||
- latest_decision_assistant_digest_persistence_slice: digest_preferences_api_archive_history
|
||||
- latest_decision_assistant_digest_archive_slice: persisted_digest_archive_visibility
|
||||
- latest_decision_assistant_summary_status: implemented
|
||||
- latest_decision_assistant_change_summary_status: implemented
|
||||
- latest_decision_assistant_evidence_status: implemented
|
||||
@@ -242,10 +244,12 @@ Automation execution-layer PXX remains closed and its known scope stays frozen.
|
||||
- latest_decision_assistant_watchlist_focus_status: implemented
|
||||
- latest_decision_assistant_review_action_status: implemented
|
||||
- latest_decision_assistant_review_handoff_status: implemented
|
||||
- latest_decision_assistant_digest_persistence_status: implemented
|
||||
- latest_decision_assistant_digest_archive_status: implemented
|
||||
- latest_decision_assistant_workbench_status: visible_local_smoke
|
||||
- latest_remote_decision_assistant_deploy_status: deployed_current_head
|
||||
- latest_remote_decision_assistant_smoke_status: passed
|
||||
- latest_remote_decision_assistant_review_packet_status: watchlist_review_loop_batch_visible
|
||||
- latest_remote_decision_assistant_review_packet_status: digest_preferences_archive_visible
|
||||
- latest_p7_selector_choice: continue_periodic_analysis_decision_monitoring_with_maturity_rollup
|
||||
- latest_p14_slice: webhook_transport_retry
|
||||
- latest_p15_pxx_tasklist_status: ready
|
||||
@@ -661,11 +665,12 @@ Automation execution-layer PXX remains closed and its known scope stays frozen.
|
||||
- Latest E7 validation passed: focused operator workspace tests, 68 related unit/API/static tests, 713 full unit tests, 73 script static checks, `node --check src/web/app.js`, deploy smoke passed 11 endpoints, remote ops smoke passed 9 checks, and remote field smoke confirmed `chart_adapter_mode=optional_reference`, `chart_primary_mode=internal_review_packet_html`, and a non-blocking external chart adapter even when other supporting context sources are stale.
|
||||
- Latest W4/D25/F25/H25 validation passed: focused operator workspace tests, 68 related unit/API/static tests, 713 full unit tests, 73 script static checks, `node --check src/web/app.js`, deploy smoke passed 11 endpoints, remote ops smoke passed 9 checks, and remote field smoke confirmed `workflow_artifact_summary`, `digest_action_queue`, `learning_focus_matrix`, and `workspace_continuity_handoff` plus matching UI strings.
|
||||
- Latest C3/G20/H26 validation passed: focused operator workspace tests, 68 related unit/API/static tests, 713 full unit tests, 73 script static checks, `node --check src/web/app.js`, deploy smoke passed 11 endpoints, remote ops smoke passed 9 checks, and remote field smoke confirmed `watchlist focus matrix`, `review_entry_action_queue`, `review_entry_handoff_summary`, and watchlist-focus presence inside `assistant_loop_queue`.
|
||||
- Latest F26/H27 validation passed: focused operator workspace/API tests, 69 related unit/API/static tests, 714 full unit tests, 73 script static checks, `node --check src/web/app.js`, remote migration `044_create_decision_assistant_digest_state.sql` applied successfully, deploy completed, remote ops smoke passed 9 checks, and remote field smoke confirmed `/trading/alerts/digest-preferences`, persisted `digest_preference_profile`, and `digest_history_archive`.
|
||||
- Real-live order submission remains intentionally disabled because the OKX order adapter is still not a real-live submitter.
|
||||
|
||||
## Exact Next Step
|
||||
|
||||
C3/G20/H26 batch is closed. External chart URLs remain optional reference enhancements and are not required for the current analysis-only loop. Internal non-trading backlog still exists, but PXX remains closed; do not create P25/P26 automatically, and do not enable real trading.
|
||||
F26/H27 backend digest persistence batch is closed. External chart URLs remain optional reference enhancements and are not required for the current analysis-only loop. Internal non-trading backlog still exists, but PXX remains closed; do not create P25/P26 automatically, and do not enable real trading.
|
||||
|
||||
## Validation Commands Used Recently
|
||||
|
||||
|
||||
@@ -160,6 +160,8 @@
|
||||
- [x] C3 watchlist focus matrix / next-focus prompt slice
|
||||
- [x] G20 review entry action queue slice
|
||||
- [x] H26 review/watchlist continuity into loop queue slice
|
||||
- [x] F26 digest preference backend persistence / history archive slice
|
||||
- [x] H27 persisted digest archive / cockpit sync slice
|
||||
- [x] P8 local closure complete
|
||||
- [x] P9 local closure complete
|
||||
- [x] P10 local closure complete
|
||||
@@ -282,6 +284,8 @@
|
||||
- [x] latest_decision_assistant_watchlist_focus_slice: watchlist_focus_matrix
|
||||
- [x] latest_decision_assistant_review_action_slice: review_entry_action_queue
|
||||
- [x] latest_decision_assistant_review_handoff_slice: review_entry_handoff_summary
|
||||
- [x] latest_decision_assistant_digest_persistence_slice: digest_preferences_api_archive_history
|
||||
- [x] latest_decision_assistant_digest_archive_slice: persisted_digest_archive_visibility
|
||||
- [x] latest_decision_assistant_summary_status: implemented
|
||||
- [x] latest_decision_assistant_change_summary_status: implemented
|
||||
- [x] latest_decision_assistant_evidence_status: implemented
|
||||
@@ -392,10 +396,12 @@
|
||||
- [x] latest_decision_assistant_watchlist_focus_status: implemented
|
||||
- [x] latest_decision_assistant_review_action_status: implemented
|
||||
- [x] latest_decision_assistant_review_handoff_status: implemented
|
||||
- [x] latest_decision_assistant_digest_persistence_status: implemented
|
||||
- [x] latest_decision_assistant_digest_archive_status: implemented
|
||||
- [x] latest_decision_assistant_workbench_status: visible_local_smoke
|
||||
- [x] latest_remote_decision_assistant_deploy_status: deployed_current_head
|
||||
- [x] latest_remote_decision_assistant_smoke_status: passed
|
||||
- [x] latest_remote_decision_assistant_review_packet_status: watchlist_review_loop_batch_visible
|
||||
- [x] latest_remote_decision_assistant_review_packet_status: digest_preferences_archive_visible
|
||||
- [x] latest_p7_selector_choice: continue_periodic_analysis_decision_monitoring_with_maturity_rollup
|
||||
- [x] latest_p14_slice: webhook_transport_retry
|
||||
- [x] latest_p15_pxx_tasklist_status: ready
|
||||
@@ -707,6 +713,8 @@
|
||||
- [x] Deployed W4/D25/F25/H25 current tree and passed remote smoke: 11 deploy endpoints, 9 remote ops checks, and review-packet/static field smoke for `workflow_artifact_summary`, `digest_action_queue`, `learning_focus_matrix`, and `workspace_continuity_handoff`
|
||||
- [x] Completed C3/G20/H26 focused validation: operator workspace tests, 68 related unit/API/static tests, 713 full unit tests, 73 script static checks, and dashboard JS syntax
|
||||
- [x] Deployed C3/G20/H26 current tree and passed remote smoke: 11 deploy endpoints, 9 remote ops checks, and review-packet/static field smoke for `watchlist focus matrix`, `review_entry_action_queue`, `review_entry_handoff_summary`, and watchlist-focus presence inside `assistant_loop_queue`
|
||||
- [x] Completed F26/H27 focused validation: operator workspace/API tests, 69 related unit/API/static tests, 714 full unit tests, 73 script static checks, and dashboard JS syntax
|
||||
- [x] Deployed F26/H27 current tree, applied remote migration `044_create_decision_assistant_digest_state.sql`, and passed remote smoke for `/trading/alerts/digest-preferences`, persisted `digest_preference_profile`, and `digest_history_archive`
|
||||
|
||||
## Blocked
|
||||
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
create table if not exists decision_assistant_preferences (
|
||||
id bigserial primary key,
|
||||
profile_key varchar(64) not null,
|
||||
instrument_id bigint not null,
|
||||
digest_mode varchar(32) not null default 'all_reminders',
|
||||
preferred_history_kind varchar(64),
|
||||
importance_threshold integer not null default 70,
|
||||
payload jsonb not null default '{}'::jsonb,
|
||||
updated_ts timestamptz not null default now(),
|
||||
unique (profile_key, instrument_id)
|
||||
);
|
||||
|
||||
create index if not exists idx_decision_assistant_preferences_instrument
|
||||
on decision_assistant_preferences (instrument_id, updated_ts desc);
|
||||
|
||||
create table if not exists decision_assistant_digest_history (
|
||||
id bigserial primary key,
|
||||
profile_key varchar(64) not null default 'local_operator',
|
||||
instrument_id bigint not null,
|
||||
digest_mode varchar(32) not null default 'all_reminders',
|
||||
preferred_history_kind varchar(64),
|
||||
top_summary text,
|
||||
snapshot_signature varchar(128) not null,
|
||||
payload jsonb not null default '{}'::jsonb,
|
||||
created_ts timestamptz not null default now()
|
||||
);
|
||||
|
||||
create index if not exists idx_decision_assistant_digest_history_instrument_ts
|
||||
on decision_assistant_digest_history (instrument_id, created_ts desc);
|
||||
|
||||
create index if not exists idx_decision_assistant_digest_history_signature
|
||||
on decision_assistant_digest_history (instrument_id, snapshot_signature);
|
||||
@@ -57,6 +57,8 @@ from src.api.alerts import (
|
||||
get_alert_quality_summary,
|
||||
get_alert_silent_window_status,
|
||||
get_operator_digest,
|
||||
get_operator_digest_preferences,
|
||||
save_operator_digest_preferences,
|
||||
)
|
||||
from src.api.feedback import (
|
||||
create_alert_feedback,
|
||||
@@ -137,6 +139,8 @@ __all__ = [
|
||||
"get_alert_deliveries",
|
||||
"get_alert_quality_summary",
|
||||
"get_operator_digest",
|
||||
"get_operator_digest_preferences",
|
||||
"save_operator_digest_preferences",
|
||||
"get_alert_silent_window_status",
|
||||
"create_alert_feedback",
|
||||
"create_visual_review_feedback",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from src.repositories.alert_delivery_repository import AlertDeliveryRepository
|
||||
from src.repositories.decision_assistant_repository import DecisionAssistantRepository
|
||||
from src.services.alerts import (
|
||||
AlertDispatcherService,
|
||||
AlertQualitySummaryService,
|
||||
@@ -43,5 +44,47 @@ def get_operator_digest(
|
||||
)
|
||||
|
||||
|
||||
def get_operator_digest_preferences(
|
||||
instrument_id: int,
|
||||
*,
|
||||
profile_key: str = DecisionAssistantRepository.DEFAULT_PROFILE_KEY,
|
||||
) -> dict:
|
||||
repository = DecisionAssistantRepository()
|
||||
payload = repository.fetch_preference(instrument_id=instrument_id, profile_key=profile_key)
|
||||
if payload:
|
||||
return payload
|
||||
return {
|
||||
"profile_key": profile_key,
|
||||
"instrument_id": instrument_id,
|
||||
"digest_mode": "all_reminders",
|
||||
"preferred_history_kind": None,
|
||||
"importance_threshold": 70,
|
||||
"payload": {},
|
||||
"updated_ts": None,
|
||||
"status": "default",
|
||||
}
|
||||
|
||||
|
||||
def save_operator_digest_preferences(
|
||||
instrument_id: int,
|
||||
*,
|
||||
profile_key: str = DecisionAssistantRepository.DEFAULT_PROFILE_KEY,
|
||||
digest_mode: str = "all_reminders",
|
||||
preferred_history_kind: str | None = None,
|
||||
importance_threshold: int = 70,
|
||||
payload: dict | None = None,
|
||||
) -> dict:
|
||||
saved = DecisionAssistantRepository().upsert_preference(
|
||||
instrument_id=instrument_id,
|
||||
profile_key=profile_key,
|
||||
digest_mode=digest_mode,
|
||||
preferred_history_kind=preferred_history_kind,
|
||||
importance_threshold=importance_threshold,
|
||||
payload=payload,
|
||||
)
|
||||
saved["status"] = "saved"
|
||||
return saved
|
||||
|
||||
|
||||
def get_alert_silent_window_status() -> dict:
|
||||
return AlertSilentWindowService().build_status()
|
||||
|
||||
@@ -17,6 +17,8 @@ from src.api.alerts import (
|
||||
get_alert_quality_summary,
|
||||
get_alert_silent_window_status,
|
||||
get_operator_digest,
|
||||
get_operator_digest_preferences,
|
||||
save_operator_digest_preferences,
|
||||
)
|
||||
from src.api.feedback import (
|
||||
create_alert_feedback,
|
||||
@@ -124,6 +126,8 @@ def build_api_dependencies() -> dict:
|
||||
"trading_alert_dispatch": run_trading_alert_dispatch,
|
||||
"trading_alert_quality_summary": get_alert_quality_summary,
|
||||
"trading_alert_operator_digest": get_operator_digest,
|
||||
"trading_alert_operator_digest_preferences": get_operator_digest_preferences,
|
||||
"trading_alert_operator_digest_preferences_save": save_operator_digest_preferences,
|
||||
"trading_alert_silent_window_status": get_alert_silent_window_status,
|
||||
"trading_reconciliation_run": run_trading_reconciliation,
|
||||
"trading_runtime_status": get_trading_runtime_status,
|
||||
@@ -307,6 +311,22 @@ def route_request(
|
||||
visual_limit=visual_limit,
|
||||
top_limit=top_limit,
|
||||
)
|
||||
if method == "GET" and path == "/trading/alerts/digest-preferences":
|
||||
instrument_id = _require_int(query, "instrument_id")
|
||||
return 200, deps["trading_alert_operator_digest_preferences"](
|
||||
instrument_id=instrument_id,
|
||||
profile_key=_get_single(query, "profile_key", "local_operator"),
|
||||
)
|
||||
if method == "POST" and path == "/trading/alerts/digest-preferences":
|
||||
instrument_id = _require_json_int(body or {}, "instrument_id")
|
||||
return 200, deps["trading_alert_operator_digest_preferences_save"](
|
||||
instrument_id=instrument_id,
|
||||
profile_key=_get_body_value(body, "profile_key", "local_operator"),
|
||||
digest_mode=_get_body_value(body, "digest_mode", "all_reminders"),
|
||||
preferred_history_kind=_get_body_value(body, "preferred_history_kind"),
|
||||
importance_threshold=_get_body_optional_int(body, "importance_threshold") or 70,
|
||||
payload=body.get("payload") if isinstance(body, dict) else None,
|
||||
)
|
||||
if method == "POST" and path == "/trading/alerts/dispatch":
|
||||
limit = _get_body_optional_int(body, "limit") or 50
|
||||
return 200, deps["trading_alert_dispatch"](limit=limit)
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
|
||||
from sqlalchemy import text
|
||||
|
||||
from src.db.session import SessionLocal
|
||||
|
||||
|
||||
class DecisionAssistantRepository:
|
||||
DEFAULT_PROFILE_KEY = "local_operator"
|
||||
|
||||
def fetch_preference(self, *, instrument_id: int, profile_key: str = DEFAULT_PROFILE_KEY) -> dict | None:
|
||||
sql = text(
|
||||
"""
|
||||
select id, profile_key, instrument_id, digest_mode, preferred_history_kind,
|
||||
importance_threshold, payload, updated_ts
|
||||
from decision_assistant_preferences
|
||||
where instrument_id = :instrument_id and profile_key = :profile_key
|
||||
limit 1
|
||||
"""
|
||||
)
|
||||
with SessionLocal() as session:
|
||||
row = session.execute(
|
||||
sql,
|
||||
{"instrument_id": instrument_id, "profile_key": profile_key},
|
||||
).mappings().first()
|
||||
return dict(row) if row else None
|
||||
|
||||
def upsert_preference(
|
||||
self,
|
||||
*,
|
||||
instrument_id: int,
|
||||
profile_key: str = DEFAULT_PROFILE_KEY,
|
||||
digest_mode: str = "all_reminders",
|
||||
preferred_history_kind: str | None = None,
|
||||
importance_threshold: int = 70,
|
||||
payload: dict | None = None,
|
||||
updated_ts=None,
|
||||
) -> dict:
|
||||
sql = text(
|
||||
"""
|
||||
insert into decision_assistant_preferences (
|
||||
profile_key,
|
||||
instrument_id,
|
||||
digest_mode,
|
||||
preferred_history_kind,
|
||||
importance_threshold,
|
||||
payload,
|
||||
updated_ts
|
||||
) values (
|
||||
:profile_key,
|
||||
:instrument_id,
|
||||
:digest_mode,
|
||||
:preferred_history_kind,
|
||||
:importance_threshold,
|
||||
cast(:payload as jsonb),
|
||||
coalesce(:updated_ts, now())
|
||||
)
|
||||
on conflict (profile_key, instrument_id) do update
|
||||
set digest_mode = excluded.digest_mode,
|
||||
preferred_history_kind = excluded.preferred_history_kind,
|
||||
importance_threshold = excluded.importance_threshold,
|
||||
payload = excluded.payload,
|
||||
updated_ts = excluded.updated_ts
|
||||
returning id, profile_key, instrument_id, digest_mode, preferred_history_kind,
|
||||
importance_threshold, payload, updated_ts
|
||||
"""
|
||||
)
|
||||
with SessionLocal.begin() as session:
|
||||
row = session.execute(
|
||||
sql,
|
||||
{
|
||||
"profile_key": profile_key,
|
||||
"instrument_id": instrument_id,
|
||||
"digest_mode": digest_mode,
|
||||
"preferred_history_kind": preferred_history_kind,
|
||||
"importance_threshold": importance_threshold,
|
||||
"payload": json.dumps(payload or {}, ensure_ascii=False, default=str),
|
||||
"updated_ts": updated_ts,
|
||||
},
|
||||
).mappings().one()
|
||||
return dict(row)
|
||||
|
||||
def fetch_recent_digest_history(
|
||||
self,
|
||||
*,
|
||||
instrument_id: int,
|
||||
profile_key: str = DEFAULT_PROFILE_KEY,
|
||||
limit: int = 20,
|
||||
) -> list[dict]:
|
||||
sql = text(
|
||||
"""
|
||||
select id, profile_key, instrument_id, digest_mode, preferred_history_kind,
|
||||
top_summary, snapshot_signature, payload, created_ts
|
||||
from decision_assistant_digest_history
|
||||
where instrument_id = :instrument_id and profile_key = :profile_key
|
||||
order by created_ts desc, id desc
|
||||
limit :limit
|
||||
"""
|
||||
)
|
||||
with SessionLocal() as session:
|
||||
rows = session.execute(
|
||||
sql,
|
||||
{"instrument_id": instrument_id, "profile_key": profile_key, "limit": limit},
|
||||
).mappings().all()
|
||||
return [dict(row) for row in rows]
|
||||
|
||||
def fetch_latest_digest_history(
|
||||
self,
|
||||
*,
|
||||
instrument_id: int,
|
||||
profile_key: str = DEFAULT_PROFILE_KEY,
|
||||
) -> dict | None:
|
||||
rows = self.fetch_recent_digest_history(instrument_id=instrument_id, profile_key=profile_key, limit=1)
|
||||
return rows[0] if rows else None
|
||||
|
||||
def insert_digest_history(
|
||||
self,
|
||||
*,
|
||||
instrument_id: int,
|
||||
profile_key: str = DEFAULT_PROFILE_KEY,
|
||||
digest_mode: str = "all_reminders",
|
||||
preferred_history_kind: str | None = None,
|
||||
top_summary: str | None = None,
|
||||
snapshot_signature: str,
|
||||
payload: dict | None = None,
|
||||
created_ts=None,
|
||||
) -> dict:
|
||||
sql = text(
|
||||
"""
|
||||
insert into decision_assistant_digest_history (
|
||||
profile_key,
|
||||
instrument_id,
|
||||
digest_mode,
|
||||
preferred_history_kind,
|
||||
top_summary,
|
||||
snapshot_signature,
|
||||
payload,
|
||||
created_ts
|
||||
) values (
|
||||
:profile_key,
|
||||
:instrument_id,
|
||||
:digest_mode,
|
||||
:preferred_history_kind,
|
||||
:top_summary,
|
||||
:snapshot_signature,
|
||||
cast(:payload as jsonb),
|
||||
coalesce(:created_ts, now())
|
||||
)
|
||||
returning id, profile_key, instrument_id, digest_mode, preferred_history_kind,
|
||||
top_summary, snapshot_signature, payload, created_ts
|
||||
"""
|
||||
)
|
||||
with SessionLocal.begin() as session:
|
||||
row = session.execute(
|
||||
sql,
|
||||
{
|
||||
"profile_key": profile_key,
|
||||
"instrument_id": instrument_id,
|
||||
"digest_mode": digest_mode,
|
||||
"preferred_history_kind": preferred_history_kind,
|
||||
"top_summary": top_summary,
|
||||
"snapshot_signature": snapshot_signature,
|
||||
"payload": json.dumps(payload or {}, ensure_ascii=False, default=str),
|
||||
"created_ts": created_ts,
|
||||
},
|
||||
).mappings().one()
|
||||
return dict(row)
|
||||
@@ -1,9 +1,12 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, timezone
|
||||
import hashlib
|
||||
import json
|
||||
from typing import Optional
|
||||
|
||||
from src.repositories.decision_packet_repository import DecisionPacketRepository
|
||||
from src.repositories.decision_assistant_repository import DecisionAssistantRepository
|
||||
from src.repositories.market_session_repository import MarketSessionRepository
|
||||
from src.repositories.operator_note_repository import OperatorNoteRepository
|
||||
from src.repositories.trade_plan_candidate_repository import TradePlanCandidateRepository
|
||||
@@ -127,6 +130,7 @@ class DecisionAssistantSummaryService:
|
||||
research_council_service: Optional[ResearchCouncilService] = None,
|
||||
exchange_status_provider: Optional[OkxSystemStatusProvider] = None,
|
||||
announcements_provider: Optional[OkxAnnouncementsProvider] = None,
|
||||
decision_assistant_repository: Optional[DecisionAssistantRepository] = None,
|
||||
) -> None:
|
||||
self.launch_readiness_service = launch_readiness_service or LaunchReadinessService()
|
||||
self.session_focus_service = session_focus_service or SessionFocusService()
|
||||
@@ -136,6 +140,7 @@ class DecisionAssistantSummaryService:
|
||||
self.research_council_service = research_council_service or ResearchCouncilService()
|
||||
self.exchange_status_provider = exchange_status_provider or OkxSystemStatusProvider()
|
||||
self.announcements_provider = announcements_provider or OkxAnnouncementsProvider()
|
||||
self.decision_assistant_repository = decision_assistant_repository or DecisionAssistantRepository()
|
||||
|
||||
def build_summary(
|
||||
self,
|
||||
@@ -165,7 +170,15 @@ class DecisionAssistantSummaryService:
|
||||
instrument_id=instrument_id,
|
||||
fallback_session_code=fallback_session_code or None,
|
||||
)
|
||||
return build_decision_assistant_summary(
|
||||
persisted_digest_preference = _safe_fetch_digest_preference(
|
||||
repository=self.decision_assistant_repository,
|
||||
instrument_id=instrument_id,
|
||||
)
|
||||
persisted_digest_history = _safe_fetch_digest_history(
|
||||
repository=self.decision_assistant_repository,
|
||||
instrument_id=instrument_id,
|
||||
)
|
||||
summary = build_decision_assistant_summary(
|
||||
instrument_id=instrument_id,
|
||||
timeframe=timeframe,
|
||||
decision_packet=dict(decision_packet or {}),
|
||||
@@ -181,7 +194,28 @@ class DecisionAssistantSummaryService:
|
||||
recent_alerts=list(recent_alerts or []),
|
||||
recent_notes=list(recent_notes or []),
|
||||
recent_executions=list(recent_executions or []),
|
||||
persisted_digest_preference=dict(persisted_digest_preference or {}),
|
||||
persisted_digest_history=list(persisted_digest_history or []),
|
||||
)
|
||||
persistence_result = _safe_persist_digest_history_snapshot(
|
||||
repository=self.decision_assistant_repository,
|
||||
instrument_id=instrument_id,
|
||||
summary=summary,
|
||||
)
|
||||
refreshed_digest_history = _safe_fetch_digest_history(
|
||||
repository=self.decision_assistant_repository,
|
||||
instrument_id=instrument_id,
|
||||
)
|
||||
digest_history_archive = _build_decision_assistant_digest_history_archive(refreshed_digest_history)
|
||||
summary["digest_history_archive"] = digest_history_archive
|
||||
summary["digest_persistence_status"] = persistence_result.get("status")
|
||||
digest_surface = dict(summary.get("digest_surface") or {})
|
||||
digest_surface["digest_history_archive"] = digest_history_archive
|
||||
digest_surface["digest_history_archive_count"] = digest_history_archive.get("snapshot_count") or 0
|
||||
digest_surface["next_digest_history_archive_review"] = digest_history_archive.get("next_review_prompt")
|
||||
digest_surface["digest_persistence_status"] = persistence_result.get("status")
|
||||
summary["digest_surface"] = digest_surface
|
||||
return summary
|
||||
|
||||
|
||||
class ReviewPacketService:
|
||||
@@ -247,6 +281,57 @@ class ReviewPacketService:
|
||||
}
|
||||
|
||||
|
||||
def _safe_fetch_digest_preference(*, repository: DecisionAssistantRepository, instrument_id: int) -> dict:
|
||||
try:
|
||||
return dict(repository.fetch_preference(instrument_id=instrument_id) or {})
|
||||
except Exception:
|
||||
return {}
|
||||
|
||||
|
||||
def _safe_fetch_digest_history(*, repository: DecisionAssistantRepository, instrument_id: int, limit: int = 20) -> list[dict]:
|
||||
try:
|
||||
return list(repository.fetch_recent_digest_history(instrument_id=instrument_id, limit=limit) or [])
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
|
||||
def _safe_persist_digest_history_snapshot(
|
||||
*,
|
||||
repository: DecisionAssistantRepository,
|
||||
instrument_id: int,
|
||||
summary: dict,
|
||||
) -> dict:
|
||||
digest_surface = dict(summary.get("digest_surface") or {})
|
||||
preference_profile = dict(digest_surface.get("digest_preference_profile") or {})
|
||||
snapshot_payload = {
|
||||
"focus_mode": summary.get("focus_mode"),
|
||||
"headline": summary.get("headline"),
|
||||
"digest_mode": preference_profile.get("default_mode"),
|
||||
"preferred_history_kind": preference_profile.get("preferred_history_kind"),
|
||||
"top_summary": digest_surface.get("summary"),
|
||||
"top_opportunity_title": digest_surface.get("top_opportunity_title"),
|
||||
"next_digest_action_label": dict(digest_surface.get("next_digest_action") or {}).get("label"),
|
||||
"reminder_count": digest_surface.get("reminder_count"),
|
||||
"high_value_reminder_count": digest_surface.get("high_value_reminder_count"),
|
||||
}
|
||||
signature = hashlib.sha1(json.dumps(snapshot_payload, sort_keys=True, ensure_ascii=False, default=str).encode("utf-8")).hexdigest()
|
||||
try:
|
||||
latest = repository.fetch_latest_digest_history(instrument_id=instrument_id)
|
||||
if latest and str(latest.get("snapshot_signature") or "") == signature:
|
||||
return {"status": "duplicate_skipped", "snapshot_signature": signature}
|
||||
repository.insert_digest_history(
|
||||
instrument_id=instrument_id,
|
||||
digest_mode=str(preference_profile.get("default_mode") or "all_reminders"),
|
||||
preferred_history_kind=preference_profile.get("preferred_history_kind"),
|
||||
top_summary=str(digest_surface.get("summary") or ""),
|
||||
snapshot_signature=signature,
|
||||
payload=snapshot_payload,
|
||||
)
|
||||
return {"status": "persisted", "snapshot_signature": signature}
|
||||
except Exception:
|
||||
return {"status": "unavailable", "snapshot_signature": signature}
|
||||
|
||||
|
||||
def _build_research_council_summary(run: Optional[dict]) -> dict:
|
||||
payload = dict(((run or {}).get("payload") or {}).get("decision") or {})
|
||||
return {
|
||||
@@ -281,6 +366,8 @@ def build_decision_assistant_summary(
|
||||
recent_alerts: list[dict],
|
||||
recent_notes: list[dict],
|
||||
recent_executions: list[dict],
|
||||
persisted_digest_preference: dict,
|
||||
persisted_digest_history: list[dict],
|
||||
) -> dict:
|
||||
market_state = dict(launch_readiness.get("market_state") or {})
|
||||
session_state = dict(market_state.get("session") or {})
|
||||
@@ -425,6 +512,7 @@ def build_decision_assistant_summary(
|
||||
digest_history_preview,
|
||||
recommended_kind=digest_history_summary.get("recommended_kind"),
|
||||
)
|
||||
digest_history_archive = _build_decision_assistant_digest_history_archive(persisted_digest_history)
|
||||
digest_surface = _build_decision_assistant_digest_surface(
|
||||
focus_mode=focus_mode,
|
||||
personalized_reminders=personalized_reminders,
|
||||
@@ -433,6 +521,8 @@ def build_decision_assistant_summary(
|
||||
digest_history_preview=digest_history_preview,
|
||||
digest_history_summary=digest_history_summary,
|
||||
digest_history_groups=digest_history_groups,
|
||||
persisted_digest_preference=persisted_digest_preference,
|
||||
digest_history_archive=digest_history_archive,
|
||||
)
|
||||
digest_history_browser = dict(digest_surface.get("history_browser") or {})
|
||||
decision_timeline = _build_decision_assistant_timeline(
|
||||
@@ -506,6 +596,7 @@ def build_decision_assistant_summary(
|
||||
"digest_history_summary": digest_history_summary,
|
||||
"digest_history_groups": digest_history_groups,
|
||||
"digest_history_browser": digest_history_browser,
|
||||
"digest_history_archive": digest_history_archive,
|
||||
"digest_surface": digest_surface,
|
||||
"setup_learning_rollup": setup_learning_rollup,
|
||||
"decision_timeline": decision_timeline,
|
||||
@@ -3235,6 +3326,8 @@ def _build_decision_assistant_digest_surface(
|
||||
digest_history_preview: list[dict],
|
||||
digest_history_summary: dict,
|
||||
digest_history_groups: list[dict],
|
||||
persisted_digest_preference: dict,
|
||||
digest_history_archive: dict,
|
||||
) -> dict:
|
||||
high_value_reminders = [
|
||||
dict(item)
|
||||
@@ -3244,7 +3337,9 @@ def _build_decision_assistant_digest_surface(
|
||||
digest_preference_profile = _build_digest_preference_profile(
|
||||
personalized_reminders=personalized_reminders,
|
||||
digest_history_summary=digest_history_summary,
|
||||
persisted_digest_preference=persisted_digest_preference,
|
||||
)
|
||||
importance_threshold = int((persisted_digest_preference or {}).get("importance_threshold") or 70)
|
||||
digest_review_back_actions = _build_digest_review_back_actions(
|
||||
digest_history_preview=digest_history_preview,
|
||||
digest_history_summary=digest_history_summary,
|
||||
@@ -3266,7 +3361,7 @@ def _build_decision_assistant_digest_surface(
|
||||
"high_priority_count": sum(1 for item in personalized_reminders if str(item.get("severity") or "") == "high"),
|
||||
"high_value_reminders": high_value_reminders[:5],
|
||||
"high_value_reminder_count": len(high_value_reminders),
|
||||
"important_change_filter": _digest_important_change_filter(personalized_reminders),
|
||||
"important_change_filter": _digest_important_change_filter(personalized_reminders, threshold=importance_threshold),
|
||||
"importance_templates": _digest_importance_templates(),
|
||||
"digest_preference_profile": digest_preference_profile,
|
||||
"next_high_value_change": high_value_reminders[0] if high_value_reminders else None,
|
||||
@@ -3280,6 +3375,9 @@ def _build_decision_assistant_digest_surface(
|
||||
"digest_action_queue": digest_action_queue[:6],
|
||||
"digest_action_queue_count": len(digest_action_queue),
|
||||
"next_digest_action": digest_action_queue[0] if digest_action_queue else None,
|
||||
"digest_history_archive": digest_history_archive,
|
||||
"digest_history_archive_count": digest_history_archive.get("snapshot_count") or 0,
|
||||
"next_digest_history_archive_review": digest_history_archive.get("next_review_prompt"),
|
||||
"top_opportunity_title": top_opportunities[0].get("title") if top_opportunities else None,
|
||||
"learning_status": learning_rollup.get("status") or "idle",
|
||||
"history_count": len(digest_history_preview),
|
||||
@@ -3323,12 +3421,13 @@ def _reminder_high_value_reason(reminder: dict) -> str:
|
||||
return "below_high_value_threshold"
|
||||
|
||||
|
||||
def _digest_important_change_filter(personalized_reminders: list[dict]) -> dict:
|
||||
def _digest_important_change_filter(personalized_reminders: list[dict], *, threshold: int = 70) -> dict:
|
||||
effective_threshold = max(0, int(threshold or 70))
|
||||
return {
|
||||
"threshold": 70,
|
||||
"threshold": effective_threshold,
|
||||
"mode": "high_value_only",
|
||||
"input_count": len(personalized_reminders),
|
||||
"passed_count": sum(1 for item in personalized_reminders if bool(item.get("high_value"))),
|
||||
"passed_count": sum(1 for item in personalized_reminders if int(item.get("importance_score") or 0) >= effective_threshold),
|
||||
"template_counts": _digest_importance_template_counts(personalized_reminders),
|
||||
}
|
||||
|
||||
@@ -3351,7 +3450,12 @@ def _digest_importance_templates() -> list[dict]:
|
||||
]
|
||||
|
||||
|
||||
def _build_digest_preference_profile(*, personalized_reminders: list[dict], digest_history_summary: dict) -> dict:
|
||||
def _build_digest_preference_profile(
|
||||
*,
|
||||
personalized_reminders: list[dict],
|
||||
digest_history_summary: dict,
|
||||
persisted_digest_preference: dict,
|
||||
) -> dict:
|
||||
template_counts = _digest_importance_template_counts(personalized_reminders)
|
||||
preferred_template = next(
|
||||
(
|
||||
@@ -3365,20 +3469,26 @@ def _build_digest_preference_profile(*, personalized_reminders: list[dict], dige
|
||||
None,
|
||||
)
|
||||
high_value_count = sum(1 for item in personalized_reminders if bool(item.get("high_value")))
|
||||
default_mode = "high_value_only" if high_value_count else "all_reminders"
|
||||
default_mode = str((persisted_digest_preference or {}).get("digest_mode") or ("high_value_only" if high_value_count else "all_reminders"))
|
||||
if preferred_template == "risk_first_change":
|
||||
default_mode = "risk_first"
|
||||
persisted_threshold = int((persisted_digest_preference or {}).get("importance_threshold") or 70)
|
||||
preferred_history_kind = (persisted_digest_preference or {}).get("preferred_history_kind") or digest_history_summary.get("recommended_kind")
|
||||
return {
|
||||
"status": "active" if personalized_reminders else "idle",
|
||||
"default_mode": default_mode,
|
||||
"available_modes": ["all_reminders", "high_value_only", "risk_first"],
|
||||
"preferred_history_kind": digest_history_summary.get("recommended_kind"),
|
||||
"preferred_history_kind": preferred_history_kind,
|
||||
"preferred_importance_template": preferred_template,
|
||||
"importance_threshold": persisted_threshold,
|
||||
"persistence_status": "persisted" if persisted_digest_preference else "computed_default",
|
||||
"persisted_digest_mode": (persisted_digest_preference or {}).get("digest_mode"),
|
||||
"persisted_updated_ts": (persisted_digest_preference or {}).get("updated_ts"),
|
||||
"high_value_only_recommended": high_value_count > 0,
|
||||
"profile_reason": _digest_preference_profile_reason(
|
||||
preferred_template=preferred_template,
|
||||
high_value_count=high_value_count,
|
||||
recommended_kind=str(digest_history_summary.get("recommended_kind") or ""),
|
||||
recommended_kind=str(preferred_history_kind or ""),
|
||||
),
|
||||
}
|
||||
|
||||
@@ -3552,6 +3662,47 @@ def _build_digest_action_queue(
|
||||
return queue
|
||||
|
||||
|
||||
def _build_decision_assistant_digest_history_archive(rows: list[dict]) -> dict:
|
||||
if not rows:
|
||||
return {
|
||||
"status": "idle",
|
||||
"snapshot_count": 0,
|
||||
"latest_created_ts": None,
|
||||
"latest_summary": None,
|
||||
"next_review_prompt": None,
|
||||
"items": [],
|
||||
}
|
||||
items = []
|
||||
for row in rows[:12]:
|
||||
payload = dict(row.get("payload") or {})
|
||||
items.append(
|
||||
{
|
||||
"created_ts": row.get("created_ts"),
|
||||
"digest_mode": row.get("digest_mode"),
|
||||
"preferred_history_kind": row.get("preferred_history_kind"),
|
||||
"top_summary": row.get("top_summary"),
|
||||
"focus_mode": payload.get("focus_mode"),
|
||||
"top_opportunity_title": payload.get("top_opportunity_title"),
|
||||
"next_digest_action_label": payload.get("next_digest_action_label"),
|
||||
"high_value_reminder_count": payload.get("high_value_reminder_count"),
|
||||
"reminder_count": payload.get("reminder_count"),
|
||||
}
|
||||
)
|
||||
latest = dict(items[0] or {})
|
||||
return {
|
||||
"status": "active",
|
||||
"snapshot_count": len(rows),
|
||||
"latest_created_ts": latest.get("created_ts"),
|
||||
"latest_summary": latest.get("top_summary"),
|
||||
"next_review_prompt": (
|
||||
f"Review persisted digest snapshot from {latest.get('created_ts')} before comparing today's recall drift."
|
||||
if latest.get("created_ts")
|
||||
else None
|
||||
),
|
||||
"items": items,
|
||||
}
|
||||
|
||||
|
||||
def _digest_history_browser_hint(*, recommended_kind: str, total_count: int) -> str:
|
||||
if recommended_kind:
|
||||
return f"Open {recommended_kind} first, then review the remaining {total_count} digest history item(s) by freshness debt."
|
||||
|
||||
+86
-13
@@ -82,9 +82,9 @@ function revealPanel(node) {
|
||||
|
||||
function loadAssistantHistoryKindFilter() {
|
||||
try {
|
||||
return window.localStorage?.getItem('assistantHistoryKindFilter') || 'all';
|
||||
return window.localStorage?.getItem('assistantHistoryKindFilter') || '';
|
||||
} catch {
|
||||
return 'all';
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,9 +99,9 @@ function persistAssistantHistoryKindFilter(value) {
|
||||
|
||||
function loadAssistantDigestPreferenceMode() {
|
||||
try {
|
||||
return window.localStorage?.getItem('assistantDigestPreferenceMode') || 'all_reminders';
|
||||
return window.localStorage?.getItem('assistantDigestPreferenceMode') || '';
|
||||
} catch {
|
||||
return 'all_reminders';
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -114,6 +114,22 @@ function persistAssistantDigestPreferenceMode(value) {
|
||||
}
|
||||
}
|
||||
|
||||
async function persistAssistantDigestPreferenceServer({ digestMode, preferredHistoryKind, importanceThreshold = 70 }) {
|
||||
try {
|
||||
await postJson('/trading/alerts/digest-preferences', {
|
||||
instrument_id: Number(instrumentIdInput.value || 1),
|
||||
digest_mode: digestMode || 'all_reminders',
|
||||
preferred_history_kind: preferredHistoryKind || null,
|
||||
importance_threshold: Number(importanceThreshold || 70),
|
||||
payload: {
|
||||
source: 'decision_assistant_workbench',
|
||||
},
|
||||
});
|
||||
} catch {
|
||||
// no-op
|
||||
}
|
||||
}
|
||||
|
||||
function loadAssistantScanHandoffMarker() {
|
||||
try {
|
||||
const raw = window.localStorage?.getItem('assistantScanHandoffMarker');
|
||||
@@ -522,7 +538,14 @@ function renderDecisionAssistant(summary) {
|
||||
const continuousWorkspaceRail = summary.continuous_workspace_rail || [];
|
||||
const continuousWorkspaceSummary = summary.continuous_workspace_summary || null;
|
||||
const workspaceContinuityHandoff = summary.workspace_continuity_handoff || null;
|
||||
const visibleReminders = filterAssistantReminders(personalizedReminders, assistantDigestPreferenceMode);
|
||||
const effectiveDigestPreferenceMode = assistantDigestPreferenceMode
|
||||
|| digestPreferenceProfile?.persisted_digest_mode
|
||||
|| digestPreferenceProfile?.default_mode
|
||||
|| 'all_reminders';
|
||||
const activeHistoryFilter = assistantHistoryKindFilter
|
||||
|| digestPreferenceProfile?.preferred_history_kind
|
||||
|| 'all';
|
||||
const visibleReminders = filterAssistantReminders(personalizedReminders, effectiveDigestPreferenceMode);
|
||||
const reminderCards = visibleReminders.length ? visibleReminders : personalizedReminders;
|
||||
const savedHandoffAge = resolveAssistantHandoffAge(assistantScanHandoffMarker, scanHandoffSummary?.default_stale_after_seconds);
|
||||
const assistantSections = [
|
||||
@@ -850,8 +873,11 @@ function renderDecisionAssistant(summary) {
|
||||
['reminders', String(digestSurface.reminder_count ?? 0)],
|
||||
['high priority reminders', String(digestSurface.high_priority_count ?? 0)],
|
||||
['high value reminders', String(digestSurface.high_value_reminder_count ?? 0)],
|
||||
['digest preference', assistantDigestPreferenceMode || 'all_reminders'],
|
||||
['digest preference', effectiveDigestPreferenceMode || 'all_reminders'],
|
||||
['preference default', digestPreferenceProfile?.default_mode || 'n/a'],
|
||||
['preference persistence', digestPreferenceProfile?.persistence_status || 'n/a'],
|
||||
['preference threshold', String(digestPreferenceProfile?.importance_threshold ?? 'n/a')],
|
||||
['preference updated', formatTimestamp(digestPreferenceProfile?.persisted_updated_ts)],
|
||||
['preferred history kind', digestPreferenceProfile?.preferred_history_kind || 'n/a'],
|
||||
['preferred template', digestPreferenceProfile?.preferred_importance_template || 'n/a'],
|
||||
['preference reason', digestPreferenceProfile?.profile_reason || 'n/a'],
|
||||
@@ -869,6 +895,9 @@ function renderDecisionAssistant(summary) {
|
||||
['digest action queue', String(digestSurface.digest_action_queue_count ?? 0)],
|
||||
['next digest action', digestSurface.next_digest_action?.label || 'n/a'],
|
||||
['next digest prompt', digestSurface.next_digest_action?.prompt || 'n/a'],
|
||||
['digest archive', String(digestSurface.digest_history_archive_count ?? 0)],
|
||||
['next archive review', digestSurface.next_digest_history_archive_review || 'n/a'],
|
||||
['digest persistence', digestSurface.digest_persistence_status || 'n/a'],
|
||||
['digest history', String(digestSurface.history_count ?? 0)],
|
||||
['digest kinds', formatList(digestSurface.history_summary?.kinds || [])],
|
||||
['recommended history kind', digestSurface.history_summary?.recommended_kind || 'n/a'],
|
||||
@@ -912,7 +941,7 @@ function renderDecisionAssistant(summary) {
|
||||
['confirmation trail', formatList((digestSurface.history_summary?.refresh_confirmation_trail || []).map((item) => `${item.kind}:${item.proof_label}`))],
|
||||
['refresh evidence anchors', String(digestSurface.history_summary?.refresh_evidence_anchor_count ?? 0)],
|
||||
['refresh prompts', formatList((digestSurface.history_summary?.refresh_prompts || []).map((item) => `${item.kind}: ${item.prompt}`))],
|
||||
['history filter', assistantHistoryKindFilter || 'all'],
|
||||
['history filter', activeHistoryFilter || 'all'],
|
||||
['digest latest', formatTimestamp(digestSurface.history_summary?.latest_ts)],
|
||||
['digest top opportunity', digestSurface.top_opportunity_title || 'n/a'],
|
||||
['learning status', digestSurface.learning_status || 'n/a'],
|
||||
@@ -941,6 +970,11 @@ function renderDecisionAssistant(summary) {
|
||||
label: `${item.label || item.kind}${item.recommended ? ' *' : ''}`,
|
||||
onClick: () => {
|
||||
persistAssistantHistoryKindFilter(item.kind || 'all');
|
||||
persistAssistantDigestPreferenceServer({
|
||||
digestMode: effectiveDigestPreferenceMode,
|
||||
preferredHistoryKind: item.kind || 'all',
|
||||
importanceThreshold: digestPreferenceProfile?.importance_threshold || 70,
|
||||
});
|
||||
renderDecisionAssistant(window.__latestDecisionAssistantSummary || summary);
|
||||
},
|
||||
}))
|
||||
@@ -953,12 +987,36 @@ function renderDecisionAssistant(summary) {
|
||||
label: mode === 'all_reminders' ? 'All Reminders' : (mode === 'high_value_only' ? 'High Value' : 'Risk First'),
|
||||
onClick: () => {
|
||||
persistAssistantDigestPreferenceMode(mode);
|
||||
persistAssistantDigestPreferenceServer({
|
||||
digestMode: mode,
|
||||
preferredHistoryKind: activeHistoryFilter === 'all' ? null : activeHistoryFilter,
|
||||
importanceThreshold: digestPreferenceProfile?.importance_threshold || 70,
|
||||
});
|
||||
renderDecisionAssistant(window.__latestDecisionAssistantSummary || summary);
|
||||
},
|
||||
}))
|
||||
);
|
||||
decisionAssistantPanel.appendChild(preferenceBar);
|
||||
}
|
||||
if (digestSurface?.digest_history_archive?.items?.length) {
|
||||
const archiveGrid = createElement('div', 'cards assistant-tight-grid');
|
||||
(digestSurface.digest_history_archive.items || []).forEach((item) => {
|
||||
archiveGrid.appendChild(
|
||||
buildItemCard(
|
||||
item.top_summary || item.top_opportunity_title || 'digest snapshot',
|
||||
item.digest_mode || 'digest',
|
||||
[
|
||||
['time', formatTimestamp(item.created_ts)],
|
||||
['history kind', item.preferred_history_kind || 'n/a'],
|
||||
['next action', item.next_digest_action_label || 'n/a'],
|
||||
['high value reminders', String(item.high_value_reminder_count ?? 0)],
|
||||
['reminders', String(item.reminder_count ?? 0)],
|
||||
]
|
||||
)
|
||||
);
|
||||
});
|
||||
decisionAssistantPanel.appendChild(archiveGrid);
|
||||
}
|
||||
if (reminderCards.length) {
|
||||
const reminderGrid = createElement('div', 'cards assistant-tight-grid');
|
||||
reminderCards.forEach((item) => {
|
||||
@@ -982,7 +1040,7 @@ function renderDecisionAssistant(summary) {
|
||||
if (digestHistoryGroups.length) {
|
||||
const recommendedKind = digestSurface?.history_summary?.recommended_kind || 'all';
|
||||
const groupKinds = digestHistoryGroups.map((item) => item.kind);
|
||||
if (assistantHistoryKindFilter !== 'all' && !groupKinds.includes(assistantHistoryKindFilter)) {
|
||||
if (activeHistoryFilter !== 'all' && !groupKinds.includes(activeHistoryFilter)) {
|
||||
persistAssistantHistoryKindFilter(recommendedKind);
|
||||
}
|
||||
const historyFilterBar = buildActionBar([
|
||||
@@ -990,6 +1048,11 @@ function renderDecisionAssistant(summary) {
|
||||
label: 'All History',
|
||||
onClick: () => {
|
||||
persistAssistantHistoryKindFilter('all');
|
||||
persistAssistantDigestPreferenceServer({
|
||||
digestMode: effectiveDigestPreferenceMode,
|
||||
preferredHistoryKind: null,
|
||||
importanceThreshold: digestPreferenceProfile?.importance_threshold || 70,
|
||||
});
|
||||
renderDecisionAssistant(window.__latestDecisionAssistantSummary || summary);
|
||||
},
|
||||
},
|
||||
@@ -998,6 +1061,11 @@ function renderDecisionAssistant(summary) {
|
||||
label: `Recommended ${recommendedKind}`,
|
||||
onClick: () => {
|
||||
persistAssistantHistoryKindFilter(recommendedKind);
|
||||
persistAssistantDigestPreferenceServer({
|
||||
digestMode: effectiveDigestPreferenceMode,
|
||||
preferredHistoryKind: recommendedKind,
|
||||
importanceThreshold: digestPreferenceProfile?.importance_threshold || 70,
|
||||
});
|
||||
renderDecisionAssistant(window.__latestDecisionAssistantSummary || summary);
|
||||
},
|
||||
}]
|
||||
@@ -1006,6 +1074,11 @@ function renderDecisionAssistant(summary) {
|
||||
label: `${item.kind} (${item.count})${item.recommended ? ' *' : ''}`,
|
||||
onClick: () => {
|
||||
persistAssistantHistoryKindFilter(item.kind || 'all');
|
||||
persistAssistantDigestPreferenceServer({
|
||||
digestMode: effectiveDigestPreferenceMode,
|
||||
preferredHistoryKind: item.kind || null,
|
||||
importanceThreshold: digestPreferenceProfile?.importance_threshold || 70,
|
||||
});
|
||||
renderDecisionAssistant(window.__latestDecisionAssistantSummary || summary);
|
||||
},
|
||||
})),
|
||||
@@ -1013,14 +1086,14 @@ function renderDecisionAssistant(summary) {
|
||||
decisionAssistantPanel.appendChild(historyFilterBar);
|
||||
decisionAssistantPanel.appendChild(
|
||||
buildInfoGrid([
|
||||
['active history focus', assistantHistoryKindFilter || 'all'],
|
||||
['active history focus', activeHistoryFilter || 'all'],
|
||||
['recommended history focus', recommendedKind || 'n/a'],
|
||||
])
|
||||
);
|
||||
const groupGrid = createElement('div', 'cards assistant-history-grid');
|
||||
const visibleGroups = assistantHistoryKindFilter === 'all'
|
||||
const visibleGroups = activeHistoryFilter === 'all'
|
||||
? digestHistoryGroups
|
||||
: digestHistoryGroups.filter((item) => String(item.kind || '') === assistantHistoryKindFilter);
|
||||
: digestHistoryGroups.filter((item) => String(item.kind || '') === activeHistoryFilter);
|
||||
visibleGroups.forEach((item) => {
|
||||
const card = buildItemCard(
|
||||
item.recommended ? `Recommended ${item.kind || 'history'}` : `History: ${item.kind || 'history'}`,
|
||||
@@ -1845,9 +1918,9 @@ function renderDecisionAssistant(summary) {
|
||||
}
|
||||
}
|
||||
if (digestHistoryPreview.length) {
|
||||
const filteredHistory = assistantHistoryKindFilter === 'all'
|
||||
const filteredHistory = activeHistoryFilter === 'all'
|
||||
? digestHistoryPreview
|
||||
: digestHistoryPreview.filter((item) => String(item.kind || '') === assistantHistoryKindFilter);
|
||||
: digestHistoryPreview.filter((item) => String(item.kind || '') === activeHistoryFilter);
|
||||
const historyGrid = createElement('div', 'cards assistant-history-grid');
|
||||
filteredHistory.forEach((item) => {
|
||||
historyGrid.appendChild(
|
||||
|
||||
@@ -119,6 +119,46 @@ class ApiServerRoutingTests(unittest.TestCase):
|
||||
self.assertEqual(payload["top_limit"], 3)
|
||||
self.assertEqual(payload["review_queue"][0]["queue_type"], "failed_retry")
|
||||
|
||||
def test_routes_trading_alert_digest_preferences(self) -> None:
|
||||
get_status, get_payload = route_request(
|
||||
"/trading/alerts/digest-preferences",
|
||||
{"instrument_id": ["1"], "profile_key": ["local_operator"]},
|
||||
dependencies={
|
||||
"trading_alert_operator_digest_preferences": lambda **kwargs: {
|
||||
"instrument_id": kwargs["instrument_id"],
|
||||
"profile_key": kwargs["profile_key"],
|
||||
"digest_mode": "high_value_only",
|
||||
}
|
||||
},
|
||||
)
|
||||
post_status, post_payload = route_request(
|
||||
"/trading/alerts/digest-preferences",
|
||||
{},
|
||||
method="POST",
|
||||
body={
|
||||
"instrument_id": 1,
|
||||
"profile_key": "local_operator",
|
||||
"digest_mode": "risk_first",
|
||||
"preferred_history_kind": "reminder",
|
||||
"importance_threshold": 80,
|
||||
},
|
||||
dependencies={
|
||||
"trading_alert_operator_digest_preferences_save": lambda **kwargs: {
|
||||
"instrument_id": kwargs["instrument_id"],
|
||||
"digest_mode": kwargs["digest_mode"],
|
||||
"preferred_history_kind": kwargs["preferred_history_kind"],
|
||||
"importance_threshold": kwargs["importance_threshold"],
|
||||
"status": "saved",
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
self.assertEqual(get_status, 200)
|
||||
self.assertEqual(get_payload["digest_mode"], "high_value_only")
|
||||
self.assertEqual(post_status, 200)
|
||||
self.assertEqual(post_payload["digest_mode"], "risk_first")
|
||||
self.assertEqual(post_payload["importance_threshold"], 80)
|
||||
|
||||
def test_routes_maintenance_summary(self) -> None:
|
||||
status, payload = route_request(
|
||||
"/trading/maintenance-summary",
|
||||
|
||||
@@ -98,6 +98,7 @@ class OperatorWorkspaceServiceTests(unittest.TestCase):
|
||||
research_council_service=_ResearchCouncilSummaryService(),
|
||||
exchange_status_provider=_ExchangeStatusProvider(),
|
||||
announcements_provider=_AnnouncementsProvider(),
|
||||
decision_assistant_repository=_DecisionAssistantRepo(),
|
||||
)
|
||||
|
||||
summary = service.build_summary(
|
||||
@@ -347,6 +348,9 @@ class OperatorWorkspaceServiceTests(unittest.TestCase):
|
||||
self.assertIn("default_mode", summary["digest_surface"]["digest_preference_profile"])
|
||||
self.assertIn("available_modes", summary["digest_surface"]["digest_preference_profile"])
|
||||
self.assertIn("profile_reason", summary["digest_surface"]["digest_preference_profile"])
|
||||
self.assertIn("digest_history_archive", summary["digest_surface"])
|
||||
self.assertIn("digest_history_archive_count", summary["digest_surface"])
|
||||
self.assertIn("digest_persistence_status", summary["digest_surface"])
|
||||
self.assertIn("next_high_value_change", summary["digest_surface"])
|
||||
self.assertIn("digest_review_back_actions", summary["digest_surface"])
|
||||
self.assertIn("digest_review_back_action_count", summary["digest_surface"])
|
||||
@@ -849,5 +853,49 @@ class _AnnouncementsProvider:
|
||||
}
|
||||
|
||||
|
||||
class _DecisionAssistantRepo:
|
||||
def fetch_preference(self, **kwargs):
|
||||
return {
|
||||
"profile_key": "local_operator",
|
||||
"instrument_id": kwargs["instrument_id"],
|
||||
"digest_mode": "high_value_only",
|
||||
"preferred_history_kind": "reminder",
|
||||
"importance_threshold": 75,
|
||||
"payload": {"source": "unit"},
|
||||
"updated_ts": "2026-05-12T02:05:00+00:00",
|
||||
}
|
||||
|
||||
def fetch_recent_digest_history(self, **kwargs):
|
||||
return [
|
||||
{
|
||||
"profile_key": "local_operator",
|
||||
"instrument_id": kwargs["instrument_id"],
|
||||
"digest_mode": "high_value_only",
|
||||
"preferred_history_kind": "reminder",
|
||||
"top_summary": "Review Candidate #18",
|
||||
"snapshot_signature": "unit-history-signature",
|
||||
"payload": {
|
||||
"focus_mode": "review_top_setup",
|
||||
"top_opportunity_title": "Candidate #18",
|
||||
"next_digest_action_label": "Review reminder",
|
||||
"high_value_reminder_count": 1,
|
||||
"reminder_count": 2,
|
||||
},
|
||||
"created_ts": "2026-05-12T02:06:00+00:00",
|
||||
}
|
||||
]
|
||||
|
||||
def fetch_latest_digest_history(self, **kwargs):
|
||||
return self.fetch_recent_digest_history(**kwargs)[0]
|
||||
|
||||
def insert_digest_history(self, **kwargs):
|
||||
return {
|
||||
"id": 9,
|
||||
"instrument_id": kwargs["instrument_id"],
|
||||
"snapshot_signature": kwargs["snapshot_signature"],
|
||||
"created_ts": "2026-05-12T02:07:00+00:00",
|
||||
}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
Reference in New Issue
Block a user