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 정밀도 개선 등
7.2 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 |
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:
// ✅ 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 DBExperionHistoryService— periodic snapshot (60s) from realtime_table → history_tableExperionOpcServerService— exposes realtime data as OPC UA server (port 4841)McpServerHostedService— Python MCP server bridgeExperionFastService— high-frequency data capture sessionsExperionFastCleanupService— 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]; useDefaultSessionFactory.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
SelectEndpointAsyncwith a 10sCancellationTokenSource(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
- User clicks
.nav-item[data-tab="X"] core.js:activateTab(X)fires:- If
pane-Xnot loaded →fetch(/panes/X.html)→innerHTMLinject - Activate pane + nav highlight
paneInit[X]?.()— each tab file registers its own init
- If
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 참조.