Skip to content

Custom-page SDK API reference

The SDK provides project HTML with realtime Tags, typed reads and writes, history, alarms, user, permissions, language, navigation, device popups, and table helpers. Project code handles layout and business calculations.

SDK
Page locationProject HTML page deployed in Mingcedar and run inside a sandbox iframe
Minimum accessDeclare Tag scope, call ready() for the API, then read, subscribe, or query history and alarms
Use casesShift reports, energy dashboards, device diagnostics, and customer-specific pages

SDK: Custom-page SDK API reference

Native HTML/Vue integration and public API reference. Tag reads and writes use the PPT ReadTag*/WriteTag* names; the system LANG control switches languages.

What the SDK owns

The SDK provides project HTML with realtime Tags, typed reads and writes, history, alarms, user, permissions, language, navigation, device popups, and table helpers. Project code handles layout and business calculations.

  • Good fits: shift production reports, daily energy reports, equipment health pages, quality trace pages, work-order boards, and customer views.
  • Page forms: plain HTML, native ES modules, or built Vue pages; ship CSS, charts and components with the project.
  • Data sources: declared tags, history data, alarm data, and language resources.
  • Responsibility boundary: Mingcedar provides data and permission boundaries; the project page owns presentation and business calculations.
  • SDK version: 1.5.0.

Expected result Start with SDK quick start. Except for complete pages/components, snippets assume api, the required DOM elements, and project renderers are ready.

Runtime origin and permission boundaries

Project pages run in an iframe on a separate Screen Origin, isolated from the Portal. The bridge validates origin, window, session, and document identity. Pages may use their own origin’s cookies, storage, URL parameters, and ordinary Web Workers, but not Service Workers. They cannot access Portal cookies, administration DOM, or configuration APIs. Data and writes remain subject to page declarations and server authorization.

What page-owned JavaScript can do

Custom pages may use DOM, CSS, components, chart libraries, and interactions allowed by the browser and iframe sandbox. The SDK governs Mingcedar Host capabilities.

  • Supported: DOM/component logic and alert, confirm, and prompt.
  • Supported: window.open, form submission, browser navigation, and user-initiated downloads; browser popup and user-activation rules still apply.
  • Clipboard API is supported only when secure-context, user activation, browser permission, and enterprise-policy requirements are met.
  • Service Worker registration is unsupported. The page does not receive Portal cookies, login tokens, Admin DOM, configuration APIs, or raw Host service objects.
  • The page cannot poll a PLC directly; the SDK does not provide alarm acknowledge, shelve, or close operations.
  • api.supports() checks Host Bridge capabilities, not browser API availability or permission.

SDK loading is automatic; module imports are optional

Runtime loads the SDK for published HTML/Vue pages. Call await HQVISU_CUSTOM_PAGE.ready(); no extra script tags are needed.

  • An ES module can import { ready } from "/runtime/hqVisuDesigner/hq-custom-page.mjs"; it reuses the installed SDK.
  • Only non-injected environments such as a custom Mock need hq-common.js followed by hq-custom-page.js. Loading files does not create runtime permissions.
  • Static Tag declarations remain in entry HTML. No SDK npm package is required. A Vite dev server or file:// is not the runtime environment.
JavaScriptJavaScript
HQVISU_CUSTOM_PAGE.ready().then(async (api) => {
  // Use the examples below here. Reuse this api object.
}).catch(error => console.error(error.message));

How to declare tag scope

The server grants page capabilities from supported HTML JSON and data declarations. Use full dotted Tag codes, such as <device-code>.<temperature-tag-code>, and canonical bit references [0]–[31]. Invalid or over-limit declarations affect capability grants.

  • Supported config block ids: hqvisu-custom-page, hqvisu-custom-page-config, hqvisu-page-config, and hqvisu-dependencies.
  • Readable aliases: readableTags, tags, tagNames, readTags, and watchTags.
  • Writable aliases: writableTags and writeTags; matching declarations under outputs are also collected.
  • Under dependencies, supported fields are tags, tagNames, readableTags, readTags, writableTags, and writeTags.
  • Values may be arrays or strings separated by newline, comma, or semicolon.
  • Bit references such as <device-code>.<status-word-tag-code>[3] are read-only, for example ReadTagBoolean(name, null). Write a bit with WriteTagBit("<device-code>.<status-word-tag-code>", 3, "set") and declare the parent Tag readable and writable. No write method accepts [3] in its target. History also queries the parent Tag.
  • <device-code> and <tag-code> in this manual are placeholders. Replace them with actual project codes before publishing.
  • ready({ tags }) requests tracking; it does not enlarge server write, history, alarm or Popup scope. Put a controlled Tag that needs readback in both readableTags and writableTags.
HTMLHTML
<script type="application/json" id="hqvisu-custom-page">
{
  "readableTags": [
    "<device-code>.<temperature-tag-code>",
    "<device-code>.<pressure-tag-code>",
    "<device-code>.<run-state-tag-code>",
    "<device-code>.<output-tag-code>",
    "<device-code>.<energy-tag-code>",
    "<device-code>.<setpoint-tag-code>"
  ],
  "writableTags": ["<device-code>.<setpoint-tag-code>"]
}
</script>

Dynamic exact-on-use Tags from parameters, loops, and composition

Use exact-on-use for full Tag codes built from runtime parameters, loops, or functions. Each exact Tag is checked for format, existence, enabled state, page session, and read/write rules. Wildcards, catalog enumeration, and unlimited access are unsupported.

  • liveTagRead.maxActiveTags accepts 1–8000. This small-page example budgets 64 read Tags and 16 write Tags. Size it for actual use; quotas are not a performance guarantee.
  • The formal liveTagWrite.maxActiveTags limit is 2000.
  • Good fits: select a device from page parameters, loop over a known device list, build a full Tag in a function, or bind a component to a business object.
  • Unsupported: <device-code>.* search, Tag catalog reads, device enumeration, or bypassing per-Tag Host validation.
  • Dynamic declaration expands exact live read/write only. History, alarms, device popups, and prefetch retain their own scopes.
  • Use static declarations for fixed Tags to support initial prefetch, subscriptions, and scope management.
HTMLHTML
<script type="application/json" id="hqvisu-custom-page">
{
  "readableTags": ["<device-code>.<run-state-tag-code>"],
  "dependencies": {
    "runtimeAccess": {
      "liveTagRead": { "mode": "exact-on-use", "maxActiveTags": 64 },
      "liveTagWrite": { "mode": "exact-on-use", "maxActiveTags": 16 }
    }
  }
}
</script>

const deviceCode = new URLSearchParams(location.search).get("device");
const temperatureTag = `${deviceCode}.<temperature-tag-code>`;
api.track(temperatureTag);

ready() and resource lifetime

HQVISU_CUSTOM_PAGE.ready(options?), ready(callback), and ESM ready(options?) return Promise<PageApi>. Initialization confirms the bridge, not GOOD values for every Tag. One SDK belongs to the HTML document and is reused across Vue mounts.

  • tags: string[] adds tracked Tags; prefer static declarations. Display initialization errors with try/catch; do not loop ready retries.
  • subscribe, alarms.subscribe, language.onChange and bindJsonTable return cleanup functions. Call each component-owned cleanup on unmount.
  • destroy() permanently ends the document. A later ready() in that document rejects with RUNTIME_PAGE_DESTROYED. Do not destroy on ordinary Vue unmount; only end the whole document.
  • pagehide with persisted=true means BFCache retention, not permanent destruction. An unmounted component must not register subscriptions after async setup resolves.
  • timeoutMs controls the host wait (10000 ms in the examples), not PLC read/write timeouts. Check each Tag quality after ready resolves.
JavaScriptJavaScript
const api = await HQVISU_CUSTOM_PAGE.ready();
const stop = api.subscribe(["<device-code>.<tag-code>"], renderValue);
// renderValue is your UI renderer.
window.addEventListener("pagehide", event => {
  if (!event.persisted) { stop(); api.destroy(); }
});

SDK global-object API

The global object is HQVISU_CUSTOM_PAGE, with compatibility alias HQVisuCustomPage. These members are available before ready(); page data requires a ready Host connection.

  • version: string 1.5.0.
  • ready(options | callback): returns Promise<PageApi>.
  • whenVisualReady({ timeoutMs? }): waits for the first authorized visual snapshot and returns Promise<void>; ordinary pages use ready() to obtain the api.
  • destroy(): permanently ends this document's SDK connection. On component unmount, cancel only subscriptions owned by that component.
  • normalizeTagName(value): stringifies, trims, and removes one matching outer quote pair; returns a string.
  • tagList(value): accepts strings, nested arrays, or a scalar; splits newline/comma/semicolon text, normalizes, drops empty values and legacy placeholders shaped like <TagName>, and deduplicates; returns string[].
  • renderTable(target, rows, options): renders a lightweight HTML table and returns { rowCount, columnCount }.
JavaScriptJavaScript
const tag = "<device-code>.<tag-code>";
const normalized = HQVISU_CUSTOM_PAGE.normalizeTagName(tag);
const tags = HQVISU_CUSTOM_PAGE.tagList([tag, tag]);

PageApi root object and capability matrix

Use the root methods returned by ready() and stable namespaces such as api.auth, api.permissions, api.history, and api.alarms. Low-level api.host interfaces vary by transport.

  • Properties: version, bridgeVersion, schemaRevision, frozen capabilities, and low-level host.
  • Discovery: getCapabilities() returns a mutable copy; supports(name) returns a boolean and false for an unknown capability.
  • Namespaces: auth, permissions, language, history, alarms, navigation, and dialog.
  • Root methods: track, subscribe, all read/write methods, queryHistory, translate / t, renderTable, bindJsonTable, and destroy.
  • Capability names: page.parameters, tag.read, tag.write, history.read, user.read, navigation, popup, alarm.read, and log.write.
  • supports() indicates transport exposure only; it is not user permission and does not enlarge page scope.
  • log.write is optional: when supports("log.write") is false, do not call it or report logging success. PageApi exposes no standalone root log.write API.

Current-user and permission API

The stable session result is { user, permissions }. For an authenticated user, user contains username, displayName, and permissions. An anonymous direct Screen Gateway returns user: null and an empty permission array. Tokens, cookies, roles, and page-capability secrets are never exposed.

  • api.auth.getSession(): session snapshot. Use await api.auth.getSession() for both synchronous and asynchronous Hosts.
  • api.auth.getCurrentUser() / getUser() → user snapshot or null; getUsername() → username; getDisplayName() → display name, falling back to username.
  • api.auth.hasPermission(code) / api.permissions.has(code): returns Promise<boolean>.
  • api.auth.requirePermission(code) / api.permissions.require(code): resolves true when allowed and rejects when denied.
  • These methods belong to PageApi.auth. Prefer this public wrapper over transport-specific objects under api.host.
JavaScriptJavaScript
const api = await HQVISU_CUSTOM_PAGE.ready();
const session = await api.auth.getSession();
const canWrite = api.supports("tag.write")
  && await api.permissions.has("tag:write");

renderCurrentUser(session.user);
setWriteControlsEnabled(canWrite);

Read API: the same names as PPT

Reads synchronously return the current collected snapshot without polling a PLC. Typed reads with explicit fallback return it for non-GOOD/missing quality or conversion failure; omitting fallback is not a quality check. ReadTag returns raw values without typed conversion or quality filtering. Use ReadTagDouble(name, null) and show quality to distinguish a real zero from unavailable data.

  • ReadTag(name, fallback?) → raw snapshot value, without typed conversion.
  • ReadTagBoolean(name, fallback?) → boolean; recognizes booleans, finite numbers and true/false, 1/0, on/off, yes/no text; default false.
  • ReadTagInteger(name, fallback?) → safe integer truncated toward zero; default 0. Preserve large integers as raw/text instead of coercing to Number.
  • ReadTagDouble(name, fallback?) → finite number; default 0.
  • ReadTagString(name, fallback?) → display text, preferring displayValue and numeric precision/round rules; default empty string. GOOD empty strings are valid.
  • ReadTagJson(name, fallback?) → parsed JSON value; default null. It may be an object, array or scalar.
  • ReadTagDateTime(name, fallback?) → valid Date or fallback; default null. See the UTC-field rule for PLC calendars below.
  • ReadCalculatedTag(name, fallback?) → calculated number/boolean with calculated-source and GOOD checks; default null. Use the calculated Tag code and declare its read scope.
  • ReadTagQuality(name, fallback?) → "good" | "bad"; ReadTagQualityGood(name) → boolean. Missing quality defaults to bad; do not pass good to conceal missing quality.
  • getMeta(name) → metadata object. dataType, qualityDetail, timestamp, source, and other fields depend on the supplied snapshot; stale/pending diagnostics do not add a third ReadTagQuality result.
JavaScriptJavaScript
const name = "<device-code>.<tag-code>";
const raw = api.ReadTag(name);
const quality = api.ReadTagQuality(name); // "good" | "bad"
const usable = api.ReadTagQualityGood(name);
const value = api.ReadTagDouble(name, null);
document.getElementById("value").textContent = usable && value !== null ? String(value) : "—";
const meta = api.getMeta(name); // source, dataType, ts/persistenceVersion when supplied
// Choose the reader matching each Tag's type; do not cast one Tag to every type.
const running = api.ReadTagBoolean("<device-code>.<run-state-tag-code>", null);
const count = api.ReadTagInteger("<device-code>.<count-tag-code>", null);
const label = api.ReadTagString("<device-code>.<text-tag-code>", "—");
const rows = api.ReadTagJson("<device-code>.<json-tag-code>", null);
const date = api.ReadTagDateTime("<device-code>.<datetime-tag-code>", null);
const calculated = api.ReadCalculatedTag("<calculated-tag-code>", null);

Subscribe to realtime changes

track(tags) normalizes and tracks Tags, returning deduplicated names. subscribe(tags, callback) returns an idempotent unsubscribe function and calls (items, api). Read the snapshot for initial rendering, then subscribe to changes.

  • Each change includes tagName and value; fullCode, quality, timestamp, source, and other metadata are provided when available.
  • A page may hold at most 8000 active unique Tags. Invalid, missing, disabled, or rejected candidates do not permanently consume the budget.
  • The wire boundary carries at most 256 Tags per batch and the SDK runs up to four batches concurrently; a large page still uses one logical realtime subscription and one WebSocket.
  • Use the callback's second argument to read a consistent current snapshot; group high-frequency Tags and update UI in one pass.
  • Unsubscribe when scope changes or the component exits. destroy() cleans up remaining Tag and alarm subscriptions.
JavaScriptJavaScript
const tag = "<device-code>.<process-tag-code>";
function renderValue() {
  const value = api.ReadTagDouble(tag, null);
  document.getElementById("value").textContent =
    api.ReadTagQualityGood(tag) && value !== null ? String(value) : "—";
}
renderValue();
const stopRealtime = api.subscribe([tag], renderValue);
// stopRealtime() when this view no longer needs the subscription.

Write API: parameters, prerequisites and returns

Typed writes use (name, value, options?). Always await the result: a Host may return synchronously, a Promise, or undefined; do not assume .then() exists. Writes require tag.write capability, admitted scope, user tag:write permission, Tag control permission and valid quality; execution rechecks Owner and user action.

  • WriteTagBoolean(name, value, options?): use boolean, 0/1 or explicit boolean text; "false" means false, not JavaScript truthiness.
  • WriteTagInteger(name, value, options?): number must be a safe integer; BigInt or decimal integer text preserves precision. The service checks the actual device range.
  • WriteTagDouble(name, value, options?): finite numeric value or valid numeric text; reject NaN, Infinity and blank form input.
  • WriteTagString(name, value, options?): text; empty string is an intentional value, still subject to PLC encoding and length limits.
  • WriteTagJson(name, value, options?): pass the actual JSON object/array/value, without JSON.stringify on an object. Restore original empty text with WriteTagString.
  • WriteTagDateTime(name, value, options?): Date or date text; PLC calendar metadata selects serialization, as explained below.
  • WriteTag(name, value, options?) / WriteTag({ TagName, Value, ValueType, ... }): generic entry, also accepting a camelCase payload; false, 0, empty text and JSON values are retained.
  • WriteTagBit(name, bitIndex, bitAction, options?): name is the parent Tag, with GOOD quality and a supported 8/16/32-bit integer type. The index must fit that type and be within 0–31; actions are set/reset/toggle. Blank indexes are invalid. Protocol support varies; Modbus requires confirmed FC22 support, with no toggle or full-word read/modify/write fallback.
  • options.reason supplies an operation reason; typed helpers fix their type and caller source. Precheck with await api.auth.hasPermission("tag:write"); capability tag.write is not the permission code.
  • Initiate from a user click handler. Page confirmation does not replace configured Host confirmation. Do not write automatically from timers, loading or subscriptions.
Bind a write button inside ready (id="write", result id="result")JavaScript
const button = document.getElementById("write");
const output = document.getElementById("result");
button.disabled = !(api.supports("tag.write") && await api.auth.hasPermission("tag:write"));
button.onclick = async () => {
  if (button.disabled) return;
  const tag = "<device-code>.<tag-code>";
  if (!api.ReadTagQualityGood(tag)) { output.textContent = "Tag unavailable"; return; }
  if (!confirm("Write 12.5?")) return;
  button.disabled = true;
  try {
    const result = await api.WriteTagDouble(tag, 12.5, { reason: "Operator setpoint update" });
    const done = result?.accepted === true && result?.status === "done"
      && result?.outcomeUncertain !== true && result?.operatorVerificationRequired !== true
      && result?.transportOnly !== true;
    output.textContent = (done ? "Command completed; verify collected value: " : "Result unconfirmed; verify before another write: ")
      + JSON.stringify(result ?? null);
  } catch (error) {
    output.textContent = "Result unconfirmed: " + error.message + " " + (error.commandId || error.result?.commandId || "");
  }
  // Reconcile the result before another write. Never retry automatically.
};

Classify the command, then verify normal collected readback

accepted=true means admitted. Inspect status, commandId, journalId and uncertainty fields. currentValue may be a runtime-write echo and is not proof of later normal collection. In this fragment nextValue and reason come from validated page inputs.

  • done: command completed; verify normal collected source, newer timestamp/version and quality before confirming the device value. Do not overwrite live display with the target.
  • proxied: inspect actual completion evidence after forwarding. sent or transportOnly=true proves transport only, not PLC execution.
  • readback_failed/readback_mismatch/latest_superseded: show reconciliation required, not a confirmed collected value.
  • pending_verification/outcome_uncertain, timeout, disconnect, undefined or unknown status: keep operations blocked, retain commandId, reconcile records and actual values, without automatic retry.
  • accepted=false, cancelled, failed or page_changed: no confirmed completion; show the actual result.
JavaScriptJavaScript
// Call from the existing async click handler, after input/quality/permission checks.
try {
  const result = await api.WriteTagDouble("<device-code>.<setpoint-tag-code>", nextValue, { reason });
  const confirmed = result?.accepted === true && result?.status === "done"
    && result?.outcomeUncertain !== true && result?.operatorVerificationRequired !== true
    && result?.transportOnly !== true;
  document.getElementById("write-result").textContent =
    (confirmed ? "Command completed; verify normal collected state: " : "Verify before another write: ")
    + JSON.stringify(result ?? null);
  // Do not overwrite the live value with nextValue or result.currentValue.
  // Reconcile the command record and a later GOOD collected sample before another operation.
} catch (error) {
  document.getElementById("write-result").textContent =
    "Result unconfirmed; do not retry automatically. " + error.message;
}

Expected result PageApi has no public command-journal query method. Retain commandId from the receipt or error for an authorized administration/operations integration to reconcile. Use subscribe and getMeta to inspect later GOOD normal collection. Keep controls disabled while unconfirmed; allow a new operator action only after reconciliation, never based solely on a delay, refresh or value equality.

Query history and trends

api.history.query(request) and root alias api.queryHistory(request) both return Promise<HistoryQueryResult>. The request reads the history store and does not poll a PLC. Every Tag must be inside the page history-read scope.

  • Request fields: tags, start, end, interval, and aggregate, with optional points.
  • tags is required. The submitted array accepts at most 16 entries, including duplicates; the server then deduplicates them. History does not accept [0] through [31] bit references; query the parent Tag.
  • start and end must be RFC 3339 timestamps, with a maximum range of 366 days.
  • interval supports m, h, d, and w, for example 15m, 1h, 1d, or 1w; minimum 1 minute.
  • aggregate supports avg, min, max, sum, count, first, last, delta, and raw.
  • delta fits meters and accumulating counters such as energy, water, or production total.
  • The example queries the last seven days. The target Tags need recorded history for data to appear.
  • Omitted or zero points defaults to 20000. Maximum is 20000 and it cannot be smaller than the deduplicated Tag count.
JavaScriptJavaScript
const weekEnd = new Date();
const weekStart = new Date(weekEnd.getTime() - 7 * 24 * 60 * 60 * 1000);

const weekEnergy = await api.history.query({
  tags: [
    "<primary-device-code>.<energy-tag-code>",
    "<secondary-device-code>.<energy-tag-code>"
  ],
  start: weekStart.toISOString(),
  end: weekEnd.toISOString(),
  interval: "1d",
  aggregate: "delta",
  points: 20000
});

renderEnergyReport(weekEnergy.items, {
  interval: weekEnergy.effectiveInterval,
  truncated: weekEnergy.truncated
});

How to consume a history result

The result is { items, effectiveInterval, rangeStart, rangeEnd, truncated }. items contains one series per requested Tag. A series is { tag, tagId, displayName, points }; every point is { t, value, quality }.

  • tag is the full Tag code, tagId is the server identifier, and displayName may be empty.
  • points[].t is RFC 3339, value is raw or aggregated, and quality is that point's quality.
  • effectiveInterval is the interval actually used and may be coarser than requested when the point budget is tight.
  • rangeStart and rangeEnd are the actual UTC/RFC 3339 range.
  • truncated: true means the aggregation interval was coarsened or raw records were cut off by the point budget. Show the effective interval and an incomplete-result notice. To retrieve complete raw records, query smaller time windows.
  • Read each series with series.points || [] to handle missing data. Render an empty state instead of treating absent records as zero.
JavaScriptJavaScript
for (const series of weekEnergy.items || []) {
  const chartPoints = (series.points || []).map((point) => ({
    time: new Date(point.t),
    value: point.quality === "good" ? point.value : null,
    quality: point.quality
  }));
  drawEnergySeries(series.displayName || series.tag, chartPoints);
}
showActualInterval(weekEnergy.effectiveInterval, weekEnergy.truncated === true);

Alarm-scope declaration

The server mints alarm scope from page declarations; client query fields cannot enlarge it. Declare dimensions under alarm, alarmScope, or pageAlarm in the JSON configuration. Readable Tags are also added automatically to the alarm Tag scope.

  • Supported dimensions: tags, plcCodes, functionGroupCodes, deviceGroupCodes, and signalGroupCodes.
  • With no alarm scope, active and historical alarm queries return an empty scope.
  • Dynamic liveTagRead does not enlarge alarms. Statically declare required alarm Tags or groups.
  • The server overwrites any client scope fields with the minted capability; additive JSON cannot reveal more alarms.
  • Group codes need the device prefix. Empty scope means unbound, not all alarms. showWindow controls the page alarm rail, separately from the equipment Popup.
HTMLHTML
<script type="application/json" id="hqvisu-custom-page">
{
  "readableTags": ["<device-code>.<temperature-tag-code>"],
  "alarmScope": {
    "plcCodes": ["<device-code>"],
    "functionGroupCodes": ["<device-code>.<function-group-code>"],
    "deviceGroupCodes": [],
    "signalGroupCodes": []
  }
}
</script>

Query and subscribe to alarms

Alarm APIs provide synchronous snapshots, asynchronous queries, and change subscriptions. getActive(), getActiveAlarms(), and queryHistory(request) return paged Promise<AlarmQueryResult> results.

  • getSnapshot() returns the current Host snapshot synchronously. A Host may return an array or a wrapper such as { alarms } or { activeAlarms }.
  • Query fields: start, end, severity, state, source / q, limit, offset, cursorTs, and cursorId.
  • History defaults to the most recent 24 hours when times are omitted; one query spans at most 24 hours.
  • The example queries the last hour; the history result is empty if no alarm occurred in that period.
  • Omitted or zero limit is 500 and the maximum is 500; larger values are clamped. offset must be nonnegative.
  • Result: { items, count, hasMore, nextCursorTs, nextCursorId }. When more data exists, pass both next-cursor fields into the next request.
  • subscribe(callback) returns an idempotent unsubscribe function and calls (items, api). A Host may send a complete snapshot or change items; use it as a refresh signal rather than blindly appending.
  • getActive(request?) accepts the filters/pagination above; getActiveAlarms() is the no-argument convenience entry.
JavaScriptJavaScript
function alarmItemsFromSnapshot(snapshot) {
  if (Array.isArray(snapshot)) return snapshot;
  if (Array.isArray(snapshot?.alarms)) return snapshot.alarms;
  if (Array.isArray(snapshot?.activeAlarms)) return snapshot.activeAlarms;
  return [];
}

renderAlarmSummary(alarmItemsFromSnapshot(api.alarms.getSnapshot()));
const activeResult = await api.alarms.getActiveAlarms();
renderAlarmSummary(activeResult.items || []);

const alarmEnd = new Date();
const alarmStart = new Date(alarmEnd.getTime() - 60 * 60 * 1000);
const historyResult = await api.alarms.queryHistory({
  start: alarmStart.toISOString(),
  end: alarmEnd.toISOString(),
  severity: ["critical", "high"],
  state: ["active", "recovered"],
  limit: 200
});

const stopAlarms = api.alarms.subscribe(() => scheduleActiveAlarmRefresh());

Alarm item and paging fields

Identify events by id and display state from state and timestamps. Leave missing optional fields empty; use messageText/displayName when translations are missing, without exposing internal keys.

  • Identity: id, deviceId, deviceCode, deviceName, tagId, tagCode, fullCode, and displayName.
  • Content: messageText, alarmKind, severity, state, displayValue, and quality.
  • Times: activeTs, recoverTs, closeTs, ackTs, shelvedUntil, and serverTs.
  • Flags: suppressed; some items also carry ackUserId, areaCode, objectIdentifier, and groupCode.
  • count is this page's item count and hasMore signals another page. Cursor paging avoids offset duplicates or gaps while alarms change.
  • The SDK provides alarm reads and subscriptions, but no acknowledge, shelve, or close operations.

Navigation: parent, child, assets root and URL

Custom api.navigation.gotoPage and PPT hqVisuDesigner.openPage share resolution. Targets need not all be prelisted. Mapping provides names/breadcrumbs; explicit paths take precedence over same-name mappings.

  • gotoPage(target, options?): resolve ./, ../, child and sibling paths from the current screen URL; append .html to local extensionless paths. Navigate across folders while remaining under the assets root.
  • assets/<folder>/... and /assets/<folder>/... address the current Runtime Unit user assets root, including navigation from published or built-in screens. Other local relative paths in those sources retain their package boundary.
  • openUrl(target, options?): uses the same rules; write external targets as http://, https:// or //. Dotted folder names are not guessed to be domains. Preserve query/hash.
  • gotoAndon(target, options?): request an Andon/fullscreen host window.
  • gotoScreen(screenCode): navigate by screen menu code, not an arbitrary file path.
  • options: openInNewWindow, frameTarget, parameters:[{key,value}]. At most 64 parameters, keys ≤256 and values ≤2048 characters; read them with URLSearchParams.
  • Legal HTTP(S) addresses can navigate; third-party iframe embedding depends on its CSP/X-Frame-Options. Open Portal administration routes top-level/in a new window. External sites gain no Tag or Portal identity.
  • Reject escapes above assets, javascript:/data:/file:, backslashes and ambiguous encodings. Each destination HTML needs its own static data scope. true means dispatch, not confirmed target loading.
JavaScriptJavaScript
// Inside the ready() scope; the HTML contains the corresponding buttons.
document.getElementById("parent").onclick = () =>
  api.navigation.gotoPage("../overview.html?view=summary#status");
document.getElementById("child").onclick = () =>
  api.navigation.gotoPage("details/equipment", { parameters: [{ key: "device", value: "<device-code>" }] });
document.getElementById("other-package").onclick = () =>
  api.navigation.gotoPage("/assets/<page-folder>/overview.html");
document.getElementById("external").onclick = () =>
  api.navigation.openUrl("https://<server-host>/manual", { openInNewWindow: true });
// In the destination page:
const device = new URLSearchParams(location.search).get("device");

Device-popup API

api.dialog.openDevicePopup(payload) asks the Host to open the formal device popup; api.dialog.close() asks it to close the current Host dialog. Check api.supports("popup") and call from a real click. A true result means dispatched only.

  • plc or deviceCode is required, and that PLC must have at least one Tag inside page read scope.
  • Optional focusTagNames contains at most 64 Tags, all inside page read scope.
  • templateCode / id can select a popup template.
  • diagnosisControl and groupRightIndex are optional popup context.
  • A device popup does not expand page read/write scope or expose popup service objects.
  • Configure bound Tags, controls, alarms and trend areas in Popup Templates first. Real Popup writes still enforce user permissions, control flags, quality and Host confirmation.
JavaScriptJavaScript
// Add <button id="open-popup">Open equipment</button>
// and <button id="close-popup">Close</button> to the HTML.
// Place this code inside the existing ready() scope.
document.getElementById("open-popup").disabled = !api.supports("popup");
document.getElementById("open-popup").onclick = () => {
  try {
    const dispatched = api.dialog.openDevicePopup({
      plc: "<device-code>",
      templateCode: "<popup-template-code>",
      focusTagNames: ["<device-code>.<setpoint-tag-code>"]
    });
    if (dispatched !== true) throw new Error("Popup request was not dispatched");
    // Verify the actual drawer and target Tag; true is not a load-complete receipt.
  } catch (error) { document.getElementById("status").textContent = error.message; }
};
document.getElementById("close-popup").onclick = () => api.dialog.close();

Complete monitoring page (optional)

This HTML includes a live value, one setpoint write, active alarms, and trend history. Replace Tags and declarations, and adjust the input range before publishing. Runtime supplies the SDK.

View complete source
HTMLHTML
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Mingcedar equipment monitor</title>
  <script type="application/json" id="hqvisu-custom-page">
  {"readableTags":["<device-code>.<process-tag-code>","<device-code>.<setpoint-tag-code>"],
   "writableTags":["<device-code>.<setpoint-tag-code>"]}
  </script>
  <script>
    window.HQVISU_PAGE_META = { layoutMode: "native" };
    window.HQVISU_PAGE_NAME = "equipment-monitor";
    window.HQVISU_PAGE_ALARM = {
      showWindow: true, tags: ["<device-code>.<process-tag-code>"],
      plcCodes: [], functionGroupCodes: [], deviceGroupCodes: [], signalGroupCodes: []
    };
  </script>
  <style>
    body { font: 16px/1.6 system-ui; margin: 24px; color: #16263c; }
    section { margin-block: 24px; } button, input { font: inherit; padding: 8px; }
    table { border-collapse: collapse; width: 100%; }
    th, td { text-align: left; padding: 8px; border-bottom: 1px solid #d9e1eb; }
    pre { white-space: pre-wrap; overflow-wrap: anywhere; }
  </style>
</head>
<body>
  <h1>Equipment monitor</h1>
  <p id="status" role="status">Connecting to Mingcedar…</p>
  <section><h2>Live value</h2><output id="value">Waiting for data</output></section>
  <section><h2>Setpoint</h2>
    <label>Target (0–100) <input id="setpoint" type="number" min="0" max="100" step="0.1"></label>
    <button id="write" disabled>Write setpoint</button><pre id="write-result"></pre>
  </section>
  <section><h2>Active alarms</h2><button id="alarms" disabled>Refresh alarms</button><div id="alarm-list"></div><p id="alarm-status"></p></section>
  <section><h2>Last hour</h2><button id="history" disabled>Query historical trend</button><div id="trend"></div><p id="trend-status"></p></section>
  <script>
  (async function () {
    const element = (id) => document.getElementById(id);
    const processTag = "<device-code>.<process-tag-code>";
    const setpointTag = "<device-code>.<setpoint-tag-code>";
    const api = await HQVISU_CUSTOM_PAGE.ready({timeoutMs: 10000});
    let stopValues = () => {};
    let stopAlarms = () => {};
    element("status").textContent = "Connected";
    window.addEventListener("pagehide", event => {
      if (!event.persisted) { stopValues(); stopAlarms(); api.destroy(); }
    });
        function refreshValue() {
      const quality = api.ReadTagQuality(processTag);
      const value = api.ReadTagDouble(processTag, null);
      element("value").textContent = quality === "good" && value !== null
        ? String(value) : "Unavailable — quality: " + quality;
    }
    stopValues = api.subscribe([processTag, setpointTag], refreshValue);
    refreshValue();
    element("status").textContent = "Connected. Reads remain subject to data quality.";
        element("write").disabled = !(api.supports("tag.write") && await api.permissions.has("tag:write"));
    element("write").onclick = async () => {
      const input = element("setpoint").value.trim();
      const value = Number(input);
      if (!input || !Number.isFinite(value) || value < 0 || value > 100) {
        element("write-result").textContent = "Enter a number between 0 and 100."; return;
      }
      if (!api.ReadTagQualityGood(setpointTag)) {
        element("write-result").textContent = "Setpoint quality is not good."; return;
      }
      if (!confirm("Write setpoint " + value + "?")) return;
      element("write").disabled = true;
      try {
        const result = await api.WriteTagDouble(setpointTag, value, {reason: "Operator setpoint update"});
        const done = result?.accepted === true && result?.status === "done" && result?.outcomeUncertain !== true && result?.operatorVerificationRequired !== true && result?.transportOnly !== true;
        element("write-result").textContent = (done ? "Command completed; verify normal collected state: " : "Verify before another write: ") + JSON.stringify(result ?? null);
      } catch (error) {
        element("write-result").textContent = "Result not confirmed. Check actual value and command records. " + error.message;
      }
      // Keep the button disabled after submission; refresh only after result reconciliation.
    };
        let alarmPending = false;
    async function refreshAlarms() {
      if (alarmPending) return;
      alarmPending = true;
      try {
        const result = await api.alarms.getActive({limit: 100, offset: 0});
        api.renderTable(element("alarm-list"), result.items, {columns: [
          {key:"fullCode",label:"Tag"}, {key:"messageText",label:"Message"},
          {key:"severity",label:"Severity"}, {key:"state",label:"State"}
        ]});
        element("alarm-status").textContent = result.hasMore ? "Showing first 100 alarms; narrow the filter or request the next page." : "Current active alarms: " + result.count;
      } catch (error) { element("alarm-status").textContent = error.message; }
      finally { alarmPending = false; }
    }
    element("alarms").disabled = !api.supports("alarm.read");
    element("alarms").onclick = refreshAlarms;
    stopAlarms = api.supports("alarm.read") ? api.alarms.subscribe(refreshAlarms) : () => {};
    if (api.supports("alarm.read")) await refreshAlarms();
        element("history").disabled = !api.supports("history.read");
    element("history").onclick = async () => {
      element("history").disabled = true;
      try {
        const end = new Date();
        const result = await api.history.query({tags:[processTag],
          start:new Date(end.getTime()-3600000).toISOString(), end:end.toISOString(),
          interval:"1m", aggregate:"avg", points:1000});
        const rows = (result.items || []).flatMap(series => (series.points || []).map(point => ({
          tag:series.tag, time:point.t, value:point.quality === "good" ? point.value : null, quality:point.quality
        })));
        api.renderTable(element("trend"), rows);
        element("trend-status").textContent = rows.length
          ? "Interval: " + result.effectiveInterval + (result.truncated ? " (adjusted to point budget)" : "")
          : "No archived values in this time range.";
      } catch (error) { element("trend-status").textContent = error.message; }
      finally { element("history").disabled = false; }
    };
  })().catch(error => { document.getElementById("status").textContent = error.message; });
  </script>
</body>
</html>

renderTable and bindJsonTable

api.renderTable and global HQVISU_CUSTOM_PAGE.renderTable render arrays, { rows }, or { items } as HTML tables and return { rowCount, columnCount }. bindJsonTable renders once, subscribes to a JSON Tag, and returns an unsubscribe function.

  • target may be a CSS selector or DOM element.
  • options.columns can be an array of strings or { key, label } objects.
  • When columns are omitted, the SDK infers columns from the first 25 rows.
  • options.className sets the table class; the page owns the CSS.
  • Cells are written through textContent, not executed as HTML. A missing target returns { rowCount: 0, columnCount: 0 }.
  • bindJsonTable(target, tagName, options) accepts array JSON, { rows }, or { items }; empty or invalid JSON renders an empty table. It renders the raw snapshot without filtering Tag quality. For a quality-aware table, subscribe, check ReadTagQualityGood in the callback, then call renderTable.
  • Complex sorting, pagination, frozen columns, and Excel export should be implemented by the project page or a table component.
JavaScriptJavaScript
api.renderTable("#quality-table", {
  rows: [
    { tag: "<device-code>.<temperature-tag-code>", value: "72.4", quality: "good" },
    { tag: "<device-code>.<pressure-tag-code>", value: "0.68", quality: "good" }
  ]
}, {
  className: "report-table",
  columns: [
    { key: "tag", label: "Tag" },
    { key: "value", label: "Current value" },
    { key: "quality", label: "Quality" }
  ]
});

const stopTable = api.bindJsonTable("#event-table", "<device-code>.<event-rows-tag-code>", {
  columns: ["time", "type", "message"]
});

Language: follow the system LANG control

Use the system LANG control without adding another selector. Render the current locale, then subscribe to changes. Translate display text; retain response data, raw Tag values, and command status codes.

  • language.current() / getLocale() → string.
  • language.translate(key, fallback?) / t(key, fallback?); root api.translate/api.t are equivalent. Missing keys return fallback or the key if no fallback is given. Maintain business translation keys in system language configuration.
  • language.onChange((locale, translations) => ...) / subscribe(...) → cleanup function; no automatic initial callback.
  • language.setLocale(locale) → string | Promise<string>; affects system language, not component-local state. Page language controls should stay consistent with the system header.
  • Breadcrumb names come from static navigation declarations; PageApi has no dynamic rename method. Do not repeat handshakes to refresh them.
JavaScriptJavaScript
// The system top-bar LANG is the language selector. Do not add a second one.
function renderText() {
  const locale = api.language.current();
  document.documentElement.lang = locale;
  const fallback = locale.startsWith("zh") ? "设备总览" : "Equipment overview";
  document.getElementById("title").textContent = api.t("equipment.overview.title", fallback);
}
renderText(); // onChange does not supply an initial callback.
const stopLanguage = api.language.onChange(renderText);
// Call stopLanguage() in this page/component's existing cleanup handler.

Local development with the Mock Host

The versioned runtime-page-sdk-mock-host-v1.js provides development data without Mingcedar. Create the Mock Host before loading hq-common.js and hq-custom-page.js. It tests rendering, subscription, and error states; it is not a security, permission, audit, write-confirmation, history-store, or PLC simulator.

  • createHQVisuMockHost(options) returns a development Host.
  • options.values, meta, user, alarms, and locale establish initial state.
  • queryHistory and queryAlarmHistory can supply custom query results.
  • host.__mock.setTag(name, value, meta) pushes a Tag change; setAlarms(items) pushes an alarm snapshot.
  • Delivery validation must return to Mingcedar Runtime and retest capability, ordinary-user permission, write confirmation, and real query limits.
  • Use the supplied development files for local Mock work. In production, load the SDK from the URL provided by Runtime; do not deploy the development copy of hq-common.js as a standalone package.
HTMLHTML
<script src="./runtime-page-sdk-mock-host-v1.js"></script>
<script>
window.HQVISU_HOST = createHQVisuMockHost({
  values: { "<device-code>.<process-tag-code>": 72.4 },
  meta: { "<device-code>.<process-tag-code>": { quality: "good" } },
  user: {
    username: "<operator-name>",
    displayName: "Shift Operator",
    permissions: []
  }
});
</script>
<script src="./hq-common.js"></script>
<script src="./hq-custom-page.js"></script>

PPT and custom pages: one public read/write vocabulary

Custom pages use api.ReadTagInteger / api.WriteTagInteger; PPT uses ReadTagInteger / WriteTagInteger. Case, parsing, quality fallbacks, dates, and result handling match. Replace old names when upgrading.

  • Reads: ReadTag, ReadCalculatedTag, ReadTagBoolean/Integer/Double/String/Json/DateTime, ReadTagQuality, ReadTagQualityGood.
  • Writes: WriteTag, WriteTagBoolean/Integer/Double/String/Json/DateTime, WriteTagBit.
  • Reads return the current snapshot synchronously. Await writes and inspect the result. Business wrappers should preserve quality checks, permission checks, and command-result handling.
JavaScriptJavaScript
const api = await HQVISU_CUSTOM_PAGE.ready();
const value = api.ReadTagInteger("<device-code>.<tag-code>", null);
// PPT script reads the same name without the api prefix:
// const value = ReadTagInteger("<device-code>.<tag-code>", null);

Error handling and delivery checks

Use fallback and quality for unavailable reads, and await/try-catch for initialization, writes, history and alarms. Writes may have no return value; undefined is not success. Retain error.code, error.message and an available commandId, never capability secrets.

  • Open the page as administrator and as an ordinary user to verify the permission boundary.
  • Try an undeclared Tag and verify a clear rejection or empty state without a blank page.
  • Simulate bad or stale quality and confirm the page does not display it as a normal value.
  • Verify history range, aggregate meaning, units, decimals, and chart explanations.
  • Verify writes include confirmation, failure messages, and an operation reason when required by the project.
  • Verify outcome_uncertain / pending_verification enters a blocking state and never retries automatically.
  • On component unmount, cancel its Tag, alarm, language and table subscriptions. Call api.destroy() only when the document permanently leaves (pagehide with !event.persisted), not during BFCache suspension or Vue unmount. Do not reuse subscriptions or pending requests after destruction.
  • Open the browser console and confirm there are no SDK initialization failures, 404s, JSON parse errors, or unhandled Promises.
  • Common errors: INVALID_ARGUMENT (input), CAPABILITY_DENIED (page scope/capability), PERMISSION_DENIED (user permission), NOT_FOUND, TIMEOUT, UNAVAILABLE, RUNTIME_WRITE_OUTCOME_UNCERTAIN and RUNTIME_PAGE_DESTROYED. Distinguish page readiness, service availability and Tag quality.
JavaScriptJavaScript
try {
  const history = await api.history.query({
    tags: ["<device-code>.<output-tag-code>"],
    start,
    end,
    interval: "1h",
    aggregate: "sum"
  });
  renderTrend(history.items || []);
} catch (err) {
  renderEmptyState("History query failed: " + err.message);
}

Native HTML module: a complete page

Save as equipment-overview.html and replace the Tag in both declaration and script. The mjs exports only ready and default {ready}; obtain methods such as ReadTagInteger from await ready(). The page is responsive and follows LANG.

View complete source
HTMLHTML
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Mingcedar equipment overview</title>
  <script>window.HQVISU_PAGE_META = { layoutMode: "native" };</script>
  <script type="application/json" id="hqvisu-custom-page">
  {"readableTags":["<device-code>.<process-tag-code>"],"writableTags":[]}
  </script>
</head>
<body>
  <h1 id="title">Equipment overview</h1>
  <output id="value">—</output><p id="status" role="status"></p>
  <script type="module">
    import { ready } from "/runtime/hqVisuDesigner/hq-custom-page.mjs";
    const tag = "<device-code>.<process-tag-code>";
    const element = id => document.getElementById(id);
    let page, stopValue, stopLanguage, disposed = false;
    window.addEventListener("pagehide", event => {
      if (event.persisted) return;
      disposed = true;
      stopValue?.(); stopLanguage?.(); page?.destroy();
    });
    try {
      page = await ready({ tags: [tag] });
      if (!disposed) {
        function render() {
          const zh = page.language.current().startsWith("zh");
          document.documentElement.lang = page.language.current();
          element("title").textContent = zh ? "设备总览" : "Equipment overview";
          const quality = page.ReadTagQuality(tag);
          const value = page.ReadTagDouble(tag, null);
          element("value").textContent = quality === "good" && value !== null ? String(value) : "—";
          element("status").textContent = (zh ? "数据质量:" : "Quality: ") + quality;
        }
        render();
        stopValue = page.subscribe([tag], render);
        stopLanguage = page.language.onChange(render);
      }
    } catch (error) {
      if (!disposed) element("status").textContent = (error.code ? error.code + ": " : "") + error.message;
    }
  </script>
</body>
</html>

Vue integration 1: declare scope in entry HTML

Use this index.html in an existing Vue 3/Vite project. Bundle Vue from project dependencies; no CDN is needed. readableTags and writableTags must match the component prop. Call ready after onMounted in SSR, otherwise it rejects with RUNTIME_PAGE_BROWSER_REQUIRED.

HTMLHTML
<!-- Vite project root: index.html -->
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Mingcedar equipment monitor</title>
  <script>window.HQVISU_PAGE_META = { layoutMode: "native" };</script>
  <script type="application/json" id="hqvisu-custom-page">
  {"readableTags":["<device-code>.<setpoint-tag-code>"],
   "writableTags":["<device-code>.<setpoint-tag-code>"]}
  </script>
</head>
<body><div id="app"></div><script type="module" src="/src/main.js"></script></body>
</html>

Vue integration 2: mount the component

Save as src/main.js. Match the prop, entry declaration and component binding to the approved numeric setpoint Tag. Remove write declarations and controls for a read-only project.

JavaScriptJavaScript
// src/main.js — use the Vue dependency installed in your project.
import { createApp } from "vue";
import EquipmentPanel from "./EquipmentPanel.vue";
createApp(EquipmentPanel, {
  tagName: "<device-code>.<setpoint-tag-code>"
}).mount("#app");

Vue integration 3: read, write once, LANG and unmount

Save as src/EquipmentPanel.vue. On unmount, cancel only component subscriptions; stop setup if unmounted while awaiting. Submission disables the button, but reloads or remounts reset it. Reconcile the previous command and normal collected value before another write; persist and restore pending-verification state if blocking must survive reloads.

View complete source
VueVue
<script setup>
import { onMounted, onUnmounted, ref } from "vue";
const props = defineProps({ tagName: { type: String, required: true } });
const tag = props.tagName; // Remount with a new key when changing this Tag.
const value = ref(null), quality = ref("bad"), locale = ref("en-US");
const target = ref(""), message = ref(""), canWrite = ref(false), submitted = ref(false);
let page, stopValue, stopLanguage, disposed = false, allowed = false;
const text = (zh, en) => locale.value.startsWith("zh") ? zh : en;
function render() {
  value.value = page.ReadTagDouble(tag, null);
  quality.value = page.ReadTagQuality(tag);
  canWrite.value = allowed && quality.value === "good";
}
onMounted(async () => {
  try {
    const sdkUrl = "/runtime/hqVisuDesigner/hq-custom-page.mjs";
    const { ready } = await import(/* @vite-ignore */ sdkUrl);
    if (disposed) return;
    const api = await ready({ tags: [tag] });
    if (disposed) return;
    page = api;
    locale.value = page.language.current();
    stopLanguage = page.language.onChange(next => { if (!disposed) locale.value = next; });
    render();
    stopValue = page.subscribe([tag], render);
    allowed = page.supports("tag.write") && await page.auth.hasPermission("tag:write") === true;
    if (!disposed) render();
  } catch (error) {
    if (!disposed) message.value = error.message;
  }
});
onUnmounted(() => {
  disposed = true;
  stopValue?.(); stopLanguage?.();
  // Do not destroy the shared document SDK on component unmount.
});
async function write() {
  if (submitted.value || !canWrite.value) return;
  const input = String(target.value).trim(), next = Number(input);
  // Replace 0–100 with the approved engineering range for this Tag.
  if (!input || !Number.isFinite(next) || next < 0 || next > 100) {
    message.value = text("请输入 0–100 的数值", "Enter a number from 0 to 100"); return;
  }
  if (!page.ReadTagQualityGood(tag)) return;
  if (!window.confirm(tag + ": " + next)) return;
  submitted.value = true;
  try {
    const result = await page.WriteTagDouble(tag, next, { reason: "Operator setpoint update" });
    if (!disposed) message.value = text("命令回执(需核对正常采集值):", "Command receipt (verify normal collected value): ")
      + JSON.stringify(result ?? null);
  } catch (error) {
    if (!disposed) message.value = text("结果未确认,核对后再操作:", "Result unconfirmed; reconcile before another operation: ") + error.message;
  }
  // Keep submission blocked until the operator reconciles command and collected state.
}
</script>
<template>
  <section>
    <h1>{{ text("设备设定值", "Equipment setpoint") }}</h1>
    <p>{{ tag }}: {{ quality === "good" ? value : "—" }} · {{ quality }}</p>
    <label>{{ text("目标值(0–100)", "Target (0–100)") }} <input v-model="target" type="number" step="0.1"></label>
    <button :disabled="!canWrite || submitted" @click="write">{{ text("写入一次", "Write once") }}</button>
    <pre role="status">{{ message }}</pre>
  </section>
</template>

Vue integration 4: build and publish

Keep the existing Vue plugin and set Vite base: "./" so output can live under any assets subfolder. The SDK uses an absolute /runtime/ service URL, independent of the relative application asset base.

  1. Run the project npm run build; confirm dist/index.html retains the hqvisu-custom-page static JSON declaration.
  2. Upload dist contents with the original hierarchy into one Screen Files directory, including index.html and built assets/*.js/CSS. Uploading only .vue sources or only index.html is insufficient.
  3. Point the runtime menu to that directory index.html. Check SDK readiness, GOOD values, LANG changes, remounting and actual operation results.
JavaScriptJavaScript
// In the existing vite.config.js:
export default defineConfig({ plugins: [vue()], base: "./" });

Dates and large integers: preserve meaning

DATE_AND_TIME, DTL and LDT metadata denotes a timezone-free PLC calendar. ReadTagDateTime returns a Date carrying those calendar fields in UTC, not an actual UTC instant; read fields with getUTC*. WriteTagDateTime(Date) writes the same UTC fields without Z. Other instant-style Date values retain ISO instant semantics.

  • A PLC calendar value 08:30 must remain 08:30 in Shanghai and other zones; do not interpret that calendar container with local getters or toLocaleString.
  • Date preserves milliseconds only; retain original text for DTL/LDT sub-millisecond precision. Date validity, year and encoding ranges remain device-type checks.
  • For integers outside JavaScript safe range, preserve text through ReadTag/ReadTagString and pass decimal text or BigInt to WriteTagInteger; do not first convert through Number.
JavaScriptJavaScript
const date = api.ReadTagDateTime("<device-code>.<datetime-tag-code>", null);
if (date) {
  const fields = [date.getUTCFullYear(), date.getUTCMonth() + 1, date.getUTCDate(),
    date.getUTCHours(), date.getUTCMinutes(), date.getUTCSeconds()];
  document.getElementById("calendar").textContent = fields.join(" / ");
}

Click the image again or press Esc to close