Files
ExperionCrawler/AGENTS.md
windpacer 9bcba0a317 feat: 웹UI Phase 4 완료 — CSS 분리, pane 중첩 버그 수정, app.js 제거
Phase 4 — CSS 분리:
- style.css(2,230→670줄)에서 탭별 스타일을 css/<tab>.css 8개로 분할
  (t2s:437, pid:236, pb:106, hist:100, evt:50, opcsvr:14, llmchat:501, kbadmin:109)
- 크로스탭 공유 스타일(nm-*, hist-status, dt-picker 등)은 style.css 잔류
- index.html head에 11개 CSS link 태그 (1 style.css + 8 tab + 2 lib)

app.js 제거:
- index.html에서 <script src=/js/app.js> 참조 제거
- app.js → 10줄 placeholder (이미 Phase 0-3에서 모든 로직 이전 완료)

Pane wrapper 버그 수정:
- 16개 pane 파일에서 <section class=pane id=pane-xxx> wrapper 제거
- activateTab이 innerHTML로 주입 시 중첩 section + display:none 발생
- 내용이 전혀 안 보이는 문제 해결

문서 갱신:
- AGENTS.md: Frontend Architecture 섹션 추가
- 웹UI-개선플랜-byOPUS.md: Phase 0-4 완료 상태로 갱신, 결과 검증 추가

MCP:
- server.py: timestamp 정밀도 개선 등
2026-05-24 18:47:25 +09:00

147 lines
7.2 KiB
Markdown

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