Structure and Boundaries
This describes the structure you need to know when fixing the field tablet app or integrating it with other systems. The source is the plantpulse-platform/plantpulse-frontline-web repository.
Overview
태블릿 브라우저 (PWA) frontline 컨테이너 플랫폼
┌──────────────────────┐ HTTPS ┌──────────────────────────────┐ V5 ┌──────────────┐
│ React 웹 앱 │ ──────▶ │ BFF (/api/*) │ ────▶ │ server-web │
│ 서비스워커 · 입력 큐 │ 9701 │ · 기기 토큰 · 작업자 세션 │ 토큰 │ /api/v5/... │
└──────────────────────┘ │ · 범위 검사 · 감사 로그 │ 하나 └──────────────┘
└──────────────┬───────────────┘
│ JDBC
플랫폼 DB pp · 스키마 pf
| Component | Description | Repository Location |
|---|---|---|
| Web | React 19 + TypeScript + Vite PWA. Service workers, offline input queue (IndexedDB), and last values per screen (localStorage). Graphs use ECharts 6 — as a separate chunk loaded only when a screen with a graph is opened. | web/ |
| BFF | Spring Boot 4.1 WAR, running on the common Platform Tomcat 10.1 (runtime-base). The dispatcher is applied only to /api/*. Scope checks (scope/), maintenance (maintenance/), defects (defect/), and reports (floor/ReportController) are located here. | src/plantpulse/frontline/ |
| DB | The pf schema (previously frontline before 2026-10-29) of the Platform database pp, using the existing Platform DB account. Tables start with fl_. The schema is migrated via Flyway (V1~V14). | src/db/migration/ |
| Image | docker.kopens.io/pp/plantpulse-frontline-web. Port 9701 (TLS) · 9700 (Health check only) | Dockerfile |
Schema History
| Version | Description |
|---|---|
| V1~V4 | Operator · PIN · Device · Settings · Reason List · Shift Notes · Audit Logs, Screen Language · Theme |
| V5 | Removed CHECK IN ('ko','en') from the language field and expanded fl_operator.lang · fl_operator_revision.lang · fl_device.default_lang to VARCHAR(35) (BCP 47). Allowed languages are determined by a single list on the server, not in the schema. |
| V6 | Deleted the kiosk.micro_stop_seconds row in fl_setting. The short stop criterion is a Platform setting (old values are not migrated). |
| V7 | Assigned scope. Below. |
| V8 | Index for reading audit logs by person in page units. |
| V9 | Connection field between operator login and Platform employee (pp.mm_employee). |
| V10 | Migrated operator accounts to the Platform. Created login cache fl_account_cache and synchronization status fl_account_sync, and changed the session to point to the Platform employee ID (existing sessions are deleted — all tablets re-login after migration). Old fl_operator · fl_operator_scope are deleted in V11 after confirming migration to the Platform. |
| V11 | Delete old operator tables (fl_operator · fl_operator_scope and their history tables). Deletion occurs only in installations (fl_account_sync.imported_at) marked as migration complete; those not yet migrated are retained. |
| V12 | Equipment «Key Values» fl_key_value — up to 6 tags to be shown first in the equipment overview (tag ID and order only; values are read from V5). |
| V13 | Customer Logo fl_brand_logo — one PNG each for light and dark themes. |
| V14 | Expanded the allowance levels of the device default scope fl_device.default_scope_type to four levels: SITE · AREA · LINE · ASSET (previously LINE · ASSET). Existing rows remain. |
Schema Renaming. The app performs ALTER SCHEMA frontline RENAME TO pf as a single statement only once when the frontline schema exists and pf does not, prior to Flyway. Tables, data, Flyway history, and settings (ai.url · ai.api_key) are moved without copying. Upon restart, it does nothing as pf already exists (idempotent). For new installations, Flyway creates pf. If both exist, it leaves a warning without guessing and uses pf. If the DB does not respond, the app does not start — otherwise, Flyway might create an empty pf, making data appear lost.
Operator Account Migration. Before the first synchronization after startup, fl_operator · fl_operator_scope are sent once to V5 POST /app-accounts/frontline/import. PIN hashes are moved as-is (not re-hashed). For logins not linked to an employee, the Platform creates and attaches an employee EMP_FL_<로그인 번호>. If a login number already exists on the Platform, the Platform version is kept (EXISTS). Migration is marked complete only when all rows return via CREATED · EXISTS; otherwise, they are resent in the next cycle (idempotent).
Boundaries with Platform
OEE, availability, performance, quality, downtime, loss, MTBF, and MTTR are displayed as values provided by Platform V5 only. Values not provided by the Platform are left blank, shown as «No Data», or hidden; they are never filled with 0 or estimated values. The only value calculated on the screen is the display progress rate (good products ÷ planned), and graphs draw V5 values as-is, changing only the axis units. Thus, the tablet, Platform web, reports, and AI all see the same numbers.
- Platform data is read and written only via V5. Even if they share the same
ppdatabase, the app does not read or write directly to Platform tables, nor does it keep copies of Platform data in thepfschema. Maintenance calls and defect records also have no copies in the app — the authoritative sources are V5 maintenance orders and defect records. - One exception — Login Cache
fl_account_cache. This is maintained so the field can log in even while the Platform is restarting. See Operator Account below. PIN hashes, employee names, and employment statuses are not stored in other tables (prevented by tests). - No MES functions. Operator accounts consist only of login means (login number, role, PIN) and assigned scope (what they can see). Assigned scope is not an MES manpower assignment — there are no certifications, shift rosters, individual performance, or headcount aggregations. Orders are selected and then started, paused, resumed, ended, or stopped (with reason codes), and completion rates are reported based on time — issuance and editing are handled by the Platform.
- Items stored in the
pfschema include devices (including default language and screen theme), reason lists per equipment, kiosk settings, shift notes, audit logs, sessions, and the login cache (fl_account_cache·fl_account_sync). The authoritative source for operator accounts (login number, PIN hash, role, assigned scope, screen language, theme) is Platformpp.mm_app_account·pp.mm_app_account_scope. - When calling V5, the viewer's language is sent as
Accept-Language. Stop reasons, failure modes, and defect cause names are displayed exactly as sent by V5.
Determination by Platform
The app does not perform determinations for the screen; it uses values provided by V5. Determination codes previously in the app (plan, source, length comparison, shift window selection, and 8-hour replacement window) have been removed.
| Determination | V5 Source |
|---|---|
| Current Shift Window | GET /calendar/shift-schedule/current. The «Last 9 Hours» used when no roster exists is also provided by V5 (if a person selects 12 hours, it is widened accordingly). If V5 cannot provide a window, the card states that refusal; the app does not invent a window. |
| «Current Plan» Order | planned_order_id in the same response |
| Stops Missing Reasons | needs_reason for each stop |
| Short Stops | short for each stop. Criterion is GET /config/PP_STOP_MICRO_STOP_SECONDS (default 60s, 0~1800, asks for all if 0). |
V5 Calls by App
| Read | Purpose |
|---|---|
GET /site · /asset/lines/by-site/… · /asset/equipments/by-line/… · /path/asset/… | Scope selection (Site → Line → Equipment) |
GET /asset/… · GET /site/… · GET /asset/equipments/by-line/… | Interpreting assigned scope nodes (parent chain). Line plan comparisons are requested via the equipment list. |
GET /asset/…/status | Status and duration |
GET /kpi/oee/status/current · /kpi/period | Shift OEE, dashboard period metrics |
GET /kpi/daily | Daily trends for dashboards and reports |
GET /ram/breakdown?by=CAUSE · /failure-dictionary/alarm-cause | Failure cause Pareto and cause names |
GET /asset/…/stops · /asset/…/stop-loss | Timeline, stops missing reasons, short stops, loss by stop reason |
GET /asset/…/timeline/alarms | Alarms (min_priority, limit=500). BFF filters for those in progress (turned on within the last 7 days) and those ended today. |
GET /stop-reasons | Cause dictionary (reason buttons, stop reason list in settings) |
GET /calendar/shift-schedule/current | Current shift window (asset_id). Response fields are from_ts · to_ts · shift_code · shift_date · from_plan · planned_order_id |
GET /config/PP_STOP_MICRO_STOP_SECONDS | Short stop criterion (1-minute cache) |
GET /order/query · /order/current-asset · /order/… · /order/…/quantity | Order list, active order, details, quantity |
GET /order/…/revisions · /order/…/progress | Change history, completion rate report history. V5 provides only employee numbers, so the BFF attaches names found in the login cache as names (otherwise uses the employee number). |
GET /maintenance-orders · /maintenance-orders/options · /maintenance-orders/… | Maintenance board (open=true&order=asc&limit=100, scope/assignee filtering), symptom/code list, confirmation before cancellation |
GET /asset/…/defects/options · /asset/…/defects · /order/…/defects · /defects/… | Defect cause list, defect records for equipment/orders |
GET /asset/qr/{token} | QR label token → equipment. Called by BFF only; tokens are not stored in logs. |
GET /order/deviation-summary · /defects/summary · /maintenance-orders/summary | Report plan comparisons, defect causes, maintenance summaries (slow read) |
GET /order/query uses line_id · site_id · status=END,ABORTED · end_since · end_before · include=oee · sort · limit=500 · order_id filtering for the work order screen. The BFF maintains a 5-second cache for maintenance board reads and a 60-second shared cache for maintenance/defect options; caches are cleared upon writing.
| Write | Purpose | Header |
|---|---|---|
PUT /asset/{asset}/stops/{started_at}/reason | Record stop reason | Idempotency-Key |
PUT /order/{order}/quantity | Correct quantity (Good/Defect total + reason) | Idempotency-Key |
POST /order/{order}/start · pause · resume · end | Order transition. Timestamp occurred_at (epoch ms) in body | If-Match (Order version) |
POST /order/{order}/abort | Stop order. abort_code · abort_notes · occurred_at | If-Match · Idempotency-Key |
POST /order/{order}/progress | Report completion rate. percent_complete (0~100 integer) · note · reported_at | If-Match · Idempotency-Key |
POST /asset/{asset}/maintenance-orders | Call maintenance (maintenance_kind_code: CORRECTIVE, failure_mode_code · description · caller) | Idempotency-Key · X-PP-Channel: FRONTLINE |
POST /maintenance-orders/{id}/transition | Acknowledge, Start Repair, Complete (status_code + action · part · failure code), Cancel (status_code: CANCELLED + cancel_reason_code). Version is in the body. | Idempotency-Key · X-PP-Channel |
POST /asset/{asset}/defects | Record defect | Idempotency-Key · X-PP-Channel |
POST /defects/{id}/cancel | Cancel defect record | If-Match · X-PP-Channel |
- Include
X-PP-Performed-By: 로그인한 사번in all writes. V5 records this asperformed_byand keepschanged_byas the app account. The employee number of the person who pressed the button is included even when sent later by the offline queue. - Attach
Idempotency-Keyto writes. This prevents V5 from processing the same write twice if the offline queue resends. - Order transitions match versions using
If-Match. If another person changed it in the meantime, 409 is returned and the screen shows «Another person changed this first». - Order transitions, stops, and defect records include the timestamp of the press. Even if sent later from the queue, the performance time is the press time.
- «End previous order and start» consists of the BFF calling
endandstartsequentially. Separate idempotency keys are used for each half, and both halves share the same press timestamp. If the second part fails, the first part is not rolled back and the status is reported as-is.
For V5 itself, refer to the API documentation.
Entry via QR Label
The Platform's Public Page «Open in frontline» arrives via <PP_UI_FRONTLINE_URL>/#/qr/<token>. The URL does not contain the equipment ID.
- The app immediately removes the token from the URL and stores it in the tab's sessionStorage (
fl.qr_pending). It is not sent to the server before login. - After login, the token is retrieved and sent in the body of BFF
POST /api/v1/qr/resolve— placing it in the path would record it in every access log. - The BFF first checks the token format (43 characters, base64url), finds the equipment via V5
GET /asset/qr/{token}, and verifies if that account/tablet can select the equipment using the same rules as scope chips. - If passed, the app moves the scope to that equipment and opens «Now».
| Response | Code | Screen |
|---|---|---|
| 400 | qr_token | Not a label token |
| 404 | qr_unknown | Token reissued or equipment no longer exists |
| 403 | scope_denied | Out of assigned scope |
Operator Account V5
Operator accounts are located in Platform V5 /api/v5/app-accounts/frontline. Only ADMIN tokens or the Platform master key can call this (since change feeds include PIN hashes).
| Call | Purpose |
|---|---|
GET /app-accounts/frontline | Account list (including employee name and employment status). Read-only «Operator» tab in app. |
GET /app-accounts/frontline/{employee_id} | Single record (ETag = version) |
PUT /app-accounts/frontline/{employee_id} | Create/Edit account (login number, role, allowance, temporary PIN) |
DELETE /app-accounts/frontline/{employee_id} | Delete login (employee remains). The last administrator cannot be deleted. |
PUT /app-accounts/frontline/{employee_id}/scope | Assigned scope — {"all": true} or {"nodes": [...]} |
POST /app-accounts/frontline/pin/verify | Keypad PIN verification. Always returns 200 and result (OK · INVALID · LOCKED · DISABLED · PIN_RESET_REQUIRED · EMPLOYEE_INACTIVE · EMPLOYEE_ON_LEAVE). |
POST /app-accounts/frontline/{employee_id}/pin | Change own PIN (current PIN, new PIN) |
POST /app-accounts/frontline/{employee_id}/pin/reset | Admin PIN reset (temporary PIN, also unlocks) |
POST /app-accounts/frontline/{employee_id}/unlock | Unlock |
GET /app-accounts/frontline/changes | Change feed (cursor). Login cache reads this. |
POST /app-accounts/frontline/import | Migrate old app logins (idempotent) |
- Login. Upon reaching the Platform, the BFF calls only
pin/verify. Failure counts, locks (5 min after 5 fails), and rejections for퇴직/휴직 are handled by the Platform. - Login Cache.
AccountSyncreads the change feed every 60 seconds from the cursor to synchronizefl_account_cache(snapshot includes PIN hash, employment status, and scope). The cache is read only when the Platform is unreachable. If the last successful synchronization is older thanaccounts.offline_days(Admin «Kiosk» screen, default 7 days, 1~30 days), offline login is rejected (fail-closed,503 offline_login_expired). - Offline Failure. When verifying with the cache, incorrect PINs are counted by the app (5 min for 5 failures per person, 15 min for 20 failures per device) and are not sent to the Platform.
- Offline Restrictions. PIN changes, screen language, and theme changes are only possible when connected to the Platform. Account editing, PIN reset, and unlocking are not available in the app (available in console «Employee»).
- User-modifiable items are PIN (BFF
PUT /v1/session/pin), screen language, theme (PUT /v1/me/preferences), and contact/photo in «My Profile» (PUT /v1/me/profile·PUT /v1/me/photo, Platform employee info).
High Availability (HA)
PG meta synchronization for Platform HA also moves frontline data to the standby cluster.
| Synchronized | Not Synchronized (per node) |
|---|---|
Operator accounts and assigned scope pp.mm_app_account · pp.mm_app_account_scope (including PIN hash) | Sessions, device registration codes, audit logs, shift notes, idempotency records |
Kiosk settings pf.fl_setting, stop reason lists per equipment pf.fl_asset_reason_list, registered tablets pf.fl_device | Login cache fl_account_cache · fl_account_sync — each node fills its own via its Platform's change feed |
AI key (ai.api_key in fl_setting) only when metasync.include.tokens=true |
Platform Restart
Platform restarts typically take 1~3 minutes, up to 10 minutes. The app does not stop during this time.
- If the BFF cannot reach V5 or its own DB, it notifies «Currently Unavailable» via
503andX-FL-Offlineheaders. This is distinguished from rejections (4xx). - Upon receiving this response, the web indicates a disconnection, and the screen shows the last values with the time they were received, e.g., «As of 3 minutes ago». No invented values are shown — only the last value provided by the BFF.
- Stop reasons, order transitions (including stops), completion rate reports, maintenance calls, and defect records are placed in the queue. Inputs affecting the same order or stop maintain queue order; if a preceding item is rejected, subsequent items are not sent until human judgment is made.
- Recovery involves polling
/api/healthat intervals of 2, 4, 8, 15 seconds (then repeating 15s). If both the DB and Platform areUP, it restores the connection, sends the queue, and re-reads the screen. - Quantity corrections, maintenance stage moves, maintenance call cancellations, and defect record cancellations are not queued — these are writes based on the value and version of that moment and are only accepted when connected. Cancellation of a maintenance call already in the queue is handled by removing it from the queue.
- Timeouts for V5 reads are 2s for connection and 5s for response. Heavy report summaries (plan comparison, defects, maintenance) may wait up to 20s; exceeding this does not open the circuit — only that card fails via
504 platform_slowand does not transition to a disconnection state.
PlantPulse AI Chat
The «Ask AI» feature on the tablet is proxied by the BFF to the PlantPulse AI field chat API. This is a separate connection from Platform V5, so Platform connectivity is not affected if the AI is slow or offline.
| BFF | AI Side | Purpose |
|---|---|---|
GET /api/v1/chat/capabilities | GET /api/v1/chat/capabilities | Whether to show the button (60s memory). Those without scope cannot ask AI and see «None». |
POST /api/v1/chat/stream | POST /api/v1/chat/stream | Receives response via SSE and streams it as-is. |
POST /api/v1/chat/cancel | POST /api/v1/chat/cancel | Stop. Conversation for the same person only. |
- Address and keys are stored in app DB
fl_settingunderai.url·ai.api_key(Settings → AI). Keys are sent only viaX-API-Keyand are not included in logs, responses, or exception messages. - Requests include the employee number of the «Person asking on behalf» (same value as
X-PP-Performed-By) and their scope (same rule as scope check). If no scope exists, the AI is not called and a 403 is returned. - Screen language is sent as
Accept-Language.
Authentication — Three Layers
| Layer | Description | Location |
|---|---|---|
| Platform Key | Platform Master API Key. Held only by BFF. | Container environment variable PP_MASTER_API_KEY |
| Device Token | Random number issued during tablet registration. Only the hash is stored in the DB. No expiration; ends when deactivated by admin. Sent via Authorization: Bearer. | Browser localStorage (fl.device_token). Not present in other browsers, incognito windows, or browsers with cleared site data, thus unregistered. |
| Operator Session | Opened with login number + PIN. PIN is verified by Platform (or login cache if unreachable). Session points to Platform employee ID, is valid until auto-logout time, and is tied to the device token. Sent via X-FL-Session. | Tab sessionStorage — persists across refreshes, ends when tab is closed (uses memory if storage is unavailable). |
- Platform V5 does not accept USER role tokens (only ADMIN · API). Thus, individual Platform tokens cannot be given; the master key is used. The actor is recorded in both V5 records
performed_by(aboveX-PP-Performed-By) and app audit logs (fl_audit— employee number, device, V5 path, response code, Idempotency-Key). - In Platform audit logs, this app's calls are distinguished by actor
master-api-keyand User-AgentPlantPulse-frontline-web/<version>in proxy logs. - All BFF handlers specify access levels — denial is the default if not specified.
- Tokens and PINs are not included in logs, responses, or exception messages.
Role Check
Permitted roles are specified via @Access for each handler. The server blocks access independently of hiding buttons on the screen.
| Action | Permitted Roles |
|---|---|
| Maintenance Acknowledge, Start Repair, Complete | Maintenance, Administrator |
| Stop Order | Foreman, Administrator |
| Call Maintenance, Cancel Maintenance Call, Record Defect, Cancel Defect Record | All four roles. Cancellations are re-verified by the handler — must be the caller or Foreman/Administrator. |
Settings (/v1/admin/) | Administrator |
Scope Check
Assigned scope is checked after the role. All field requests pass through a single point scope/ScopeInterceptor (after the authentication interceptor).
- Targets are equipment, lines, orders (verifying equipment via V5), maintenance orders, and defect records specified in the path or query. Reads must be within scope, and writes must be
RW. - Out-of-scope requests return
403 scope_denied; attempts to write to read-only scopes return403 scope_read_only. @OpenToReadOnly(Call Maintenance, Cancel Maintenance Call) are checked at the read level — people who can only «view» equipment must still be able to call maintenance. Cancellation of others' calls is blocked by the handler.- Login Scope = Account Scope ∩ Device Default Scope (
ScopeGuard.within). If the account node is the same as or below the device node, it is kept. If above, the device node is given as the account node's access. Non-overlapping nodes are discarded. If the account isscope_all, it is the device nodeRW. The device does not expand scope or upgradeROtoRW. Devices without an administrator role and default scope do not narrow it. The device default scope can be at any stage: site, area, line, or equipment (DeviceScope). - Nodes are inherited downwards. If a piece of equipment receives both
RWandRO,RWtakes precedence. Nodes resolve the parent chain via V5 and are cached for 5 minutes. Nodes deleted in V5 have zero access. - Dashboards and reports require the node itself to be within scope. Receiving one piece of equipment does not open that line's dashboard.
- Lists are narrowed by the handler — scope tree, line cards/alarm counts, work orders (access bundled per row), and maintenance boards.
- Fail-closed. If neither
scope_allnor nodes exist, nothing can be read. Scope comes from the Platform account (pp.mm_app_account.scope_all·pp.mm_app_account_scope), and there is no API in the app to modify scope. V5 treats empty node lists as «Nothing», and nodes are limited to 200. - Administrator roles bypass scope checks. Management APIs (
/v1/admin/) skip scope checks and only verify the administrator role. - Non-logged-in tablets («View Now without PIN») read the device's default scope only via
RO.
The login response includes four scope fields.
| Field | Meaning |
|---|---|
scope | {"all": true} or node list (id · kind · access) |
scope_none | Nothing viewable on this tablet — web shows only guidance and logout, no menus |
scope_none_reason | Reason when scope_none. ACCOUNT means account has no scope («Scope not assigned»), TABLET means account scope does not overlap with tablet scope («No viewable scope on this tablet») |
home_scope | The first equipment or line to open if the starting scope (last selected, or device default) is outside the user's scope |
Environment Variables
| Variable | Description |
|---|---|
PP_TZ | Display timezone (required). Storage is UTC epoch; screen uses this timezone. Date rollover, «Today», and windows spanning midnight also use this timezone. |
PP_LANG | Installation language (required). Must be a tag from the supported language list (currently ko · en), accepting case and regional variants (en-US). If not in the list, the app does not start. |
PP_SERVER_URL | Platform V5 address |
PP_MASTER_API_KEY | Platform Master API Key. Same value as server-web, batch-web, and warehouse-web — all four must be restarted together if changed. |
PP_POSTGRES_HOST · PP_POSTGRES_PORT | Data Lake PostgreSQL |
PP_DB_NAME · PP_PG_USER · PP_PG_PASSWORD | Platform database (pp) and account. Same name and value as server-web, batch-web, and warehouse-web. There are no frontline-specific accounts/passwords. |
The container will not start if PP_MASTER_API_KEY · PP_PG_PASSWORD · PP_POSTGRES_HOST are missing. JVM heap is fixed at 512 MB (direct 128 MB). No new variables were added for assigned scope, maintenance, defects, or reports.
To add a new screen language, add web/src/i18n/<태그>.json to the web bundle and a line to the server's supported language list. The schema (V5) remains unchanged.
Fields to be Opened in Future Steps
The following fields are not yet in the app. They have no placeholders on the screen and will be opened via BFF calls once general V5 APIs are available on the Platform.
| Field | Current Status | Required from Platform |
|---|---|---|
| Weekly Trend · Previous Period Comparison | None (Daily trend opened via /kpi/daily) | Weekly period series |
| Energy (EMS) | None — no dashboard tab or report section | EMS API |
| Line-level Loss by Stop Reason | None — drawn only for equipment | Line scope stop loss aggregation |
| Hourly Performance / Target | None | Hourly target · performance API |
| Shift Loss Waterfall | None — shift OEE window only has OEE, availability, performance, quality, and top stop reasons | Loss decomposition |
| Manual Stop Registration · Quantity by Time Slot | None | Stop write path, quantity by time slot write |
Among the fields previously in this table, the following have been opened.
| Field | Implementation Method |
|---|---|
| Actor (Employee number in Platform record) | X-PP-Performed-By in all writes → V5 performed_by |
| Call Maintenance → Acknowledge → Start Repair → Complete | V5 /maintenance-orders |
| Defect Quantity + Reason | V5 /defects |
| Daily Trend | V5 /kpi/daily |
| Shift · Daily · Weekly Report | BFF GET /v1/report aggregates V5 KPI, order, and summary APIs |