Files
hiveops-guide/backend/src/main/resources/content/agent/activeteller/architect.md
T
johannes f21c783d8c fix(guide): gate Architect + Claude tabs to BCOS_ADMIN only (#16)
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>
2026-07-14 17:55:07 -04:00

9.0 KiB
Raw Blame History

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-activeteller source. 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 — the AgentModule SPI entry point (registered via META-INF/services/com.hiveops.core.module.AgentModule). Owns lifecycle (initialize()isEnabled()start()stop()), the shared single-thread ScheduledExecutorService (daemon thread named activeteller), and constructs the three monitors + the emitter.
  • TellerSessionMonitor (package-private) — parses ActiveTellerAgent_*.log.
  • SymXchangeMonitor (package-private) — parses SymXchange{date}.log.
  • CheckProcessingMonitor (package-private) — parses CheckProcessingResults{date}.log.
  • RollingLogTailer (package-private) — shared tail-by-offset reader; each monitor owns one instance.
  • IatEventEmitter (package-private) — thin wrapper over IncidentEventClient that builds the CreateJournalEventRequest and 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:

  1. device.type != NH_IAT
  2. activeteller.log.dir unset
  3. activeteller.log.dir is not an existing directory
  4. neither agent.endpoint nor incident.endpoint is 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 FileResolver to 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-stamped ActiveTellerAgent_*.log).
  • Tail-by-offset: opens with RandomAccessFile, seek(fileOffset), reads new lines to EOF, advances fileOffset = raf.getFilePointer().
  • Rotation/new file: when the resolved path changes (new day or new session file), logs watching <path>, sets currentFile and resets fileOffset = 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 receivedrequestAt = ts (idle DELETE heartbeats are ignored).
  • A payload containing "TellerSessionRequestId" → session assigned (extracts "TellerName"; dedups repeated echoes of the same payload); clears requestAt, 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, emit IAT_VIDEO_SESSION_FAIL.
  • REST outcome (https?://…) server response (\w+) → success statuses {OK, Created, Accepted, NoContent, NotModified, ResetContent} reset the failure streak (and, if IAT_SERVER_UNREACHABLE had been reported, emit IAT_SERVER_RECOVERED); anything else emits IAT_SERVER_ERROR and, at failureThreshold consecutive 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, comparing requestAt/sessionAt against logClock (not wall-clock): overdue request → IAT_TELLER_UNANSWERED; session with no video past videoTimeoutIAT_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, resets faultReportedForBlock, bumps messageCount.
  • A CONN_ERROR-matching line (unable to connect / no connection / refused / timed out / DNS) increments a streak; at failureThreshold consecutive → 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_UNREACHABLE had been reported, emits IAT_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.

  • [technical.agent] — module system, lifecycle, shared HTTP client settings, full outbound API surface
  • [platform.module-map]