Files
ExperionCrawler/AGENTS.md
windpacer 7c26aa7361 feat: Phase II auto-write (WriteGuard, audit, auth) + WO-2~7 완료
Phase II:
- FfOperatorAction entity + ff_operator_action DDL/DbSet
- IFeedforwardWriteGuard + FeedforwardWriteGuard (SP bounds, grade C, transient, NaN)
- IFeedforwardAuditService + FeedforwardAuditService (raw ADO insert/query)
- FeedforwardSupervisor.AutoWriteAsync (per-stream OPC UA after Tick, rate-limited)
- FeedforwardConfigStore: advisory_only now read/writes DB, sp_node_id column
- FeedforwardController: auth (X-Kb-Token) on config/delete/write/audit;
  POST write/{id}/{key} manual SP write; GET audit; write results in MapColumn
- ff.js: token header, auto-write badge, per-stream write result, spNodeId, advisoryOnly
- ff.css: .ff-write-badge, .ff-write, .ff-write-err, .ff-wg-blocked
- Program.cs: register audit (Scoped) + write guard (Singleton)

WO-2~7 (build 0W/0E, test 22/22):
- PCT monitor, θ auto-tune, slow bias, front position indicator,
  total reflux recovery, config form expansion
2026-05-31 20:30:06 +09:00

11 KiB

ExperionCrawler — Agent Instructions

Build / Run / Test

Action Command Working Dir
Build dotnet build src/Web/ExperionCrawler.csproj repo root
Run (dev) dotnet run src/Web/
Publish dotnet publish -c Release -o /opt/ExperionCrawler src/Web/
Tests dotnet test tests/ExperionCrawler.Tests/ExperionCrawler.Tests.csproj repo root

Single project: src/Web/ExperionCrawler.csproj. Core and Infrastructure are included via <Compile Include> globs — there are no separate projects to build. Runtime target is linux-arm64.

Architecture

src/
├── Core/            — Interfaces (IExperionServices.cs), Domain entities, DTOs, Application Services
├── Infrastructure/  — OpcUa/, Database/, Certificates/, Csv/, Mcp/
└── Web/             — Program.cs, Controllers/, wwwroot/ (SPA)

All controllers are in src/Web/Controllers/ExperionControllers.cs (single file). All interfaces are in src/Core/Application/Interfaces/IExperionServices.cs (single file).

Database

PostgreSQL (NOT SQLite — README is stale). Connection strings in src/Web/appsettings.json. TimescaleDB extension may be enabled on history_table via DDL only; no app code changes needed.

⚠️ Critical — duration_seconds semantics

event_history_table.duration_seconds = 직전 상태의 지속 시간(초). 현재 이벤트의 지속 시간이 아니다.

  • DigitalEventDetectorService가 이전 상태를 처음 관측한 시점부터 값이 바뀔 때까지의 경과 시간(wall-clock)을 기록
  • 예: prev=R-RUN, curr=R-TRIP, duration_seconds=1423R-RUN 상태가 1423초간 유지되다가 R-TRIP으로 전환된 것. TRIP이 1423초간 지속된 게 아님
  • MCP 툴(query_events, active_alarms) JSON 출력 시 prev_state_duration_s 필드명으로 변환하여 반환하므로 LLM이 필드명만 보고 의미를 알 수 있음
  • LLM 프롬프트에 이벤트 데이터를 넘길 때는 (직전상태유지={duration}s) 형식으로 전달

sub_area (세부 Area: P6-1/P6-2 등)

하나의 area(P6)는 2개 Column(증류탑)으로 나뉜다. 태그 단위 sub_area를 별도 저장한다.

  • 저장: tag_metadataattribute='sub_area' (EAV). Single Source of Truth는 tag_metadata (OPC UA 아님).
  • 값 형식: 단일 "P6-1" 또는 공용 "P6-1,P6-2"(여러 sub_area 공용 설비, 예: 진공펌프 vp-2127a/b).
  • 매칭은 항상 토큰 비교: 'P6-1' = ANY(string_to_array(value, ','))value = 'P6-1' 직접 비교 금지(공용 누락).
  • MetadataLoaderServiceattribute IN ('desc','area')만 건드림 → sub_area는 절대 덮어쓰지 않음.
  • event_history_table에는 저장 안 함 → 이벤트 조회 시 tag_metadatasplit_part(tagname,'.',1)로 JOIN.
  • seed: IExperionDbService.SeedSubAreaAsync(dryRun) — area-scoped 번호 prefix + pid_equipment 공용(role ILIKE '%공용%') 검출. prefix 미적용 태그는 NULL(수동 분류). POST /api/tags/sub-area/seed.
  • MCP: find_tags(sub_area=...) 또는 active_alarms/query_events/...(area="P6-1") — '-' 포함 시 server.py가 자동으로 sub_area 토큰 매칭.

Timezone — KST

DB는 UTC 저장, LLM에는 KST(UTC+9) 변환해서 전달.

  • server.py_kst_str() 함수로 UTC ISO 문자열 → KST ISO 문자열 변환
  • 모든 시스템 프롬프트에 "모든 시각은 KST"라고 명시
  • MCP 서버 재기동 필요 시 uv run server.py --http

Critical Convention — JSON camelCase

PropertyNamingPolicy = null in Program.cs means C# PascalCase becomes JSON keys. Frontend expects camelCase. Every controller Ok(...) response MUST use explicit anonymous objects with camelCase keys:

// ✅ Correct
return Ok(new { id = x.Id, tagName = x.TagName });

// ❌ Broken — JS gets undefined
return Ok(new { x.Id, x.TagName });
return Ok(myDto);  // typed object

See CODING_CONVENTIONS.md for full details and checklist.

Background Services (HostedServices in Program.cs)

  • ExperionRealtimeService — OPC UA subscription, 500ms batch flush to DB
  • ExperionHistoryService — periodic snapshot (60s) from realtime_table → history_table
  • ExperionOpcServerService — exposes realtime data as OPC UA server (port 4841)
  • McpServerHostedService — Python MCP server bridge
  • ExperionFastService — high-frequency data capture sessions
  • ExperionFastCleanupService — expired session cleanup

All registered as Singleton + HostedService. RealtimeService and OpcServerService share autostart flag files (realtime_autostart.json, opcserver_autostart.json) in the working directory.

Frontend

Vanilla JS SPA. wwwroot/index.html + js/app.js + css/style.css. No build step. Tab-based navigation; tabs do NOT auto-fire API calls on entry (performance fix).

OPC UA SDK Gotchas

  • SDK v1.5.378.134 — Session.Create() is [Obsolete]; use DefaultSessionFactory.CreateAsync()
  • Subscription.Create() / Delete() / ApplyChanges() also deprecated → async variants preferred
  • Certificate validation must be attached AFTER OpcUaConfigProvider.GetConfigAsync() returns the config
  • TCP connect timeout: wrap SelectEndpointAsync with a 10s CancellationTokenSource (OS default is 127s)

Deploy

sudo bash deploy.sh — publishes to /opt/ExperionCrawler, creates systemd service experioncrawler, sets up PKI dirs. Service runs as www-data.


Frontend Architecture (refactored 2026-05-24)

Directory layout

wwwroot/
├── index.html              ← data-src shell (229 lines, -87%)
├── panes/                  ← 16 pane HTML partials (lazy-loaded on tab click)
├── js/
│   ├── core.js             ← esc/setGlobal/log/api/fmtTs/fmtVal/parseEnumPv
│   │                         activateTab/paneInit + dt* date picker
│   ├── app.js              ← placeholder (10 lines)
│   ├── cert.js conn.js …   ← 15 tab-specific files
│   └── docs.js             ← separately maintained
├── css/                    ← style.css monolithic (Phase 4 pending)
└── lib/                    ← unchanged

Tab lifecycle

  1. User clicks .nav-item[data-tab="X"]
  2. core.js:activateTab(X) fires:
    • If pane-X not loaded → fetch(/panes/X.html)innerHTML inject
    • Activate pane + nav highlight
    • paneInit[X]?.() — each tab file registers its own init

PaneInit registration

Tab Init function File
docs paneInit.docs = docsInit docs.js
opcsvr paneInit.opcsvr = srvLoad opcsvr.js
t2s paneInit.t2s = t2sInitMode t2s.js
fast paneInit.fast = function() { … } fast.js
pid paneInit.pid = async function() { … } pid.js
llmchat paneInit.llmchat = function() { … } llmchat.js
kbadmin paneInit.kbadmin = async function() { … } kbadmin.js
cert/conn/crawl/db/nm-dash/pb/hist/evt/write init 불필요

JS load order (index.html)

core.jsapp.js → cert/conn/crawl/db/nm-dash/pb/hist/opcsvr/t2s/fast/pid/evt/llmchat/kbadmin/write → docs.js

Phase 4 pending — CSS 분리

style.css(2,230줄)에서 탭별 스타일 분할 미완료. docs.css가 선례. 웹UI-개선플랜-byOPUS.md §11 참조.


Anchored Summary — Phase II (Auto-Write, WriteGuard, Audit, Auth)

Done

  • WO-2 (PCT monitor), WO-3 (θ auto-tune), WO-4 (slow bias), WO-5 (front position indicator), WO-6 (total reflux recovery), WO-7 (config form expansion): all built, tested (22/22), JS OK, sign-off: windpacer 2026-05-31.
  • Phase II auto-write (23 files):
    • FfOperatorAction entity (src/Core/Domain/Entities/FfOperatorAction.cs)
    • ff_operator_action DDL + DbSet + OnModelCreating in ExperionDbContext.cs
    • IFeedforwardWriteGuard + FeedforwardWriteGuard (SP bounds, grade C, transient, NaN checks)
    • IFeedforwardAuditService + FeedforwardAuditService (raw ADO.NET insert/query)
    • FeedforwardSupervisor.AutoWriteAsync — per-stream OPC UA write after Tick (rate-limited, guarded, logged)
    • FeedforwardConfigStoreadvisory_only no longer hardcoded; reads/writes DB; sp_node_id column added
    • FeedforwardController — auth (X-Kb-Token) on config/delete/write/audit; POST /api/ff/write/{id}/{key} manual SP write; GET /api/ff/audit audit query; write results merged in MapColumn
    • Program.csIFeedforwardAuditService (Scoped), IFeedforwardWriteGuard (Singleton) registered
    • ff.jsffToken() + X-Kb-Token header; auto-write badge; per-stream write result; spNodeId field in stream table; advisoryOnly checkbox in form
    • ff.css.ff-write-badge, .ff-write, .ff-write-err, .ff-wg-blocked
  • Build 0W/0E, test 22/22, JS OK. Sign-off: windpacer 2026-05-31.

Key Design Decisions

  • IFeedforwardWriteGuard is Singleton (stateless pure check functions) — no per-request instance needed.
  • IFeedforwardAuditService is Scoped (depends on ExperionDbContext which is Scoped). Supervisor resolves it from IServiceScopeFactory scope.
  • SP writes go through IExperionOpcWriteClient (Scoped) — each call creates + destroys an OPC UA session (acceptable for low-frequency writes).
  • sp_node_id is stored per-stream in ff_stream_config. If null, auto-write is skipped for that stream (no write).
  • Rate-limit: minimum ScanSec * 2 between writes to the same stream (avoids double-writes on rapid ticks).
  • Auth: X-Kb-Token header validated via IKbAuthService.ValidateAsync() — same mechanism as RAG KB admin. Token stored in sessionStorage by kbadmin.js.
  • AdvisoryResult.AutoWriteActive is set by the Supervisor after Tick (not by the Engine). Engine remains pure computaton.
  • WriteGuardBlockedSp / WriteGuardReason on AdvisoryResult are informative only — set when streams exist but all are blocked.

Relevant Files

File Purpose
src/Infrastructure/Control/FeedforwardWriteGuard.cs New — SP safety checks
src/Infrastructure/Control/FeedforwardAuditService.cs New — operator action log
src/Core/Domain/Entities/FfOperatorAction.cs New — audit log entity
src/Infrastructure/Control/FeedforwardSupervisor.cs Modified — AutoWriteAsync + GetLastWrite + IConfiguration
src/Infrastructure/Control/FeedforwardConfigStore.cs Modified — reads/writes advisory_only from DB, sp_node_id
src/Web/Controllers/FeedforwardController.cs Modified — auth, write, audit endpoints; write results in MapColumn
src/Web/Program.cs Modified — register audit + write guard
src/Web/wwwroot/js/ff.js Modified — token, write status, spNodeId, advisoryOnly form
src/Web/wwwroot/css/ff.css Modified — auto-write/blocked styles
src/Infrastructure/Database/ExperionDbContext.cs Modified — FfOperatorAction DbSet + DDL + OnModelCreating

Next Steps

  • Phase II complete. Consider Phase III (operator dashboard, write confirmation dialog, trend overlay) when ordered.