# 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` | repo root | Single project: `src/Web/ExperionCrawler.csproj`. Core and Infrastructure are included via `` 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=1423` → **R-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_metadata`의 `attribute='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'` 직접 비교 금지(공용 누락). - `MetadataLoaderService`는 `attribute IN ('desc','area')`만 건드림 → sub_area는 절대 덮어쓰지 않음. - `event_history_table`에는 저장 안 함 → 이벤트 조회 시 `tag_metadata`를 `split_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: ```csharp // ✅ 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.js` → `app.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 참조.