The 162 Architect/Claude docs were tagged 'audience: dev'. The 'dev' tier resolves to isInternal() = ROLE_MSP_ADMIN || ROLE_BCOS_ADMIN, so every MSP_ADMIN could read full internal architecture: source paths, DB tables, service ports, NGINX routes. Verified live on bcos.dev as msp_a@msp1.bcos.dev. Retag them to 'audience: bcos' (isBcos, BCOS_ADMIN only). Content-only change; the 'dev' tier mapping is deliberately left alone so the 15 Internal, 35 Overview and 66 Testing docs stay MSP-visible. Also add platform.guide-access documenting the audience tiers and the per-role visibility matrix, so the access model is queryable from the Guide instead of re-derived from source each time. Verified: MSP module count unchanged (116 across 17 apps); no module loses all its tabs. Guide is bcos.dev only, not deployed to production. Closes #16 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
9.0 KiB
module, title, tab, order, audience
| module | title | tab | order | audience |
|---|---|---|---|---|
| agent.activeteller | Active Teller (IAT) — teller-session, core-banking & Check21 monitoring for Nautilus Hyosung ITMs | Architect | 30 | bcos |
Internal · architecture. Written 2026-07-01 from
hiveops-module-activetellersource. Verify against code before relying on specifics.
Module shape
Package com.hiveops.activeteller, six classes, no controllers/DB/Kafka (this is an agent module, not a microservice — see [technical.agent]):
ActiveTellerModule— theAgentModuleSPI entry point (registered viaMETA-INF/services/com.hiveops.core.module.AgentModule). Owns lifecycle (initialize()→isEnabled()→start()→stop()), the shared single-threadScheduledExecutorService(daemon thread namedactiveteller), and constructs the three monitors + the emitter.TellerSessionMonitor(package-private) — parsesActiveTellerAgent_*.log.SymXchangeMonitor(package-private) — parsesSymXchange{date}.log.CheckProcessingMonitor(package-private) — parsesCheckProcessingResults{date}.log.RollingLogTailer(package-private) — shared tail-by-offset reader; each monitor owns one instance.IatEventEmitter(package-private) — thin wrapper overIncidentEventClientthat builds theCreateJournalEventRequestand centralizes send-error handling.
The three monitors are plain constructed objects, not their own AgentModules — ActiveTellerModule drives all three from a single poll loop.
Lifecycle & config gating
initialize(ModuleContext) is defensive and never throws for a mis-provisioned box — it just leaves ready=false, which isEnabled() returns. It bails (module stays disabled) when any of these fail, each with a distinct log line:
device.type != NH_IATactiveteller.log.dirunsetactiveteller.log.diris not an existing directory- neither
agent.endpointnorincident.endpointis configured
Endpoint prefix is chosen the standard way: prefix = agentEndpoint != null ? "agent" : "incident", then HttpClientSettings settings = HttpClientHelper.loadSettingsFromProperties(props, prefix). The IncidentEventClient is built once with (settings, atmName, country) from ModuleContext. isHealthy() = scheduler alive and not terminated.
Flow: poll → tail → detect → emit
ActiveTellerModule.start()
→ ScheduledExecutorService (daemon "activeteller")
├─ scheduleAtFixedRate(poll, 0, pollSec, SECONDS) // default 30s
└─ scheduleAtFixedRate(heartbeat, heartbeatSec, heartbeatSec, SECONDS) // default 900s
poll():
tellerMonitor.poll(); symXchangeMonitor.poll(); checkMonitor.poll();
heartbeat():
if any monitor isAlive() → emit IAT_HEARTBEAT with concatenated fragments
poll() wraps all three in a try/catch so one monitor's parse error can't kill the scheduler. heartbeat() skips entirely if no monitor has seen a line today (isAlive() = tailer.currentFile() != null), so idle machines stay quiet.
RollingLogTailer — the shared tail engine
Every monitor reads through one RollingLogTailer constructed with (baseLogDir, tag, FileResolver). On each poll(Consumer<String>):
- Computes
dailyFolder = baseLogDir.resolve(yyyyMMdd); if it isn't a directory, no-ops. - Calls the
FileResolverto pick the concrete file inside that folder:fixedName(prefix, suffix)→{folder}/{prefix}{yyyyMMdd}{suffix}(SymXchange, CheckProcessingResults).newestMatching(prefix, suffix)→ the most-recently-modified file matching prefix+suffix (session-stampedActiveTellerAgent_*.log).
- Tail-by-offset: opens with
RandomAccessFile,seek(fileOffset), reads new lines to EOF, advancesfileOffset = raf.getFilePointer(). - Rotation/new file: when the resolved path changes (new day or new session file), logs
watching <path>, setscurrentFileand resetsfileOffset = 0(reads the new file from the top). - Truncation guard: if
raf.length() < fileOffset, resets offset to 0. - Encoding fix:
RandomAccessFile.readLine()decodes as ISO-8859-1; each line is re-decoded to UTF-8 (new String(raw.getBytes(ISO_8859_1), UTF_8)) before the consumer sees it.
RollingLogTailer holds no detection logic — the monitors own all parsing.
TellerSessionMonitor — session state machine
Line format: yyyy-MM-dd HH:mm:ss <message>. Maintains single-customer session state (an IAT serves one customer at a time): requestAt, sessionAt, videoUp, currentTeller, plus a logClock (timestamp of the last processed line).
POST TellerSessionRequest message received→requestAt = ts(idleDELETEheartbeats are ignored).- A payload containing
"TellerSessionRequestId"→ session assigned (extracts"TellerName"; dedups repeated echoes of the same payload); clearsrequestAt, resets video flags. OnAdd for RemoteControlSession(while a session is active) →videoUp = true.Ending teller session/OnDelete for TellerSessionRequest→ session end; if video never came up, emitIAT_VIDEO_SESSION_FAIL.- REST outcome
(https?://…) server response (\w+)→ success statuses{OK, Created, Accepted, NoContent, NotModified, ResetContent}reset the failure streak (and, ifIAT_SERVER_UNREACHABLEhad been reported, emitIAT_SERVER_RECOVERED); anything else emitsIAT_SERVER_ERRORand, atfailureThresholdconsecutive failures,IAT_SERVER_UNREACHABLE(once, until a success resets it). - A payload containing
"ModeType"with"ModeName":"(\w+)"→IAT_MODE_CHANGE(suppressed on first observation and on unchanged mode). checkTimers()runs after each batch, comparingrequestAt/sessionAtagainstlogClock(not wall-clock): overdue request →IAT_TELLER_UNANSWERED; session with no video pastvideoTimeout→IAT_VIDEO_SESSION_FAIL. Driving timers off the log clock means replaying a backlog can't raise spurious timeouts.
SymXchangeMonitor — header-block parsing
The SymXchange log alternates header lines ([yyyy-MM-dd HH:mm:ss-nnn][OpName]) and XML payloads (PANs/SSNs masked to **** at source). Per line:
- A header sets
currentTs/currentOp, resetsfaultReportedForBlock, bumpsmessageCount. - A
CONN_ERROR-matching line (unable to connect / no connection / refused / timed out / DNS) increments a streak; atfailureThresholdconsecutive →IAT_CORE_UNREACHABLE(once until reset). - Each SymXchange message header clears the connection-error streak — any response (incl. a fault) proves the core is reachable — and, if
IAT_CORE_UNREACHABLEhad been reported, emitsIAT_CORE_RECOVERED. - A
FAULT-matching line (faultstring,<faultcode,:Fault>,<Fault>,<ErrorMessage>, non-zero<ErrorCode>) →IAT_CORE_TXN_FAIL(at most once per header block). The fault/conn regexes are conservative heuristics noted for tuning against a real failure sample.
CheckProcessingMonitor — pipe-delimited records
Each Process Date:-prefixed line is split on | into Key: Value fields. A check is flagged (→ IAT_CHECK_IMAGE_QUALITY, informational) when any of: Status doesn't start with Processed; Invalidity Score > invalidityThreshold; or |Amount CAR − Amount LAR| >= amountMismatchCents. Amounts are in cents and rendered as dollars in the detail text. Clean checks emit nothing.
How events reach the platform
All three monitors emit through the single shared IatEventEmitter, which wraps the same IncidentEventClient class used by journal-events and other event-reporting modules — see [technical.agent] for the full outbound API surface.
monitor → emitter.emit(eventType, details, eventSource, eventTime)
→ CreateJournalEventRequest.builder()
.agentAtmId(atmId) // ATM name string, resolved to numeric atmId server-side
.eventType(eventType) // free-form String, e.g. "IAT_CORE_UNREACHABLE"
.eventDetails(details)
.eventSource(eventSource) // IAT_ACTIVETELLER | IAT_SYMXCHANGE | IAT_CHECK21
.eventTime(ts.format(ISO_LOCAL_DATE_TIME) | null)
.build()
→ IncidentEventClient.sendEvent(req)
→ HTTP POST {agent.endpoint or incident.endpoint}/api/journal-events
eventType is a plain String on CreateJournalEventRequest (the hiveops-agent-journal DTO) — not validated client-side. Nothing stops an unregistered IAT_* type from being sent; recognition/labeling happens server-side against hiveops-incident's DB-backed event_type table. On IOException, emit() logs a warning and drops the event — no retry, no queue.
Platform abstraction
None used directly. The module reads plain files via java.nio.file / RandomAccessFile — no PlatformService/PathResolver dependency. The Hyosung/Windows constraint is enforced by deployment convention (installed only on device.type=NH_IAT machines) and the configured activeteller.log.dir, not by a runtime OS check in this module's code.
Cross-links
- [technical.agent] — module system, lifecycle, shared HTTP client settings, full outbound API surface
- [platform.module-map]