[server]
listen      = ":8080"
storage_dir = "./interfaces"

# ── UI presentation defaults (sent to the browser) ──────────────────────────
# default_zoom is the base UI scale used when a browser has no saved zoom of its
# own. Handy for HiDPI screens (e.g. 4K) whose OS scaling is left at 100%, where
# the browser reports devicePixelRatio=1 and the UI would otherwise be tiny. The
# in-app A+/A− control still overrides it per browser. 1.0 = no scaling.
[ui]
default_zoom = 1.0              # e.g. 1.5 or 1.75 on dense displays

# ── Identity & access control ───────────────────────────────────────────────
# The end-user identity is read from a header set by a trusted authenticating
# reverse proxy. Leave trusted_user_header empty for unproxied/dev/LAN use.
# SECURITY: only enable this when the proxy strips any client-supplied value of
# this header, otherwise it can be spoofed.
trusted_user_header = ""        # e.g. "X-Forwarded-User"
# Identity used when the trusted header is absent/empty. Empty = anonymous.
default_user        = ""

# ── Native Kerberos / SPNEGO authentication ─────────────────────────────────
# Alternative to a reverse proxy: uopi authenticates browsers directly via
# SPNEGO ("Negotiate"). Recommended when proxy-header auth lets some browsers
# (notably Firefox) silently fall through to default_user. When enabled, uopi
# challenges API requests with 401 WWW-Authenticate: Negotiate and resolves the
# user from the validated Kerberos ticket (short principal name, realm stripped);
# any inbound trusted_user_header value is ignored to prevent spoofing.
# Browsers must be told to perform SPNEGO for this origin
# (Firefox: network.negotiate-auth.trusted-uris).
[server.kerberos]
enabled           = false
# Path to the service keytab holding the HTTP service principal's key
# (e.g. HTTP/host.example.com@REALM). Required when enabled.
keytab            = ""          # e.g. "/etc/uopi/http.keytab"
# Optionally pin which keytab principal to accept tickets for. Empty = default.
service_principal = ""          # e.g. "HTTP/host.example.com"

# ── Built-in HTTP Basic authentication (PAM) ────────────────────────────────
# Another standalone alternative (no reverse proxy, no Kerberos): uopi challenges
# with 401 WWW-Authenticate: Basic and validates the username/password through the
# host PAM stack (/etc/pam.d/<pam_service>). On an SSSD/LDAP-joined host this
# reuses each user's normal login password with no directory config in uopi. The
# validated username feeds the same access pipeline. Mutually exclusive with
# Kerberos. REQUIRES a PAM-enabled build: `make backend-pam` (cgo + libpam); the
# default static binary cannot validate and refuses to start with this enabled.
# Basic credentials travel on every request — enable [server.tls] below unless on
# a fully isolated network.
[server.basic_auth]
enabled     = false
# PAM service name under /etc/pam.d/. Empty defaults to "uopi".
pam_service = ""                # e.g. "uopi" or "login"

# ── Built-in HTTP Basic authentication (LDAP) ───────────────────────────────
# Pure-Go alternative to basic_auth/PAM: validates the Basic credentials directly
# against an LDAP directory using "search then bind" — the same flow an SSSD/LDAP
# client uses. Needs NO cgo, so it works in the default fully-static binary
# (`make release`), unlike PAM. Mutually exclusive with kerberos and basic_auth.
# Defaults mirror SSSD: user_attr → "uid", user_object_class → "posixAccount",
# empty bind_dn → anonymous search. Enable TLS below (or use ldaps://) so
# passwords are not sent in clear text.
[server.ldap]
enabled            = false
# Directory endpoint(s), tried in order (SSSD ldap_uri).
uri                = ["ldaps://ldap.example.com"]
# Subtree user entries live under (SSSD ldap_search_base).
search_base        = "dc=example,dc=com"
# Attribute matched against the login name. Empty defaults to "uid".
user_attr          = ""         # e.g. "uid" (RFC2307) or "sAMAccountName" (AD)
# Object class to restrict the search. Empty defaults to "posixAccount".
user_object_class  = ""         # e.g. "posixAccount" or "user" (AD)
# Optional service account for the search. Empty bind_dn = anonymous search.
bind_dn            = ""
bind_password      = ""
# Upgrade a plain ldap:// connection to TLS before binding (ignored for ldaps://).
start_tls          = false
# Optional PEM CA bundle to trust a private directory CA.
ca_cert            = ""         # e.g. "/etc/uopi/ldap-ca.pem"
# Disable TLS certificate verification (testing only — insecure).
insecure_skip_verify = false

# ── Built-in TLS / HTTPS ────────────────────────────────────────────────────
# Terminate HTTPS directly without a reverse proxy. Strongly recommended whenever
# basic_auth is enabled. Cert and key are PEM paths; both required when enabled.
[server.tls]
enabled = false
cert    = ""                    # e.g. "/etc/uopi/tls/cert.pem"
key     = ""                    # e.g. "/etc/uopi/tls/key.pem"
# Optionally run a plain-HTTP listener that 301-redirects visitors to the HTTPS
# service, so users who type http:// (or omit the scheme) are upgraded instead of
# hitting the TLS port with a cleartext request ("client sent an HTTP request to
# an HTTPS server"). Empty = disabled.
redirect_from = ""              # e.g. ":80"

# Role-based access through group memberships. Each [[groups]] block lists
# members by role; a user's effective global capability is the highest role
# across all their memberships, along this ladder:
#   viewer < operator < logiceditor < auditor < admin
# Capabilities: operator+ may write signals; logiceditor+ may edit panel and
# control logic; auditor+ may view the audit log; admin may open the admin pane.
#
# The built-in "public" group is implicit (every user is a viewer of it). A
# config with NO roles assigned anywhere is fully open (everyone is admin), for
# trusted-LAN/dev use; assigning any role switches to strict mode where unlisted
# users are read-only viewers. Once changed at runtime via the admin pane,
# {storage_dir}/access.json supersedes the groups below.
#
# Groups may nest via "parent": a member of a parent group inherits its role on
# every descendant group too (unless overridden lower).
# [[groups]]
# name   = "public"
# admins = ["alice"]          # alice is a global admin
# [[groups]]
# name         = "operations"
# operators    = ["bob"]
# auditors     = ["carol"]
# [[groups]]
# name         = "engineers"
# parent       = "operations" # bob inherits operator here
# logiceditors = ["dave"]
# viewers      = ["erin"]

[datasource.epics]
enabled            = true
ca_addr_list       = ""   # overrides EPICS_CA_ADDR_LIST if set
archive_url        = ""   # EPICS Archive Appliance base URL
channel_finder_url = ""   # e.g. http://channelfinder:8080/ChannelFinder
auto_sync_filter   = ""   # e.g. area=StorageRing or tags=production

[datasource.synthetic]
enabled = true

# Modbus TCP. Each device is polled over its own connection; registers become
# signals named "device:register". Disabled unless enabled = true and at least
# one device is declared.
[datasource.modbus]
enabled          = false
poll_interval_ms = 1000   # default poll period for every register

# [[datasource.modbus.devices]]
# name       = "plc1"
# address    = "192.168.1.50:502"   # ":502" appended if no port given
# unit_id    = 1
# timeout_ms = 3000
#
#   [[datasource.modbus.devices.registers]]
#   name        = "temperature"
#   kind        = "holding"   # holding | input | coil | discrete
#   address     = 100
#   encoding    = "int16"     # uint16|int16|uint32|int32|float32|float64
#   word_order  = "big"       # big (default) | little, for multi-word encodings
#   unit        = "C"
#   scale       = 0.1         # value = raw*scale + offset (scale 0 → 1)
#   offset      = 0.0
#   min         = -50
#   max         = 200
#   writable    = false       # input/discrete are always read-only
#   description = "Process temperature"

# SCPI instruments over raw TCP sockets (line-based "SCPI raw", port 5025).
# Channels become signals named "instrument:channel". A vxi11 transport is
# reserved for a future release.
[datasource.scpi]
enabled          = false
poll_interval_ms = 1000

# [[datasource.scpi.instruments]]
# name       = "dmm1"
# transport  = "raw"               # raw (default); vxi11 reserved
# address    = "192.168.1.60:5025" # ":5025" appended if no port given
# timeout_ms = 3000
# terminator = "\n"
#
#   [[datasource.scpi.instruments.channels]]
#   name        = "voltage"
#   query       = "MEAS:VOLT?"      # SCPI query whose response is the value
#   write_cmd   = "VOLT %v"         # printf template; omit for read-only
#   type        = "float"           # float (default) | int | string | bool
#   unit        = "V"
#   min         = 0
#   max         = 30
#   description = "DC voltage"
