# 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 `<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=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 참조.
