Files
controls-web/controls-rework/tag-controls.php
2026-02-17 09:29:34 -06:00

257 lines
14 KiB
PHP

<?php // phpcs:ignoreFile
require __DIR__ . '/session.php';
require __DIR__ . '/userAccess.php';
if (($_SESSION['SESS_MEMBER_LEVEL'] ?? '') !== 'controls') {
header('Location: access-denied.php');
exit;
}
require __DIR__ . '/includes/control-tags.php';
$pageTitle = 'Tag Overrides';
$pageSubtitle = 'Adjust live values with safety guardrails';
$pageDescription = 'Tags are defined in config/control-tags.php and write straight through the Kepware IoT Gateway.';
if (empty($_SESSION['control_settings_csrf'] ?? '')) {
$_SESSION['control_settings_csrf'] = bin2hex(random_bytes(32));
}
$controlCsrfToken = $_SESSION['control_settings_csrf'];
$tagOptions = [];
$tagTypes = [];
$configError = null;
$configErrorDetail = null;
try {
$normalisedTags = control_tags_normalize();
foreach ($normalisedTags as $tag) {
$tagOptions[] = $tag;
$typeKey = $tag['type_key'];
if (!isset($tagTypes[$typeKey])) {
$tagTypes[$typeKey] = [
'label' => $tag['type_label'],
'count' => 0,
];
}
$tagTypes[$typeKey]['count']++;
}
uasort(
$tagTypes,
static function (array $left, array $right): int {
return strcasecmp($left['label'], $right['label']);
}
);
} catch (Throwable $exception) {
$configError = 'Unable to load tag configuration.';
$configErrorDetail = $exception->getMessage();
error_log('Control tag configuration error: ' . $configErrorDetail);
}
$hasTags = $configError === null && count($tagOptions) > 0;
require __DIR__ . '/includes/layout/header.php';
require __DIR__ . '/menuinclude.php';
?>
<div class="app-content">
<section class="data-panel">
<div
class="control-app"
data-control-app
data-get-url="data/tag-control-get.php"
data-update-url="data/tag-control-update.php"
data-csrf="<?php echo htmlspecialchars($controlCsrfToken); ?>"
>
<header class="control-app__header">
<div>
<h2>Manage live tag overrides</h2>
<p>Select a tag, review the live value, then stage a tweak or toggle the state. Writes commit instantly through Kepware.</p>
</div>
<div class="control-app__selector">
<div class="control-app__selector-grid">
<div class="control-app__selector-field">
<label for="control-type-select">Tag type</label>
<select
id="control-type-select"
name="tag_type"
data-control-type-filter
<?php echo $hasTags ? '' : 'disabled'; ?>
>
<option value="">All types<?php echo $hasTags ? '' : ' (0)'; ?></option>
<?php foreach ($tagTypes as $typeKey => $typeMeta) : ?>
<option value="<?php echo htmlspecialchars($typeKey); ?>">
<?php echo htmlspecialchars($typeMeta['label']); ?> (<?php echo (int) $typeMeta['count']; ?>)
</option>
<?php endforeach; ?>
</select>
</div>
<div class="control-app__selector-field">
<label for="control-tag-select">Tag</label>
<select
id="control-tag-select"
name="tag"
data-control-select
<?php echo $hasTags ? '' : 'disabled'; ?>
>
<?php if (!$hasTags) : ?>
<option value="" data-type-key="">No tags available</option>
<?php else : ?>
<option value="" data-type-key="">Select a tag&hellip;</option>
<?php foreach ($tagOptions as $option) :
$config = $option['config'] ?? [];
$sourceId = $config['source'] ?? $config['idnumber'] ?? $option['id'];
$typeRaw = isset($config['type']) && is_string($config['type']) ? $config['type'] : null;
$stepAttr = isset($option['step']) && $option['step'] !== null ? (string) $option['step'] : '';
$minAttr = isset($option['min']) && $option['min'] !== null ? (string) $option['min'] : '';
$maxAttr = isset($option['max']) && $option['max'] !== null ? (string) $option['max'] : '';
$precisionAttr = isset($option['precision']) && $option['precision'] !== null ? (string) $option['precision'] : '';
$unitsAttr = isset($option['units']) && $option['units'] !== null ? $option['units'] : '';
$descriptionAttr = isset($option['description']) && $option['description'] !== null ? $option['description'] : '';
?>
<option
value="<?php echo htmlspecialchars($option['id']); ?>"
data-type-key="<?php echo htmlspecialchars($option['type_key']); ?>"
data-type-label="<?php echo htmlspecialchars($option['type_label']); ?>"
data-control-flag="1"
data-source-idnumber="<?php echo htmlspecialchars((string) $sourceId); ?>"
data-type-raw="<?php echo htmlspecialchars((string) $typeRaw); ?>"
data-control-mode="<?php echo htmlspecialchars($option['control_mode']); ?>"
data-step-size="<?php echo htmlspecialchars($stepAttr); ?>"
data-min-value="<?php echo htmlspecialchars($minAttr); ?>"
data-max-value="<?php echo htmlspecialchars($maxAttr); ?>"
data-precision="<?php echo htmlspecialchars($precisionAttr); ?>"
data-units="<?php echo htmlspecialchars($unitsAttr); ?>"
data-description="<?php echo htmlspecialchars($descriptionAttr); ?>"
>
<?php echo htmlspecialchars($option['name']); ?>
</option>
<?php endforeach; ?>
<?php endif; ?>
</select>
</div>
</div>
</div>
</header>
<?php if ($configError !== null) : ?>
<div class="control-app__status" data-status data-status-type="error" role="alert">
<?php echo htmlspecialchars($configError); ?>
<?php if ($configErrorDetail !== null) : ?>
<details class="control-app__status-details">
<summary>Technical details</summary>
<pre><?php echo htmlspecialchars($configErrorDetail); ?></pre>
</details>
<?php endif; ?>
</div>
<?php elseif (!$hasTags) : ?>
<div class="control-app__status" data-status-type="info" role="status">
<strong>No control-enabled tags found.</strong>
<p>
Add entries to <code>config/control-tags.php</code> to expose Kepware tags for override access. Each entry
should define a friendly <code>name</code>, a Kepware <code>tag</code> (or array key), and optional guardrails
such as <code>step</code>, <code>min</code>, or <code>max</code>.
</p>
</div>
<?php else : ?>
<div class="control-app__body">
<div class="control-app__primary">
<div class="control-app__value">
<span class="control-app__value-label" data-value-label>Current value</span>
<span class="control-app__value-reading" data-current-value>&mdash;</span>
<span class="control-app__timestamp" data-current-timestamp></span>
<span class="control-app__value-pending" data-pending-indicator hidden>Pending</span>
</div>
<div class="control-app__controls">
<div class="control-app__numeric" data-numeric-controls>
<div class="control-app__controls-row">
<label for="control-step-input">Step adjustment</label>
<div class="control-app__step">
<input
id="control-step-input"
class="control-app__set-input"
type="number"
name="step"
value="1"
min="0"
step="0.1"
data-step-input
>
<div class="control-app__step-actions">
<button type="button" class="button button--danger" data-action="decrease">
&minus; Step
</button>
<button type="button" class="button button--success" data-action="increase">
+ Step
</button>
</div>
</div>
</div>
<div class="control-app__controls-row">
<label for="control-set-input">Pending value</label>
<div class="control-app__set" data-set-form>
<input
id="control-set-input"
class="control-app__set-input"
type="number"
step="0.1"
data-set-input
>
<button type="button" class="button button--ghost" data-action="apply">
Apply
</button>
</div>
</div>
</div>
<div class="control-app__boolean" data-boolean-controls hidden>
<div class="control-app__controls-row">
<span class="control-app__hint">Toggle state</span>
<div class="control-app__boolean-actions">
<button type="button" class="button button--danger" data-boolean="off">
Off
</button>
<button type="button" class="button button--success" data-boolean="on">
On
</button>
</div>
</div>
</div>
<div class="control-app__controls-row control-app__controls-row--aux">
<button type="button" class="button button--ghost" data-action="refresh">
Refresh value
</button>
<span class="control-app__hint">Use refresh after out-of-band changes to confirm the live reading.</span>
</div>
</div>
</div>
<aside class="control-app__aside">
<h3>Control checklist</h3>
<ul>
<li>Tag metadata lives in <code>config/control-tags.php</code>—update that file to curate what appears here.</li>
<li>Writes go directly to Kepware via the IoT Gateway REST API; results return in real time.</li>
<li>Every change is logged to the <code>control_write_log</code> table for traceability.</li>
<li>Numeric edits stage first; press <em>Apply</em> to commit. Boolean tags toggle immediately.</li>
</ul>
</aside>
</div>
<div class="control-app__status" data-status hidden></div>
<?php endif; ?>
</div>
</section>
</div>
<script src="assets/js/tag-controls.js?v=<?php echo rawurlencode($assetVersion ?? '2025.10.06.2'); ?>"></script>
<?php require __DIR__ . '/includes/layout/footer.php'; ?>