1 Commits

Author SHA1 Message Date
58610c1e1a exchange work 2026-09-15 13:06:45 -05:00
279 changed files with 35437 additions and 142 deletions

85
.gitignore vendored
View File

@@ -1,80 +1,19 @@
# ---------------------------------------------------------------------------
# Ignition 8.3 gateway-as-files. commissioning.json, config/ and projects/ are
# bind-mounted from ignition/gateway/. Commit the authored configuration and
# projects; ignore what the gateway maintains for itself or that is specific to
# one instance.
#
# The first block is Inductive Automation's recommended .gitignore, verbatim:
# https://docs.inductiveautomation.com/docs/8.3/tutorials/version-control-guide
# Note that **/request, **/response, **/var and **/logs match a file or folder
# of that name ANYWHERE, including inside a project. If a project resource is
# ever named that way, negate it below with a leading "!".
# ---------------------------------------------------------------------------
# Database files
**/db/
**/metricsdb/
**/autobackup/
**/db_backup_sqlite.idb
**/valueStore.idb
# Cache and temporary files
**/jar-cache/
**/request
**/response
*.tmp
*.bak
**/var
# Log files
*.log
**/logs
# Certificates and security
**/certificates/
**/keystore/
# Runtime configuration (environment-specific)
**/config/local
**/config/resources/local
# Gateway-specific runtime files
**/.container-init.conf
# Backup and converted files
**/conversion-report.txt
**.digest.json
# Project conversion artifacts
**/projects/conversion-report.txt
**/migration-log-*.md
**/.resources/
# Alarm history files
**/.alarms_*
# ---------------------------------------------------------------------------
# Repo-specific additions
# ---------------------------------------------------------------------------
# Gateway API token (mint with: python3 tools/provision.py mint-token)
.env
# MariaDB password read by the 'local' file secret provider (written by provision.py)
ignition/gateway/config/secrets/
# Gateway probe output (never author/commit)
ignition/gateway/projects/.probe/
# Designer save artifacts
ignition/gateway/projects/**/thumbnail.png
# Fresh-reset rollback copy (README "Reset" workflow)
# Fresh-reset rollback copy
ignition/gateway.bak/
# Harvested component schemas (regenerate with tools/schema_harvest.sh)
.schemas/
# Python tooling
# Gateway-maintained content-addressed caches + probe output (never author/commit)
ignition/gateway/**/.resources/
ignition/gateway/projects/.probe/
__pycache__/
.pytest_cache/
# Gateway/Designer runtime churn (ONBOARDING §6) — never commit
ignition/gateway/config/ignition/tags/valueStore.idb
ignition/gateway/config/resources/local/ignition/local-system-properties/
ignition/gateway/config/resources/migration-log-*.md
# Designer save artifacts
ignition/gateway/projects/**/thumbnail.png
# Harvested component schemas (regenerate with tools/schema_harvest.sh)
.schemas/

Binary file not shown.

BIN
dist/AlarmDashboard.zip vendored Normal file

Binary file not shown.

129
dist/INSTALL.md vendored Normal file
View File

@@ -0,0 +1,129 @@
# Alarm Dashboard — install guide
A single Perspective dashboard that scores an alarm system against ISA-18.2 metrics
(rate, flood, chattering, standing, priority mix) and shows what to fix first.
Ships as one file: `AlarmDashboard.zip` (a standard Ignition project export).
## Requirements
- Ignition **8.3** or later, with the **Perspective** module.
- An **alarm journal** profile that stores events (Config → Alarming → Journal).
Any name, any datasource, any table prefix.
- Nothing else. No third-party modules, no extra database, no tags, no scripts to
install on the gateway, no images to copy.
## Install
1. Gateway web UI → **Config → Projects → Import Project** (or Designer →
**File → Import Project**), choose `AlarmDashboard.zip`.
2. Name the project anything you like — the project name is not referenced
internally, so `AlarmDashboard`, `PlantAlarms`, or a site code all work.
3. Open the dashboard: `http://<gateway>:8088/data/perspective/client/<ProjectName>`
That is the whole install on a gateway with one alarm journal.
## If the gateway has more than one alarm journal
Ignition rejects a journal query that does not name a profile when several exist
(`IllegalArgumentException: No alarm journal profile specified`), so the dashboard
must be told which journal to read. The dashboard will show this in its header
error chip, naming the fix.
Designer → **Project Library → AlarmDashboard → alarms**, set the constant at the
top of the script to the profile name from Config → Alarming → Journal:
```python
JOURNAL_NAME = "PlantJournal" # "" = single-journal gateway, let the gateway resolve it
```
Save the project. This is the **only** install-time setting in the whole project.
## Area grouping — match the site's tag structure
Every per-area metric (Areas tab, the area filter, a flood's "top area", per-area bad
actors) groups on **one folder of the alarm's tag path**. Two settings at the top of
Project Library → `AlarmDashboard``calc` control it.
`AREA_SEGMENT` picks which folder, counting from the top of the tag path:
```
prov:FW:/tag:PS/EAS/PMP1/Alarms/Change-of-State:/alm:active
│ │
│ └─ AREA_SEGMENT = 1 -> "EAS" (the individual site)
└──── AREA_SEGMENT = 0 -> "PS" (the site type) <- default
```
`AREA_NAMES` maps the raw folder to the name shown in the UI. **Several codes mapping
to the same name are summed into one area** — that is how the pump station variants
roll up:
```python
AREA_SEGMENT = 0
AREA_NAMES = {
"EST": "Elevated Storage Tank",
"PS": "Pump Station",
"PSR": "Pump Station", # rolls up
"PSR2": "Pump Station", # rolls up
"PSR_STP": "Pump Station", # rolls up
"LS": "Sewage Lift Station",
"VS": "Valve Site",
"FWC": "Customer Cities - Fresh Water",
"WWC": "Customer Cities - Waste Water",
}
```
Folders not listed appear exactly as they are in the tag path, so a new site type shows
up as its raw code rather than vanishing — add it to `AREA_NAMES` when you see it. A tag
path with fewer folders than `AREA_SEGMENT` falls back to its first folder.
### What the Source / Alarm columns show
The alarm identity in every Source column (Bad Actors, Pareto) and the Journal's Alarm
column is **the rest of the path after the area folder**, so two sites running the same
alarm are distinguishable:
```
prov:FW:/tag:PS/EAS/PMP1/Alarms/Change-of-State:/alm:active
└─ area: Pump Station
└──────────────────────────────────── Source: EAS/PMP1/Alarms/Change-of-State/active
```
`AREA_SEGMENT` drives this too — the label always starts one folder after the area. If an
alarm has a **Display Path** configured, that is used verbatim instead, which is the way
to override any of this per alarm. The raw journal source string is preserved untouched
behind the scenes as the drill-down key, so click-through to source detail is unaffected.
## Settings worth checking on the customer's journal
- **Minimum priority** should be `Diagnostic` (store everything). If the journal
drops low-priority events, the ISA 80/15/5 priority-mix score reads as badly
skewed toward high-priority alarms through no fault of the site.
- **Journal retention**: the dashboard's range presets go out to 7 days, so keep at
least that much history for the trend and comparison ("vs previous period") views.
## What the dashboard reads, and what it never touches
Reads, through the alarm API only:
- `system.alarm.queryJournal` — all history, KPIs, trends, bad actors, journal table
- `system.alarm.queryStatus` — currently active / unacked / standing alarms
- `system.alarm.getShelvedPaths` — shelved count
It issues **no SQL**, holds **no datasource reference**, reads **no tags**, and needs
no `system.db.*` access — so it cannot be broken by a schema, table-prefix, or
tag-provider difference between sites. Plant areas are derived from the alarm's
source path, so they follow whatever tag folder structure the site already uses.
## Verified before release
- Imports via the gateway API onto a project name it has never used, starts with no
errors, and serves its Perspective client (HTTP 200).
- Strict project lint: 0 failures, 0 warnings (22 views, 32 style classes).
- Data layer verified live against a real journal: bundle computed with no error,
full KPI/health output, all plant areas resolved.
- Multi-journal behavior tested on a gateway with two journal profiles: unnamed
query fails as documented above, and setting `JOURNAL_NAME` restores full data.
- Export scanned for environment coupling: no company branding, no datasource names,
no tag paths, no absolute paths, no host addresses, no gateway image references.

File diff suppressed because it is too large Load Diff

Binary file not shown.

View File

@@ -0,0 +1,202 @@
# Publishing a resource to the Ignition Exchange
How to take a conforming Ignition project and get it onto the Exchange: what file
format to upload, how to produce and verify it, and what to put in every field of
the upload form.
Companion to [`exchange_lint.py`](exchange_lint.py), which tests whether a project
follows the conventions, and to the
[Exchange Resources Style Guide](Exchange+Resources+Style+Guide.md), which is the
source for everything here.
---
## 1. What you upload
A **project export `.zip`** — the file Ignition produces from Designer's
File → Export, not a gateway backup.
| File | Upload as | Notes |
|---|---|---|
| `<resource>.zip` | Package file | The project export. The main artifact. |
| `README.md` (or PDF) | Package file | Separate upload — see the warning below. |
| Tag export `.json` | Package file | Only if the resource needs tags/UDTs. |
| Database dump `.sql` | Package file | Only if it needs a schema or seed data. |
| Screenshots `.png` | Images & Screenshots | Optional, strongly encouraged. |
> **Never upload a gateway backup (`.gwbk`).** It contains your entire gateway —
> datasource credentials, journal profiles, tags, every other project. It is the
> wrong artifact and it leaks configuration into a public resource.
The Exchange accepts multiple package files, up to 10 MB each.
### The README will probably not be inside the zip
Ignition's project export serialises **recognised project resources** — each is a
directory containing a `resource.json`. A loose `README.md` at the project root
is not a project resource, so the export is expected to drop it.
This is reasoning from how the export works, not a verified observation. **Unzip
your export and check** before uploading. Either way the guide tells you to
upload documentation separately:
> **Note:** If you already have detailed documentation for your resource please
> note it here and include the documentation file in the Package File upload.
Note also that `exchange_lint.py`'s `E-UP-DOCS` rule checks the working
directory, not the shipped package. A passing lint does not prove the zip
carries your docs.
---
## 2. Before you package
### Run the conformance checker
```bash
python3 exchangeResources/exchange_lint.py <project-dir>
```
Zero failures is the bar. Warnings are naming conventions the guide calls
"encouraged, not required" — review them and decide, rather than silencing them
by contorting the project. Document any you deliberately keep.
### Work the guide's own checklist
The guide names four pre-upload checks. Three are often N/A for a
journal- or tag-only resource; the first never is:
1. **Check the project export.** Import the exported zip into a brand-new
project and confirm it works. Then import it into a project that *already has
content* — that is the case the namespacing rules exist for, and the only way
to prove nothing collides.
2. **Include the tags.** If the resource needs tags or UDTs, export them and
verify the export covers everything the project binds to.
3. **Don't forget database backups.** If it expects tables or seed data, include
the dump and name the vendor (MySQL / MSSQL / PostgreSQL) in the description
*and* in the install instructions.
4. **Take notes while you test.** Those notes become the Custom Installation
Instructions field. Readers range from beginner to intermediate.
### Strip anything internal
Search the project for company names, project codenames, internal hostnames,
ticket numbers and placeholder text — in **user-visible strings**, not just
comments. The linter does not check content, only names and structure.
```bash
grep -rniE "yourcompany|internal|localhost|todo|fixme|demo only" <project-dir>
```
Check rendered label text too; a `props.text` value is easy to miss in JSON.
---
## 3. Produce the export
**Designer** — File → Export, select the project.
**Gateway web UI** — Config → Projects → *project* → Export.
Both produce the same zip. If your repo stores projects as unpacked files
(gateway-as-files), there is no zip in the repo — you must generate one.
---
## 4. Verify the export
```bash
unzip -l <resource>.zip # inventory
unzip -o <resource>.zip -d /tmp/verify-export # extract
```
Check:
- **File count** matches the project on disk (minus anything the export legitimately drops).
- **Every view, style class and script module** you expect is present.
- **`README.md` presence** — confirm, do not assume.
- **No stray files**: `.resources/` (gateway-owned cache), `__pycache__`, editor backups.
- **No credentials** anywhere in the archive.
Then do the real test: import it into a populated project and click through.
---
## 5. Fill in the upload form
Fields in the order the Exchange presents them.
### Overview
| Field | What to put |
|---|---|
| **Visibility** | `Public` unless it is genuinely private. |
| **Title** | Short, Title Case, says what it does. Usually matches the project title. |
| **Tagline** | One sentence. Must **not** repeat the title. |
| **Description** | What it does, how it is meant to be used, who benefits, relevant industries. |
| **Resource Type** | Closest match from the dropdown, plus a skill level reflecting install/use complexity. |
| **Background Image** | Optional colour/design for the resource card. |
| **Category** | **Up to 3.** Chosen from: Alarming, Analytics, Connectivity, Dashboard, Diagnostics, Edge Computing, Enterprise, HMI, IIOT/MQTT, MES, Maintenance, Mapping/GIS, Mobile, Monitoring, Reporting, SCADA, Scripting, Security, Simulation, Trending/Charting, Utility. |
| **Contact the Developer** | Optional. Your address is never shown; you receive the question plus the asker's address. |
| **Tags** | **Up to 10** search keywords. |
| **Images & Screenshots** | Optional but highly encouraged — shows what the title and description cannot. |
### Package
| Field | What to put |
|---|---|
| **Version** | `1.0.0` for a first release; later versions auto-increment. **Do not** put the version in the project name. |
| **Ignition Platform** | The **minimum** version required. The guide: *"If unsure, select the Ignition platform version in which the package was developed with."* Do not claim a floor you have not tested. |
| **Release Tagline** | Short summary of this version's changes. Required even for the first release. |
| **Release Notes** | Fuller explanation of what changed and how it affects users. |
| **Required Modules** | Any Inductive Automation / Cirrus Link / Sepasoft modules needed. |
| **Maker Edition** | Tick if it runs on Maker. Unavailable if you require modules Maker does not support. |
| **Other Requirements** | External or custom prerequisites, one per line. Blank if none. |
| **Package Files** | The zip, plus docs / tags / SQL as applicable. |
| **Custom Installation Instructions** | Step-by-step setup. Assume a beginner. If you shipped separate documentation, say so here and include the file. |
The Exchange **auto-generates a readme** from your Description and Custom
Installation Instructions, so write both properly.
### Verifying your minimum-version claim
Spin up a throwaway gateway at the version you intend to declare and load the
project into it. Use a **copy** of the project files — an older gateway rewrites
`resource.json` metadata and leaves root-owned `.resources/` artifacts behind.
A clean load proves resources parse. It does **not** prove screens render: a
component property added in a later version fails at render time, not load time.
Open the client and look.
---
## 6. Worked example — Alarm Analysis Dashboard
| Field | Value |
|---|---|
| Title | Alarm Analysis Dashboard |
| Tagline | ISA-18.2 alarm performance metrics straight from the alarm journal |
| Resource Type | Perspective View — Intermediate |
| Category | Alarming, Analytics, Dashboard |
| Tags | alarms, ISA-18.2, alarm journal, MTTA, MTTR, bad actors, alarm rate, KPI, dashboard, perspective |
| Version | 1.0.0 |
| Ignition Platform | 8.1.20 |
| Required Modules | Perspective |
| Maker Edition | Compatible |
| Other Requirements | An alarm journal profile with data |
| Package Files | `alarm-analysis.zip`, `README.md` |
Needs no tag export and no database dump — it reads the journal through
`system.alarm.queryJournal` / `queryStatus` with no journal name, datasource or
tag dependency. That portability is what the guide's structure rules produce.
---
## 7. After publishing
- **Updates** are new package versions on the same resource, not new resources.
Keep the namespace stable so users' imports upgrade in place.
- **Never rename the namespace folders** between versions — that turns an upgrade
into a duplicate set of resources in every user's project.
- Re-run `exchange_lint.py` before every release.
- Non-English resources: the guide encourages shipping a language dropdown with
English alongside, to widen access.

156
exchangeResources/README.md Normal file
View File

@@ -0,0 +1,156 @@
# Ignition Exchange resources
Getting an Ignition project ready to publish on the Ignition Exchange means
following Inductive Automation's naming and structure conventions — mainly so
that when someone imports your resource into a project they already have, nothing
of theirs gets overwritten.
These files help you do that.
| File | What it's for |
|---|---|
| **[`exchange_lint.py`](exchange_lint.py)** | Check your project against the conventions. Start here. |
| **[`PUBLISHING.md`](PUBLISHING.md)** | Package and upload it, once it passes. |
| [`Exchange+Resources+Style+Guide.md`](Exchange+Resources+Style+Guide.md) | The official guide, v1.1.0. Every rule traces back to it. |
**The path: check your project → fix what it reports → follow `PUBLISHING.md`.**
---
## Checking your project
```bash
python3 exchange_lint.py /path/to/YourProject
```
Nothing to install — Python 3.8+ and the standard library.
Point it at any of these:
- your project folder
- a folder containing several projects (a gateway's `projects/` directory, an
unpacked gateway backup, your repo)
- a project export `.zip`
- nothing, to scan the current directory
There's no config file. It works out your resource name from the namespace your
project already uses, and picks up tag exports and `.sql` dumps sitting alongside.
---
## Reading the report
```
Ignition Exchange Style Guide v1.1.0 — conformance report
project: my-dashboard resource: MyDashboard
Resource Structure
FAIL E-NS-ROOT script-python/myutils: project library package
'myutils' is not under exchange/<resource>/; importing
would land scripts in the host project's namespace
Perspective
warn E-COMP-NAME views/Exchange/MyDashboard/Home: component 'headerRow'
should be PascalCase
summary: 1 failure(s), 12 warning(s); 21 of 38 rules exercised
```
Each finding gives you the file, the problem, and why it matters.
### FAIL — fix these before publishing
Either the guide says *must*, or the rule protects your resource from damaging
someone else's project on import: resource namespacing, session properties,
Vision client tags, and the project-name character set.
### warn — your call
These are naming conventions. The guide is explicit that they're *"encouraged,
however these conventions are not required"*, and that consistency matters more
than which convention you choose. Read them, decide, move on. Add `--strict` if
you'd rather treat them as failures.
### "21 of 38 rules exercised"
How many rules actually applied to your project. If you have no database, no tags
and no named queries, those rules never ran — so a pass on a small project isn't
mistaken for a pass on everything.
**Exit codes:** `0` clean · `1` findings · `2` bad invocation.
---
## What gets checked
38 rules. Run `python3 exchange_lint.py --list-rules` for the full list with
descriptions.
| Area | What it looks at |
|---|---|
| **Resource structure** | Everything namespaced under `Exchange/<Resource>/` or `exchange/<resource>/` — the guide's one hard *must*, and what keeps your import from overwriting the host project |
| **Projects** | URL-friendly project name, human-friendly title, no version suffix |
| **Perspective** | Naming of views, components, properties, pages and style classes; where style classes live; message-handler namespacing |
| **Variable storage** | No session custom properties, no Vision client tags — both break a clean import |
| **Code** | Tab indentation, camelCase functions and variables, PascalCase classes, docstrings, comment style, project-library layout |
| **Loggers** | `exchange.resourceName.LoggerName` hierarchy |
| **Database** | `ex_` table prefix, snake_case, `id` primary key, `<table>_id` foreign keys |
| **Tags · Vision · Named queries · WebDev · Translations** | Naming conventions for each |
| **Upload readiness** | Description filled in, installation documentation present |
## What it can't check for you
A clean report doesn't mean ready to publish. Still on you:
- **What's written on screen.** It reads names and structure, never label text. A
visible string like *"Acme Internal — do not distribute"* passes silently.
Search your user-visible strings before you publish.
- **That references still resolve.** It doesn't confirm an embedded view path,
style class or script call points at something real. If you've just renamed
things to fit the conventions, verify the project still works.
- **That it runs.** Nothing is executed or rendered. Load it on a gateway and
open the screens.
- **Your export.** The documentation rule checks your working folder, not the
zip you upload. See [`PUBLISHING.md`](PUBLISHING.md).
---
## Options
| Flag | Effect |
|---|---|
| `--resource NAME` | Set your resource name, if the inferred one is wrong |
| `--strict` | Treat every warning as a failure |
| `--ignore E-PY-VAR,E-COMP-NAME` | Suppress specific rules |
| `-v` | Show every occurrence instead of collapsing per rule |
| `--max-per-rule N` | Change the collapse limit (default 8) |
| `--json` | Machine-readable output, for CI |
| `--tags` / `--sql` | Point at tag exports or SQL dumps explicitly |
| `--list-rules` | Print the full rule table and exit |
---
## Findings that need context
Three rules produce messages that can be confusing on first read.
**`E-NS-ROOT` — why capital `Exchange/` sometimes and lowercase `exchange/` other
times?** That's the guide's convention, not a typo. Views, windows, templates,
named queries, reports, SFCs, transaction groups, alarm pipelines and tags go
under `Exchange/`. Style classes, the project library, WebDev sources and images
go under `exchange/`. Using the wrong root is a `FAIL`; using the right root with
the wrong capitalisation is just a warning (`E-NS-CASE`).
**`E-STYLE-HIER` may flag a class you're sharing deliberately.** It decides where
a style class belongs by finding which views reference it — but it only sees
class names written as plain text. If you build one in a binding, like
`'exchange/my-resource/priority/' + level.lower()`, it can't see that usage and
may report a shared class as used by only one view. If that's your situation, the
warning is safe to ignore.
**`E-PAGE-ROOT` isn't in the guide.** It's an added caution: mapping a page to
`/` means your resource takes over the root URL of whatever project it's imported
into. Worth knowing before you decide — the trade-off is that a resource with no
`/` page can't be launched from the gateway's Perspective project list, only from
its full page URL.

View File

@@ -0,0 +1,94 @@
# Installing these skills into the `ignition` plugin
Three Claude Code skills that package the Exchange tooling in this folder so
other people get it automatically, without knowing it exists.
They are staged here rather than written directly into `AI-Tools` because that
repo was read-only for this work. Nothing below has been applied there yet.
## Where they go
Into the **existing `ignition` plugin**, not a new one. That follows the
marketplace's own rule:
> Plugins are split by **domain**, not by repo it came from. […] A fourth plugin
> with one skill in it is worse than putting that skill somewhere slightly
> imperfect: it's another thing to install and another manifest to maintain.
Exchange publishing is Ignition-domain work, and `ignition` already exists.
```bash
cp -r ignition-exchange-conformance \
ignition-exchange-convert \
ignition-exchange-publish \
~/git/AI-Tools/plugins/ignition/skills/
```
Resulting layout:
```
plugins/ignition/skills/
ignition-gateway/
ignition-trial-reset/
ignition-exchange-conformance/ SKILL.md exchange_lint.py test_exchange_lint.py
ignition-exchange-convert/ SKILL.md verify_refs.py parity.py
ignition-exchange-publish/ SKILL.md
```
## Then: bump the plugin version
`plugins/ignition/.claude-plugin/plugin.json``0.1.0``0.2.0`, and widen the
description, which currently only mentions the gateway API and trial reset:
```json
{
"name": "ignition",
"description": "Ignition 8.3 gateway skills. HTTP API authentication and endpoint discovery, trial-expiry detection and reset on unlicensed gateways, and Ignition Exchange publishing — conformance checking, converting a project into Exchange shape, and packaging for upload.",
"author": { "name": "Brendan Peck", "email": "b.peck@prime-controls.com" },
"version": "0.2.0"
}
```
`.claude-plugin/marketplace.json` carries a second copy of that description —
update it to match, or the marketplace listing will understate the plugin.
## Then: the README rows
Under `### ignition`, the skill table becomes:
```markdown
| Skill | Loads when you're... |
| --- | --- |
| `ignition-gateway` | calling an Ignition 8.3 gateway's HTTP API (`/data/api/v1/...`) |
| `ignition-trial-reset` | debugging bad-quality tags, or resetting a trial gateway |
| `ignition-exchange-conformance` | checking whether a project is ready for the Ignition Exchange |
| `ignition-exchange-convert` | turning an existing project into a publishable Exchange resource |
| `ignition-exchange-publish` | packaging and uploading a resource to the Exchange |
```
Suggested paragraph to follow it:
> The three `ignition-exchange-*` skills cover the publishing pipeline end to
> end. `-conformance` runs a 38-rule checker against the Exchange Resources Style
> Guide and, just as usefully, says what it cannot check. `-convert` carries the
> migration order and two scripts that catch what a conformance checker cannot
> see: dangling references after a mass rename, and features silently dropped.
> `-publish` is the upload form, field by field.
## Verify after copying
```bash
cd ~/git/AI-Tools
python3 plugins/ignition/skills/ignition-exchange-conformance/test_exchange_lint.py
python3 -c "import json;[json.load(open(p)) for p in ['.claude-plugin/marketplace.json','plugins/ignition/.claude-plugin/plugin.json']];print('manifests parse')"
/plugin marketplace add ~/git/AI-Tools # local path works while developing
```
## Keeping the copies in sync
`exchange_lint.py` and `test_exchange_lint.py` now exist twice: in
`exchangeResources/` (the working copy, used by this repo) and inside the skill
(the distributable). `verify_refs.py` and `parity.py` live **only** in the skill.
If you change a rule, update both — or make `exchangeResources/` a symlink to the
skill copy and keep one source of truth.

View File

@@ -0,0 +1,149 @@
---
name: ignition-exchange-conformance
description: Check whether an Ignition project follows Inductive Automation's Exchange Resources Style Guide before publishing it to the Ignition Exchange — resource namespacing, Perspective/Vision/Python naming, session-property and client-tag prohibitions, database and logger conventions. Use when asked whether a project is Exchange-ready, when reviewing a project intended for the Exchange, or before packaging one for upload. Bundles a stdlib-only checker (exchange_lint.py) with 38 rules traceable to guide sections. Not for converting a project into that shape (use ignition-exchange-convert) or for packaging and upload (use ignition-exchange-publish).
---
# Ignition Exchange conformance
Tests an Ignition project against the **Exchange Resources Style Guide v1.1.0**.
Run `exchange_lint.py` (next to this file) rather than checking by eye — the rules
are numerous, mechanical, and easy to half-apply.
```bash
python3 skills/ignition-exchange-conformance/exchange_lint.py /path/to/Project
```
Stdlib only, Python 3.8+. Accepts a project folder, a folder containing several
projects (a gateway's `projects/` directory, an unpacked backup, a repo), or a
project export `.zip`. Infers the resource name from the namespace the project
already uses — no configuration.
Exit codes: `0` clean · `1` findings · `2` bad invocation.
## The one rule that is actually mandatory
The guide states exactly one `must`:
> Views, Styles, Scripts, etc. **must** all be contained within an appropriately
> named folder. This allows easy imports into existing projects, where those
> project resources won't overlap, interfere, or overwrite existing parts of a
> user's project.
Everything else — every naming convention — the guide explicitly calls optional:
> We encourage you to follow the naming conventions outlined here, however
> **these conventions are not required**.
So `exchange_lint.py` reports **FAIL** only for namespacing, the storage
prohibitions, and the project-name character set. Naming is **warn**. Don't
report warnings as blockers, and don't contort a project to silence them —
a consistent house style is defensible and the guide says so.
## Namespace roots — two spellings, both correct
Per the guide's Project Browser screenshots:
| Under `Exchange/<ResourceName>/` | Under `exchange/<resource-name>/` |
|---|---|
| Views, Windows, Templates | Style classes |
| Named queries, Reports | Project library (scripts) |
| SFCs, Transaction groups | WebDev sources |
| Alarm pipelines, Tags | Images |
Uppercase roots take Title Case or PascalCase names; lowercase roots take
kebab-case — **except the project library**, which is code-facing and uses
lowercase or camelCase (`exchange.resourceName.scriptName`), matching the
logger convention `exchange.resourceName.LoggerName`.
## The storage prohibitions
Both are FAILs because both break a clean import, and both are easy to miss:
- **Perspective session custom properties.** Importing merges them into the host
project's session props. Use view `params`/`custom` plus, for genuinely
non-reactive cross-session state, `system.util.getGlobals()['exchange']['resourceName']`.
- **Vision client tags.** Same reasoning.
## What the checker cannot tell you
A clean report is not "ready to publish". Four gaps, all of which have bitten:
1. **Content.** It reads names and structure, never label text. A visible string
like `"Acme Internal — do not distribute"` passes silently. Always sweep
user-visible strings separately:
```bash
python3 - <<'PY'
import json, glob, re
bad = re.compile(r"internal|confidential|do not|contest|demo only|todo|fixme", re.I)
for f in glob.glob("<project>/com.inductiveautomation.perspective/views/**/view.json", recursive=True):
v = json.load(open(f))
def walk(n):
yield n
for c in (n.get("children") or []): yield from walk(c)
for n in walk(v.get("root") or {}):
t = (n.get("props") or {}).get("text")
if isinstance(t, str) and bad.search(t): print(f, repr(t))
PY
```
2. **Referential integrity.** It does not check that embedded view paths, style
classes or script calls resolve. See [`ignition-exchange-convert`](../ignition-exchange-convert/SKILL.md).
3. **Runtime.** Nothing is executed or rendered. Load it on a gateway.
4. **The shipped package.** `E-UP-DOCS` checks the working directory, not the
export zip. See [`ignition-exchange-publish`](../ignition-exchange-publish/SKILL.md).
## Findings that need interpretation
**`E-STYLE-HIER` false positives.** It decides where a style class belongs by
finding which views reference it, reading only literal `props.style.classes`
strings. A class name built in a binding —
`'exchange/my-resource/priority/' + level.lower()` — is invisible, so a shared
class gets reported as single-use. Verify before acting; do not duplicate style
classes to silence it.
**`E-PAGE-ROOT` is not in the guide.** It is an added caution: a page mapped to
`/` takes over the root URL of any project the resource is imported into. Real
trade-off — a resource with no `/` page cannot be launched from the gateway's
Perspective project list, only from its full page URL
(`/data/perspective/client/<project>/<page>`). Flag the trade-off; let the user
decide.
**`E-PY-VAR` volume.** A project written in idiomatic snake_case produces
hundreds of warnings, since the guide wants camelCase for variables and
functions (deliberately diverging from PEP-8; `UPPER_SNAKE` constants are
exempt). Offer `--ignore E-PY-VAR,E-PY-FUNC` rather than a mass rename, unless
the user wants full conformance.
## Useful flags
| Flag | Effect |
|---|---|
| `--strict` | Warnings become failures |
| `--ignore E-PY-VAR,E-COMP-NAME` | Suppress rules |
| `-v` | Every occurrence (default collapses at 8 per rule) |
| `--json` | Machine-readable, for CI |
| `--list-rules` | All 38 rules with descriptions |
## Reporting results
The summary reads `N of 38 rules exercised`. Quote it. A project with no
database, tags or named queries legitimately leaves those rules unexercised, and
a narrow pass should never be presented as a broad one.
## Self-test
`test_exchange_lint.py` builds a conforming and a non-conforming project in a
temp dir, asserts the checker is silent on the first and fires the expected rule
id on the second, and asserts every declared rule is reachable in the source.
Run it after changing any rule.
```bash
python3 skills/ignition-exchange-conformance/test_exchange_lint.py
```
## Provenance
Rules derived from Exchange Resources Style Guide v1.1.0. Checker exercised
against four real Perspective projects and validated on Ignition **8.1.20** and
**8.3.7** gateways (2026-09-15). Where a rule is an inference rather than guide
text it says so in its description — currently only `E-PAGE-ROOT`.

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,317 @@
#!/usr/bin/env python3
"""Self-test for exchange_lint.py.
Builds two throwaway Ignition projects in a temp directory -- one that follows
the Exchange Resources Style Guide and one that breaks a known rule per check --
then asserts the checker stays silent on the first and fires the expected rule
id on the second. This is what keeps the rules honest: a rule that cannot fail
and a rule that always fires both show up here.
Run directly (python3 test_exchange_lint.py) or under pytest.
"""
import json
import os
import shutil
import sys
import tempfile
import unittest
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import exchange_lint as el # noqa: E402
RESOURCE = "SampleResource"
PERSP = el.PERSPECTIVE
# ------------------------------------------------------------------ fixtures
def write(path, text):
os.makedirs(os.path.dirname(path), exist_ok=True)
with open(path, "w") as f:
f.write(text)
def write_json(path, obj):
write(path, json.dumps(obj, indent=2))
RESOURCE_JSON = {"scope": "G", "version": 1, "restricted": False,
"overridable": True, "files": ["view.json"], "attributes": {}}
CLEAN_CODE = '''"""Sample library module."""
def formatCount(rawCount):
"""Return rawCount rendered for display."""
# pad small values so the column stays aligned
displayValue = "%d" % rawCount
return displayValue
'''
DIRTY_CODE = '''def format_count(raw_count):
#no space after hash
display_value = "%d" % raw_count
return display_value
'''
def build_project(root, name, clean=True):
"""Create a minimal Perspective project; clean=False breaks one rule per check."""
proj = os.path.join(root, name)
ns_upper = "Exchange/%s" % RESOURCE if clean else "MyCompany"
ns_lower = "exchange/sample-resource" if clean else "MyCompany"
lib_pkg = "exchange/sampleresource" if clean else "MyCompany"
write_json(os.path.join(proj, "project.json"), {
"title": "Sample Resource" if clean else "",
"description": "A sample Exchange resource." if clean else "",
"enabled": True, "inheritable": False, "parent": "",
})
view_dir = os.path.join(proj, PERSP, "views", *ns_upper.split("/"), "Dashboard")
write_json(os.path.join(view_dir, "resource.json"), RESOURCE_JSON)
write_json(os.path.join(view_dir, "view.json"), {
"params": {"startMs": 0} if clean else {"start_ms": 0},
"custom": {},
"root": {
"type": "ia.container.flex",
"meta": {"name": "root"},
"props": {"style": {"classes": "%s/dashboard/card" % ns_lower
if clean else "%s/Card" % ns_lower}},
"children": [{
"type": "ia.display.label",
"meta": {"name": "TitleLabel" if clean else "titleLabel"},
"custom": {"labelText": ""} if clean else {"label_text": ""},
"props": {},
"scripts": {"messageHandlers": [{
"messageType": ("exchange.sampleResource.refresh"
if clean else "refresh")}]},
}],
},
})
style_dir = os.path.join(proj, PERSP, "style-classes", *ns_lower.split("/"),
*(("dashboard", "card") if clean else ("Card",)))
write_json(os.path.join(style_dir, "resource.json"),
dict(RESOURCE_JSON, files=["style.json"]))
write_json(os.path.join(style_dir, "style.json"), {"style": {}})
write_json(os.path.join(proj, PERSP, "page-config", "config.json"),
{"pages": {"/sample-resource" if clean else "/": {
"viewPath": "%s/Dashboard" % ns_upper}}, "sharedDocks": {}})
write_json(os.path.join(proj, PERSP, "session-props", "props.json"),
{"custom": {} if clean else {"MyCompany": {"selectedTab": 0}},
"props": {}})
code_dir = os.path.join(proj, "ignition", "script-python",
*lib_pkg.split("/"), "fmt")
write_json(os.path.join(code_dir, "resource.json"),
dict(RESOURCE_JSON, scope="A", files=["code.py"]))
write(os.path.join(code_dir, "code.py"), CLEAN_CODE if clean else DIRTY_CODE)
if clean:
write(os.path.join(proj, "README.md"), "# Sample Resource\n\nInstall steps.\n")
else:
vis_ctags = os.path.join(proj, el.VISION, "client-tags")
write_json(os.path.join(vis_ctags, "resource.json"),
dict(RESOURCE_JSON, files=["data.bin"]))
write(os.path.join(vis_ctags, "data.bin"), "x")
return proj
SQL_CLEAN = """
CREATE TABLE ex_sr_contacts (
id INT NOT NULL AUTO_INCREMENT,
contact_name VARCHAR(64),
PRIMARY KEY (id)
);
CREATE TABLE ex_sr_calls (
id INT NOT NULL AUTO_INCREMENT,
ex_sr_contacts_id INT,
PRIMARY KEY (id),
FOREIGN KEY (ex_sr_contacts_id) REFERENCES ex_sr_contacts(id)
);
"""
SQL_DIRTY = """
CREATE TABLE Contacts (
contactId INT NOT NULL AUTO_INCREMENT,
ContactName VARCHAR(64),
PRIMARY KEY (contactId)
);
CREATE TABLE ex_sr_calls (
id INT NOT NULL AUTO_INCREMENT,
contact INT,
PRIMARY KEY (id),
FOREIGN KEY (contact) REFERENCES Contacts(contactId)
);
"""
TAGS_CLEAN = {"tags": [{"name": "Exchange", "tagType": "Folder", "tags": [
{"name": RESOURCE, "tagType": "Folder", "tags": [
{"name": "AlarmCount", "tagType": "AtomicTag", "valueSource": "memory"}]}]}]}
TAGS_DIRTY = {"tags": [{"name": "my_company", "tagType": "Folder", "tags": [
{"name": "alarm_count", "tagType": "AtomicTag", "valueSource": "memory"}]}]}
# --------------------------------------------------------------------- tests
class ExchangeLintTest(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.tmp = tempfile.mkdtemp(prefix="exlint_test_")
cls.clean = build_project(cls.tmp, "sample-resource", clean=True)
cls.dirty = build_project(cls.tmp, "SampleResource_v2", clean=False)
@classmethod
def tearDownClass(cls):
shutil.rmtree(cls.tmp, ignore_errors=True)
def run_lint(self, proj, **kw):
kw.setdefault("tags", [])
kw.setdefault("sql", [])
kw.setdefault("resource", RESOURCE)
rep, _name, _res = el.lint_project(proj, **kw)
return rep
def rules(self, rep):
return {f[1] for f in rep.findings}
# -- the clean project must be silent ---------------------------------
def test_clean_project_has_no_findings(self):
rep = self.run_lint(self.clean)
self.assertEqual(rep.findings, [],
"conforming project produced findings:\n" +
"\n".join("%s %s %s: %s" % f for f in rep.findings))
def test_clean_project_exits_zero(self):
rc = el.main([self.clean, "--resource", RESOURCE, "--tags", "--sql"])
self.assertEqual(rc, 0)
# -- the dirty project must fire each rule ----------------------------
def test_dirty_project_fires_expected_rules(self):
got = self.rules(self.run_lint(self.dirty))
for rule in ("E-PROJ-NAME", "E-PROJ-TITLE", "E-PROJ-DESC",
"E-NS-ROOT", "E-COMP-NAME", "E-PROP-NAME",
"E-PAGE-ROOT", "E-STYLE-NAME", "E-MSG-NAME",
"E-STORE-SESS", "E-STORE-CTAG",
"E-PY-TABS", "E-PY-FUNC", "E-PY-VAR", "E-PY-DOC",
"E-PY-COMMENT", "E-UP-DOCS"):
self.assertIn(rule, got, "expected %s to fire" % rule)
def test_dirty_project_exits_nonzero(self):
rc = el.main([self.dirty, "--resource", RESOURCE, "--tags", "--sql"])
self.assertEqual(rc, 1)
# -- severity model ---------------------------------------------------
def test_namespace_and_storage_are_failures(self):
rep = self.run_lint(self.dirty)
fails = {f[1] for f in rep.fails}
for rule in ("E-NS-ROOT", "E-STORE-SESS", "E-STORE-CTAG", "E-PROJ-NAME"):
self.assertIn(rule, fails, "%s should be a FAIL, not a warning" % rule)
def test_naming_rules_are_warnings_by_default(self):
rep = self.run_lint(self.dirty)
warns = {f[1] for f in rep.warns}
for rule in ("E-COMP-NAME", "E-PY-FUNC", "E-PY-TABS"):
self.assertIn(rule, warns, "%s should warn by default" % rule)
def test_strict_promotes_warnings(self):
rep = self.run_lint(self.dirty, strict=True)
self.assertEqual(rep.warns, [], "--strict left warnings behind")
self.assertIn("E-COMP-NAME", {f[1] for f in rep.fails})
def test_ignore_suppresses_a_rule(self):
rep = self.run_lint(self.dirty, ignore={"E-COMP-NAME"})
self.assertNotIn("E-COMP-NAME", self.rules(rep))
# -- database and tag checks -----------------------------------------
def test_sql_clean_and_dirty(self):
clean_sql = os.path.join(self.tmp, "clean.sql")
dirty_sql = os.path.join(self.tmp, "dirty.sql")
write(clean_sql, SQL_CLEAN)
write(dirty_sql, SQL_DIRTY)
rep = el.Report()
el.check_database(rep, [clean_sql], RESOURCE)
self.assertEqual(rep.findings, [],
"conforming schema produced findings: %s" % rep.findings)
rep = el.Report()
el.check_database(rep, [dirty_sql], RESOURCE)
got = {f[1] for f in rep.findings}
for rule in ("E-DB-TABLE", "E-DB-COL", "E-DB-ID", "E-DB-FK"):
self.assertIn(rule, got, "expected %s to fire" % rule)
def test_tags_clean_and_dirty(self):
clean_tags = os.path.join(self.tmp, "clean-tags.json")
dirty_tags = os.path.join(self.tmp, "dirty-tags.json")
write_json(clean_tags, TAGS_CLEAN)
write_json(dirty_tags, TAGS_DIRTY)
rep = el.Report()
el.check_tags(rep, [clean_tags], RESOURCE)
self.assertEqual(rep.findings, [],
"conforming tag export produced findings: %s" % rep.findings)
rep = el.Report()
el.check_tags(rep, [dirty_tags], RESOURCE)
got = {f[1] for f in rep.findings}
self.assertIn("E-NS-ROOT", got)
self.assertIn("E-TAG-NAME", got)
# -- discovery --------------------------------------------------------
def test_discovers_both_projects_from_a_containing_folder(self):
found = el.discover_projects(self.tmp)
self.assertEqual(sorted(os.path.basename(p) for p in found),
["SampleResource_v2", "sample-resource"])
def test_project_folder_resolves_to_itself(self):
self.assertEqual(el.discover_projects(self.clean), [self.clean])
def test_zip_export_is_unpacked(self):
archive = shutil.make_archive(
os.path.join(self.tmp, "export"), "zip",
root_dir=os.path.dirname(self.clean),
base_dir=os.path.basename(self.clean))
found = el.discover_projects(archive)
self.assertEqual(len(found), 1)
self.assertTrue(os.path.exists(os.path.join(found[0], "project.json")))
def test_resource_name_inferred_from_namespace(self):
self.assertEqual(el.infer_resource(self.clean, "sample-resource"), RESOURCE)
# -- name predicates --------------------------------------------------
def test_name_predicates(self):
self.assertTrue(el.is_pascal("AreaCard"))
self.assertFalse(el.is_pascal("areaCard"))
self.assertTrue(el.is_camel("startMs"))
self.assertFalse(el.is_camel("start_ms"))
self.assertTrue(el.is_kebab("bad-actors"))
self.assertFalse(el.is_kebab("BadActors"))
self.assertTrue(el.is_title("Alarm Dashboard"))
self.assertTrue(el.is_title("Table of Contents"))
self.assertFalse(el.is_title("alarm dashboard"))
self.assertTrue(el.is_title_or_pascal("AlarmDashboard"))
def test_rule_table_ids_are_unique_and_referenced(self):
ids = [r[0] for r in el.RULES]
self.assertEqual(len(ids), len(set(ids)), "duplicate rule id in RULES")
with open(os.path.join(os.path.dirname(os.path.abspath(__file__)),
"exchange_lint.py")) as f:
source = f.read()
for rid in ids:
self.assertGreaterEqual(source.count('"%s"' % rid), 2,
"%s is declared but never raised" % rid)
if __name__ == "__main__":
unittest.main(verbosity=2)

View File

@@ -0,0 +1,180 @@
---
name: ignition-exchange-convert
description: Convert an existing Ignition project into a publishable Exchange resource — re-namespacing views/styles/scripts, migrating Perspective session properties to view properties, renaming Python to the guide's conventions — and prove the conversion did not break anything. Use when asked to make a project Exchange-ready, de-brand a project for publication, or refactor one out of session-scoped state. Bundles verify_refs.py and parity.py, which catch the two failure modes a conformance checker cannot see: dangling references and silently dropped features. Not for merely checking conformance (use ignition-exchange-conformance) or uploading (use ignition-exchange-publish).
---
# Converting a project into an Exchange resource
A conversion is a large mechanical rename plus one genuine refactor. It is
**very easy to produce a project that is perfectly named and completely broken**
so the verification gates below are not optional garnish, they are the point.
Check the target shape first with
[`ignition-exchange-conformance`](../ignition-exchange-conformance/SKILL.md);
this skill is what to do about the findings.
## Work on a copy
Never convert in place. Copy the project to its new name, leave the original
untouched, and diff against it at the end. The original is your parity baseline —
without it you cannot prove you did not drop a feature.
Ignition project folder name **is** the project name, so the copy's directory
must already be the new lowercase-dashed name.
## Order matters
Do it in this order. Step 2 invalidates bindings that step 3 would otherwise
have to rewrite twice.
1. **Delete what must not ship.** Vision `client-tags/`, scratch views, dev-only
resources.
2. **Session properties → view properties**, while paths are still the familiar
old ones. Verify the app still works before touching namespaces.
3. **Re-namespace** views, style classes, scripts, page config — one mechanical
sweep.
4. **Python pass** — tabs, camelCase, docstrings, loggers.
5. **Content sweep** — branding, internal names, placeholder text.
6. **Verify** (gates below), then package.
## Step 2 is the real refactor
Everything else is search-and-replace. This one changes how the app works.
The guide forbids session custom properties in an Exchange resource because
importing merges them into the host project's session props. Replace with:
- **State lives on the root view** as `custom` properties.
- **Flows down** to child views as `params` (the guide is explicit: `params` =
public/configuration, `custom` = internal).
- **Flows up** as page-scoped messages handled on the root view, named
`exchange.resourceName.handlerName`.
**Bindings must stay reactive.** Do not park binding-read state in
`system.util.getGlobals()` — globals do not trigger Perspective binding updates.
Globals are only for non-reactive cross-session data, and most resources need
none.
Before rewriting a binding, check **what it actually depended on**. A bidirectional
binding that writes a session prop does not necessarily re-trigger the data fetch;
if the fetch keyed off a `refreshToken`, then an explicit Apply button was already
the commit point and your message-based version preserves behaviour exactly. Read
the dependency graph rather than assuming.
## Jython 2.7 constraints
Gateway-scoped code is Jython 2.7. Violations are silent until they reach the gateway.
- **No f-strings.** `%` formatting only.
- No `typing`, `statistics`, `zoneinfo`.
- Java exceptions bypass `except Exception:` — use bare `except:` with
`sys.exc_info()` in gateway-facing defensive code.
- **Scripts embedded in `view.json`** are JSON strings that must be tab-indented:
the first character of every line in the block is a tab.
## Python renames
The guide wants camelCase functions and variables (deliberately not PEP-8) and
tab indentation. Two traps:
- **Rename through the tokenizer, not regex.** A blind replace will corrupt
string literals and attribute names that happen to match (`lt.tm_wday`).
- **Do not rename data-dictionary keys.** Keys inside dicts returned to view
bindings are a wire format between the scripts and every binding that reads
them. They are not covered by any rule and renaming them is a large, risky
diff for no conformance gain.
Tab conversion applies to **indentation** only — bracket continuation lines
starting with spaces are fine and normal. Verify with the tokenizer's INDENT
tokens, not by grepping for leading spaces.
## Verification gates
Run all four. Each catches something the others cannot.
### 1 · Conformance
```bash
python3 skills/ignition-exchange-conformance/exchange_lint.py <new-project>
```
Target: **0 failures**. Warnings are a judgement call.
### 2 · Referential integrity
```bash
python3 skills/ignition-exchange-convert/verify_refs.py <new-project> --old <OldNamespace>
```
Resolves every embedded view path, style-class reference, `view.params.X` /
`view.custom.X`, and every call into the project library against the functions
that actually exist. Also flags leftover old-namespace strings, surviving
`session.custom` reads, untabbed script transforms, f-strings, and `resource.json`
drift. Target: **0 breakages**.
With ~40 function renames, a missed call site is the single most likely defect,
and nothing else finds it.
### 3 · Feature parity
```bash
python3 skills/ignition-exchange-convert/parity.py <original-project> <new-project>
```
Fingerprints every view — component types and names, bindings by target property
and type, script transforms, event and message handlers, embedded views, declared
params — with names normalised for case, punctuation and snake/camel. **A pure
rename cancels out; a dropped feature does not.**
Target: **zero `LOST` or `MISSING` lines.** `ADDED` lines are expected — the
session-props refactor legitimately adds params and handlers. Read the diff, do
not just count it.
### 4 · Behaviour
If the project has a test suite over its pure modules, run it against the
converted library. Point the package name at the new location and alias
camelCase functions **and their keyword arguments** back to the original names —
a renamed kwarg (`higher_is_worse``higherIsWorse`) breaks callers that pass it
by name, which no static check catches.
Compare against the **original project's** result, not against zero failures. A
pre-existing failure staying pre-existing is a pass.
### Then: load it on a gateway
Static analysis cannot prove a screen renders. Load the project and open it.
A clean project load proves resources parse; only a browser proves components
render.
## De-branding
Conformance checkers read names, never label text. Sweep user-visible strings
explicitly — `props.text`, `title`, `placeholder`, `tooltip` — for company names,
project codenames, contest or demo references, internal hostnames, ticket numbers.
Strip the text rather than deleting the component, so layout and parity are
unchanged.
## Testing a minimum-version claim
To claim a floor lower than the gateway you built on, load the project on that
version — **against a copy of the files**. An older gateway rewrites every
`resource.json` with `lastModification` metadata and leaves root-owned
`.resources/` artifacts behind that your user account cannot delete (clean up
with a throwaway container rather than sudo).
A clean load proves resources parse. It does **not** prove screens render — a
component property introduced in a later version fails at render time, not load
time. Open the client.
## Reporting
Report what the gates actually said, with numbers. A conversion reported
accurately with two known gaps is worth far more than one claimed clean. If a
gate is a false positive, say which and why — the tooling is not infallible and
both bundled scripts have known blind spots documented in their docstrings.
## Provenance
Procedure and both scripts derived from a full conversion of a 20-view / 32-style
/ 3-module Perspective project (2026-09-15), verified on Ignition 8.1.20 and
8.3.7. The gate thresholds are the ones that actually caught defects during it.

View File

@@ -0,0 +1,264 @@
#!/usr/bin/env python3
"""Functional-parity check between an original Ignition project and a converted copy.
Renaming is supposed to be behaviour-preserving. This compares what the two
projects DO, ignoring what things are called: every view, every component, every
binding, every event/message handler, every embedded view and style reference is
fingerprinted with names normalised (case, punctuation, snake/camel folded) so a
pure rename cancels out and a DROPPED FEATURE does not.
Usage: python3 parity.py <original_project> <converted_project>
"""
import json
import os
import re
import sys
from collections import Counter
PERSP = "com.inductiveautomation.perspective"
EMBED = {"ia.display.view", "ia.display.flex-repeater"}
DIFFS = []
def diff(scope, msg):
DIFFS.append((scope, msg))
def norm(s):
"""Fold a name to bare lowercase alphanumerics so renames cancel out."""
return re.sub(r"[^a-z0-9]", "", str(s).lower())
def segs(path):
return [p for p in str(path).replace("\\", "/").split("/") if p]
def common_prefix_len(paths):
"""How many leading segments every resource shares -- i.e. the namespace.
Never eats the final segment, so a single-resource project still has a name.
"""
lists = [segs(p) for p in paths if segs(p)]
if not lists:
return 0
n = 0
while True:
if any(len(l) <= n + 1 for l in lists):
break
first = lists[0][n]
if any(l[n] != first for l in lists):
break
n += 1
return n
def key_of(path, strip):
"""Namespace-stripped identity: folds case, punctuation and re-foldering."""
return norm("".join(segs(path)[strip:]))
def collect(base, payload):
out = {}
if not os.path.isdir(base):
return out
for dp, _d, fs in os.walk(base):
if payload in fs:
rel = os.path.relpath(dp, base).replace(os.sep, "/")
if rel != ".":
out[rel] = os.path.join(dp, payload)
return out
def walk(node, visit, path="root"):
if not isinstance(node, dict):
return
visit(node, path)
for i, c in enumerate(node.get("children") or []):
n = (c.get("meta") or {}).get("name", "?") if isinstance(c, dict) else "?"
walk(c, visit, "%s/%s[%d]" % (path, n, i))
def view_fingerprint(vfile, embed_key):
"""A behaviour fingerprint for one view, insensitive to naming."""
view = json.load(open(vfile, encoding="utf-8"))
fp = {
"types": Counter(),
"names": Counter(),
"bindings": Counter(),
"transforms": Counter(),
"events": Counter(),
"handlers": Counter(),
"embeds": Counter(),
"stylerefs": 0,
"params": set(),
"components": 0,
}
def record_props(node, owner):
for key, cfg in (node.get("propConfig") or {}).items():
b = (cfg or {}).get("binding")
if not b:
continue
# the bound property is the feature; its binding type is how it is fed
fp["bindings"][(norm(key), b.get("type"))] += 1
for tr in (b.get("transforms") or []):
fp["transforms"][tr.get("type")] += 1
scripts = node.get("scripts") or {}
for ev in (scripts.get("customMethods") or []):
fp["events"][("method", norm(ev.get("name")))] += 1
for mh in (scripts.get("messageHandlers") or []):
fp["handlers"][norm((mh.get("messageType") or "").split(".")[-1])] += 1
for comp in (scripts.get("extensionFunctions") or {}):
fp["events"][("ext", norm(comp))] += 1
events = node.get("events") or {}
for domain, acts in events.items():
if isinstance(acts, dict):
for act in acts:
fp["events"][(norm(domain), norm(act))] += 1
record_props(view, "view")
for k in (view.get("propConfig") or {}):
if k.startswith("params."):
fp["params"].add(norm(k.split(".")[1].split("[")[0]))
for k in (view.get("params") or {}):
fp["params"].add(norm(k))
def visit(node, cpath):
if cpath != "root":
fp["components"] += 1
fp["types"][node.get("type")] += 1
nm = (node.get("meta") or {}).get("name")
if nm:
fp["names"][norm(nm)] += 1
record_props(node, cpath)
if node.get("type") in EMBED:
p = (node.get("props") or {}).get("path")
if isinstance(p, str) and p:
fp["embeds"][embed_key(p)] += 1
cls = ((node.get("props") or {}).get("style") or {}).get("classes")
if isinstance(cls, str):
fp["stylerefs"] += len([c for c in cls.split() if c])
walk(view.get("root") or {}, visit)
return fp
def compare_counter(scope, label, a, b):
lost = a - b
gained = b - a
for k, n in sorted(lost.items(), key=lambda kv: str(kv[0])):
diff(scope, "LOST %s %s x%d" % (label, k, n))
for k, n in sorted(gained.items(), key=lambda kv: str(kv[0])):
diff(scope, "ADDED %s %s x%d" % (label, k, n))
def main():
old_p, new_p = os.path.abspath(sys.argv[1]), os.path.abspath(sys.argv[2])
old_views = collect(os.path.join(old_p, PERSP, "views"), "view.json")
new_views = collect(os.path.join(new_p, PERSP, "views"), "view.json")
old_styles = collect(os.path.join(old_p, PERSP, "style-classes"), "style.json")
new_styles = collect(os.path.join(new_p, PERSP, "style-classes"), "style.json")
# Strip each project's own namespace so a pure re-namespacing cancels out.
ov_strip = common_prefix_len(old_views)
nv_strip = common_prefix_len(new_views)
os_strip = common_prefix_len(old_styles)
ns_strip = common_prefix_len(new_styles)
def index(d, strip):
out = {}
for k, v in d.items():
out.setdefault(key_of(k, strip), []).append((k, v))
return out
oi, ni = index(old_views, ov_strip), index(new_views, nv_strip)
for key in sorted(set(oi) - set(ni)):
diff("views", "MISSING view '%s' (no counterpart for %s)"
% (key, oi[key][0][0]))
for key in sorted(set(ni) - set(oi)):
diff("views", "EXTRA view '%s' (%s)" % (key, ni[key][0][0]))
# Style classes legitimately change shape (Grade/A -> grade-a, and
# E-STYLE-HIER re-folders them), so identity matching would be noise.
# What must not change is that none disappeared -- and verify_refs.py
# separately proves every remaining reference still resolves.
if len(old_styles) != len(new_styles):
diff("styles", "style class count %d -> %d" % (len(old_styles), len(new_styles)))
okeys = {key_of(k, os_strip) for k in old_styles}
nkeys = {key_of(k, ns_strip) for k in new_styles}
for k in sorted(okeys - nkeys):
diff("styles", "no class resembling '%s' in the new project" % k)
# ---- per-view behaviour ---------------------------------------------
for key in sorted(set(oi) & set(ni)):
ofp = view_fingerprint(oi[key][0][1], lambda p: key_of(p, ov_strip))
nfp = view_fingerprint(ni[key][0][1], lambda p: key_of(p, nv_strip))
scope = "view %s" % key
if ofp["components"] != nfp["components"]:
diff(scope, "component count %d -> %d"
% (ofp["components"], nfp["components"]))
if ofp["stylerefs"] != nfp["stylerefs"]:
diff(scope, "style class references %d -> %d"
% (ofp["stylerefs"], nfp["stylerefs"]))
for label in ("types", "names", "bindings", "transforms",
"events", "handlers", "embeds"):
compare_counter(scope, label[:-1] if label.endswith("s") else label,
ofp[label], nfp[label])
lost_params = ofp["params"] - nfp["params"]
if lost_params:
diff(scope, "params no longer declared: %s" % ", ".join(sorted(lost_params)))
# ---- library surface -------------------------------------------------
def lib(root):
out = {}
base = os.path.join(root, "ignition", "script-python")
for dp, _d, fs in os.walk(base):
if "code.py" not in fs:
continue
mod = os.path.basename(dp)
src = open(os.path.join(dp, "code.py"), encoding="utf-8",
errors="replace").read()
fns = {}
for m in re.finditer(r"^def\s+([A-Za-z_][A-Za-z0-9_]*)\s*\(([^)]*)\)",
src, re.M):
args = [a.strip().split("=")[0].strip()
for a in m.group(2).split(",") if a.strip()]
fns[norm(m.group(1))] = len(args)
out[norm(mod)] = fns
return out
ol, nl = lib(old_p), lib(new_p)
for mod in sorted(set(ol) - set(nl)):
diff("library", "MISSING module %s" % mod)
for mod in sorted(set(ol) & set(nl)):
for fn in sorted(set(ol[mod]) - set(nl[mod])):
diff("library", "MISSING function %s.%s" % (mod, fn))
for fn in sorted(set(ol[mod]) & set(nl[mod])):
if ol[mod][fn] != nl[mod][fn]:
diff("library", "arity change %s.%s: %d -> %d args"
% (mod, fn, ol[mod][fn], nl[mod][fn]))
# ---- pages -----------------------------------------------------------
def pages(root):
p = os.path.join(root, PERSP, "page-config", "config.json")
if not os.path.exists(p):
return {}
return (json.load(open(p)) or {}).get("pages") or {}
op, np_ = pages(old_p), pages(new_p)
if len(op) != len(np_):
diff("pages", "page count %d -> %d" % (len(op), len(np_)))
ot = {key_of(v.get("viewPath", ""), ov_strip) for v in op.values()}
nt = {key_of(v.get("viewPath", ""), nv_strip) for v in np_.values()}
for k in sorted(ot - nt):
diff("pages", "no page maps to view %s any more" % k)
for scope, msg in DIFFS:
print("DIFF %-28s %s" % (scope, msg))
print("parity: %d view(s) vs %d, %d style(s) vs %d, %d difference(s)"
% (len(old_views), len(new_views), len(old_styles), len(new_styles),
len(DIFFS)))
return 1 if DIFFS else 0
if __name__ == "__main__":
sys.exit(main())

View File

@@ -0,0 +1,217 @@
#!/usr/bin/env python3
"""Referential-integrity check for a migrated Ignition project.
The style checker tests NAMES. This tests that everything still POINTS somewhere
real after a namespace + function rename: embedded views, style classes, view
params, and every call into the project library. A migration can be perfectly
named and completely broken; this is the half that catches that.
Usage: python3 verify_refs.py <project_dir> [--old OldNamespace]
"""
import ast
import json
import os
import re
import sys
PERSP = "com.inductiveautomation.perspective"
EMBED = {"ia.display.view", "ia.display.flex-repeater"}
FAILS = []
def fail(where, msg):
FAILS.append((where, msg))
def walk(node, visit, path="root"):
if not isinstance(node, dict):
return
visit(node, path)
for i, c in enumerate(node.get("children") or []):
n = (c.get("meta") or {}).get("name", "?") if isinstance(c, dict) else "?"
walk(c, visit, "%s/%s[%d]" % (path, n, i))
def main():
proj = os.path.abspath(sys.argv[1])
# The project's previous namespace, e.g. "MyCompany". Any surviving mention
# of it in the converted project is a missed rename.
old = ""
if "--old" in sys.argv:
old = sys.argv[sys.argv.index("--old") + 1]
views_root = os.path.join(proj, PERSP, "views")
styles_root = os.path.join(proj, PERSP, "style-classes")
sp_root = os.path.join(proj, "ignition", "script-python")
def collect(base, payload):
out = set()
for dp, _d, fs in os.walk(base):
if payload in fs:
r = os.path.relpath(dp, base)
if r != ".":
out.add(r.replace(os.sep, "/"))
return out
views = collect(views_root, "view.json")
styles = collect(styles_root, "style.json")
# ---- project library: package path -> {function names} ----------------
lib = {}
for dp, _d, fs in os.walk(sp_root):
if "code.py" not in fs:
continue
pkg = os.path.relpath(dp, sp_root).replace(os.sep, ".")
src = open(os.path.join(dp, "code.py"), encoding="utf-8",
errors="replace").read()
try:
tree = ast.parse(src)
lib[pkg] = {n.name for n in tree.body
if isinstance(n, (ast.FunctionDef, ast.AsyncFunctionDef))}
except SyntaxError as e:
fail(pkg + "/code.py", "does not parse: line %s %s" % (e.lineno, e.msg))
lib[pkg] = set()
if re.search(r"""(^|[^A-Za-z0-9_'"])[fF]["']""", src):
fail(pkg + "/code.py", "f-string present (Jython 2.7 cannot parse)")
lib_roots = {p.split(".")[0] for p in lib}
# ---- per view ---------------------------------------------------------
for vp in sorted(views):
vfile = os.path.join(views_root, vp.replace("/", os.sep), "view.json")
where = "views/" + vp
raw = open(vfile, encoding="utf-8", errors="replace").read()
try:
view = json.loads(raw)
except Exception as e:
fail(where, "invalid JSON: %s" % e)
continue
if old and old in raw:
hits = len(re.findall(re.escape(old), raw))
fail(where, "%d leftover reference(s) to old namespace %r" % (hits, old))
if "session.custom" in raw:
fail(where, "still reads session.custom (Exchange resources must not)")
declared = set((view.get("params") or {}).keys())
declared_custom = set((view.get("custom") or {}).keys())
for k in (view.get("propConfig") or {}):
if k.startswith("params."):
declared.add(k.split(".")[1].split("[")[0])
elif k.startswith("custom."):
declared_custom.add(k.split(".")[1].split("[")[0])
def visit(node, cpath):
if node.get("type") in EMBED:
p = (node.get("props") or {}).get("path")
bound = "props.path" in (node.get("propConfig") or {})
if isinstance(p, str) and p:
if p not in views:
fail(where, "%s embeds missing view %r" % (cpath, p))
elif not bound:
pass
cls = ((node.get("props") or {}).get("style") or {}).get("classes")
if isinstance(cls, str):
for c in cls.split():
if c and c not in styles:
fail(where, "%s references missing style class %r" % (cpath, c))
walk(view.get("root") or {}, visit)
# bindings: params/custom resolution, tab indent, library calls
def each_cfg():
for k, v in (view.get("propConfig") or {}).items():
yield "view." + k, v
acc = []
walk(view.get("root") or {},
lambda n, p: [acc.append(("%s %s" % (p, k), v))
for k, v in (n.get("propConfig") or {}).items()])
for it in acc:
yield it
for owner, cfg in each_cfg():
b = (cfg or {}).get("binding")
if not b:
continue
blobs = [json.dumps(b.get("config") or {})]
for tr in b.get("transforms") or []:
code = tr.get("code", "")
blobs.append(code)
if tr.get("type") == "script" and code and not code.startswith(("\t", "\n")):
fail(where, "%s: script transform not tab-indented" % owner)
blob = " ".join(blobs)
for m in re.finditer(r"view\.params\.([A-Za-z0-9_]+)", blob):
if m.group(1) not in declared:
fail(where, "%s: undeclared view param %r" % (owner, m.group(1)))
for m in re.finditer(r"view\.custom\.([A-Za-z0-9_]+)", blob):
if m.group(1) not in declared_custom:
fail(where, "%s: undeclared view custom prop %r" % (owner, m.group(1)))
check_lib_calls(raw, where, lib, lib_roots)
# ---- library calling itself ------------------------------------------
for dp, _d, fs in os.walk(sp_root):
if "code.py" not in fs:
continue
p = os.path.join(dp, "code.py")
rel = os.path.relpath(p, proj)
src = open(p, encoding="utf-8", errors="replace").read()
if old and old in src:
fail(rel, "leftover reference to old namespace %r" % old)
check_lib_calls(src, rel, lib, lib_roots)
# ---- resource.json integrity -----------------------------------------
for dp, ds, fs in os.walk(proj):
if ".resources" in ds:
fail(os.path.relpath(dp, proj), "authored .resources directory")
ds.remove(".resources")
if "resource.json" not in fs:
continue
rel = os.path.relpath(dp, proj)
try:
res = json.load(open(os.path.join(dp, "resource.json")))
except Exception as e:
fail(rel, "invalid resource.json: %s" % e)
continue
declared = res.get("files") or []
payloads = [f for f in fs if f != "resource.json"]
for f in declared:
if f not in payloads:
fail(rel, "resource.json declares missing file %r" % f)
for f in payloads:
if f not in declared:
fail(rel, "undeclared payload file %r" % f)
want = "A" if ("script-python" in rel or "global-props" in rel) else "G"
if res.get("scope") != want:
fail(rel, "scope %r, expected %r" % (res.get("scope"), want))
for where, msg in FAILS:
print("BROKEN %s: %s" % (where, msg))
print("refs: %d view(s), %d style class(es), %d library module(s), %d breakage(s)"
% (len(views), len(styles), len(lib), len(FAILS)))
return 1 if FAILS else 0
def check_lib_calls(text, where, lib, lib_roots):
"""Every <root>.<pkg>.<fn> reference must resolve to a real function."""
for root in lib_roots:
# (?<![.\w]) so a property path like session.custom.Foo.bar is not
# mistaken for a call into a library package named Foo.
for m in re.finditer(r"(?<![.\w])" + re.escape(root)
+ r"((?:\.[A-Za-z_][A-Za-z0-9_]*)+)", text):
parts = m.group(1).lstrip(".").split(".")
for split in range(len(parts) - 1, 0, -1):
pkg = ".".join([root] + parts[:split])
fn = parts[split]
if pkg in lib:
if fn not in lib[pkg]:
fail(where, "calls %s.%s which does not exist in %s/code.py"
% (pkg, fn, pkg.replace(".", "/")))
break
# No prefix resolves to a library module, so this is not a library
# reference at all. The same dotted namespace is used for message
# types and popup ids (exchange.<resource>.<handler>), which are
# strings, not code -- flagging those would be a false positive.
if __name__ == "__main__":
sys.exit(main())

View File

@@ -0,0 +1,130 @@
---
name: ignition-exchange-publish
description: Package and upload a finished Ignition resource to the Ignition Exchange — which file format to export, what to verify inside the archive before uploading, and what to put in every field of the Exchange upload form (title, tagline, categories, version, minimum Ignition platform, required modules, package files, custom installation instructions). Use when asked how to publish, export, or upload an Ignition project to the Exchange, or what format the Exchange expects. Not for checking conformance (use ignition-exchange-conformance) or converting a project into Exchange shape (use ignition-exchange-convert).
---
# Publishing to the Ignition Exchange
## The artifact is a project export `.zip`
Produced by **Designer → File → Export**, or **Gateway → Config → Projects →
*project* → Export**. Both give the same file.
> **Never upload a gateway backup (`.gwbk`).** It contains the entire gateway —
> datasource credentials, journal profiles, tags, every other project. Wrong
> artifact, and it leaks configuration into a public resource.
A repo that stores projects as unpacked files (the gateway-as-files pattern) has
no zip in it — one has to be generated.
## What goes in the upload
The Exchange accepts multiple package files, 10 MB each.
| File | Upload as | When |
|---|---|---|
| `<resource>.zip` | Package file | Always — the project export |
| `README.md` / PDF | Package file | Always, as a **separate** file |
| Tag export `.json` | Package file | Only if it needs tags/UDTs |
| Database dump `.sql` | Package file | Only if it needs a schema or seed data |
| Screenshots | Images & Screenshots | Optional, strongly encouraged |
**The README is probably not inside the zip.** Ignition's export serialises
recognised project resources — each is a directory with a `resource.json`. A
loose `README.md` at the project root is not one, so the export is expected to
drop it. Unzip and check rather than assuming. The guide asks for docs as a
separate package file regardless:
> **Note:** If you already have detailed documentation for your resource please
> note it here and include the documentation file in the Package File upload.
Note that a conformance checker's documentation rule inspects the working
directory, not the archive — a passing lint does not prove the zip carries docs.
## Before packaging
The guide names four pre-upload checks. Three are often N/A; the first never is.
1. **Check the project export** — import the zip into a brand-new project, *and*
into a project that already has content. The second is the case the whole
namespacing convention exists for and the only way to prove nothing collides.
2. **Include the tags** — if it binds to tags or UDTs, export them and verify
coverage.
3. **Database backups** — if it expects tables or seed data, include the dump and
name the vendor (MySQL / MSSQL / PostgreSQL) in both the description and the
install instructions.
4. **Take notes while testing** — they become the Custom Installation
Instructions. Readers range from beginner to intermediate.
## Verify the archive
```bash
unzip -l <resource>.zip
unzip -o <resource>.zip -d /tmp/verify-export
```
- File count matches the project on disk, minus anything the export legitimately drops
- Every view, style class and script module present
- `README.md` presence — confirm, do not assume
- No `.resources/` (gateway-owned cache), `__pycache__`, editor backups
- No credentials anywhere
## The upload form
### Overview
| Field | Guidance |
|---|---|
| Visibility | `Public` unless genuinely private |
| Title | Short, Title Case, says what it does; usually matches project title |
| Tagline | One sentence, must **not** repeat the title |
| Description | What it does, how it's used, who benefits, relevant industries |
| Resource Type | Closest dropdown match + skill level for install/use complexity |
| Background Image | Optional card styling |
| Category | **Up to 3**: Alarming, Analytics, Connectivity, Dashboard, Diagnostics, Edge Computing, Enterprise, HMI, IIOT/MQTT, MES, Maintenance, Mapping/GIS, Mobile, Monitoring, Reporting, SCADA, Scripting, Security, Simulation, Trending/Charting, Utility |
| Contact the Developer | Optional; your address is never shown |
| Tags | **Up to 10** search keywords |
| Images & Screenshots | Optional, highly encouraged |
### Package
| Field | Guidance |
|---|---|
| Version | `1.0.0` first release; later auto-increment. **Never** put the version in the project name |
| Ignition Platform | The **minimum** required. Guide: *"If unsure, select the Ignition platform version in which the package was developed with."* Do not claim an untested floor |
| Release Tagline | Short summary of this version's changes; required even for the first |
| Release Notes | Fuller explanation of what changed and its impact |
| Required Modules | IA / Cirrus Link / Sepasoft modules needed |
| Maker Edition | Tick if it runs on Maker; unavailable if you require unsupported modules |
| Other Requirements | External prerequisites, one per line; blank if none |
| Package Files | The zip plus docs / tags / SQL |
| Custom Installation Instructions | Step-by-step; assume a beginner. If you shipped separate docs, say so here |
The Exchange **auto-generates a readme** from the Description and Custom
Installation Instructions — write both properly.
## Verifying a minimum-version claim
Load the project on a throwaway gateway at the version you intend to declare,
using a **copy** of the files — an older gateway rewrites `resource.json`
metadata and leaves root-owned `.resources/` behind.
A clean load proves resources parse. It does **not** prove screens render: a
component property added in a later version fails at render time. Open the
client and look. If you cannot test the lower version, declare the version you
developed on.
## After publishing
- Updates are **new package versions on the same resource**, not new resources.
- **Never rename the namespace folders between versions** — that turns an upgrade
into a duplicate set of resources in every user's project.
- Re-run conformance before every release.
- Non-English resources: the guide encourages shipping a language dropdown with
English alongside.
## Provenance
Field list and quoted text from Exchange Resources Style Guide v1.1.0, §Before
You Upload and §Uploading to the Exchange. Export-drops-README is reasoning from
how Ignition serialises resources, not an observed export — verify it.

1243
exchangeResources/exchange_lint.py Executable file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,317 @@
#!/usr/bin/env python3
"""Self-test for exchange_lint.py.
Builds two throwaway Ignition projects in a temp directory -- one that follows
the Exchange Resources Style Guide and one that breaks a known rule per check --
then asserts the checker stays silent on the first and fires the expected rule
id on the second. This is what keeps the rules honest: a rule that cannot fail
and a rule that always fires both show up here.
Run directly (python3 test_exchange_lint.py) or under pytest.
"""
import json
import os
import shutil
import sys
import tempfile
import unittest
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import exchange_lint as el # noqa: E402
RESOURCE = "SampleResource"
PERSP = el.PERSPECTIVE
# ------------------------------------------------------------------ fixtures
def write(path, text):
os.makedirs(os.path.dirname(path), exist_ok=True)
with open(path, "w") as f:
f.write(text)
def write_json(path, obj):
write(path, json.dumps(obj, indent=2))
RESOURCE_JSON = {"scope": "G", "version": 1, "restricted": False,
"overridable": True, "files": ["view.json"], "attributes": {}}
CLEAN_CODE = '''"""Sample library module."""
def formatCount(rawCount):
"""Return rawCount rendered for display."""
# pad small values so the column stays aligned
displayValue = "%d" % rawCount
return displayValue
'''
DIRTY_CODE = '''def format_count(raw_count):
#no space after hash
display_value = "%d" % raw_count
return display_value
'''
def build_project(root, name, clean=True):
"""Create a minimal Perspective project; clean=False breaks one rule per check."""
proj = os.path.join(root, name)
ns_upper = "Exchange/%s" % RESOURCE if clean else "MyCompany"
ns_lower = "exchange/sample-resource" if clean else "MyCompany"
lib_pkg = "exchange/sampleresource" if clean else "MyCompany"
write_json(os.path.join(proj, "project.json"), {
"title": "Sample Resource" if clean else "",
"description": "A sample Exchange resource." if clean else "",
"enabled": True, "inheritable": False, "parent": "",
})
view_dir = os.path.join(proj, PERSP, "views", *ns_upper.split("/"), "Dashboard")
write_json(os.path.join(view_dir, "resource.json"), RESOURCE_JSON)
write_json(os.path.join(view_dir, "view.json"), {
"params": {"startMs": 0} if clean else {"start_ms": 0},
"custom": {},
"root": {
"type": "ia.container.flex",
"meta": {"name": "root"},
"props": {"style": {"classes": "%s/dashboard/card" % ns_lower
if clean else "%s/Card" % ns_lower}},
"children": [{
"type": "ia.display.label",
"meta": {"name": "TitleLabel" if clean else "titleLabel"},
"custom": {"labelText": ""} if clean else {"label_text": ""},
"props": {},
"scripts": {"messageHandlers": [{
"messageType": ("exchange.sampleResource.refresh"
if clean else "refresh")}]},
}],
},
})
style_dir = os.path.join(proj, PERSP, "style-classes", *ns_lower.split("/"),
*(("dashboard", "card") if clean else ("Card",)))
write_json(os.path.join(style_dir, "resource.json"),
dict(RESOURCE_JSON, files=["style.json"]))
write_json(os.path.join(style_dir, "style.json"), {"style": {}})
write_json(os.path.join(proj, PERSP, "page-config", "config.json"),
{"pages": {"/sample-resource" if clean else "/": {
"viewPath": "%s/Dashboard" % ns_upper}}, "sharedDocks": {}})
write_json(os.path.join(proj, PERSP, "session-props", "props.json"),
{"custom": {} if clean else {"MyCompany": {"selectedTab": 0}},
"props": {}})
code_dir = os.path.join(proj, "ignition", "script-python",
*lib_pkg.split("/"), "fmt")
write_json(os.path.join(code_dir, "resource.json"),
dict(RESOURCE_JSON, scope="A", files=["code.py"]))
write(os.path.join(code_dir, "code.py"), CLEAN_CODE if clean else DIRTY_CODE)
if clean:
write(os.path.join(proj, "README.md"), "# Sample Resource\n\nInstall steps.\n")
else:
vis_ctags = os.path.join(proj, el.VISION, "client-tags")
write_json(os.path.join(vis_ctags, "resource.json"),
dict(RESOURCE_JSON, files=["data.bin"]))
write(os.path.join(vis_ctags, "data.bin"), "x")
return proj
SQL_CLEAN = """
CREATE TABLE ex_sr_contacts (
id INT NOT NULL AUTO_INCREMENT,
contact_name VARCHAR(64),
PRIMARY KEY (id)
);
CREATE TABLE ex_sr_calls (
id INT NOT NULL AUTO_INCREMENT,
ex_sr_contacts_id INT,
PRIMARY KEY (id),
FOREIGN KEY (ex_sr_contacts_id) REFERENCES ex_sr_contacts(id)
);
"""
SQL_DIRTY = """
CREATE TABLE Contacts (
contactId INT NOT NULL AUTO_INCREMENT,
ContactName VARCHAR(64),
PRIMARY KEY (contactId)
);
CREATE TABLE ex_sr_calls (
id INT NOT NULL AUTO_INCREMENT,
contact INT,
PRIMARY KEY (id),
FOREIGN KEY (contact) REFERENCES Contacts(contactId)
);
"""
TAGS_CLEAN = {"tags": [{"name": "Exchange", "tagType": "Folder", "tags": [
{"name": RESOURCE, "tagType": "Folder", "tags": [
{"name": "AlarmCount", "tagType": "AtomicTag", "valueSource": "memory"}]}]}]}
TAGS_DIRTY = {"tags": [{"name": "my_company", "tagType": "Folder", "tags": [
{"name": "alarm_count", "tagType": "AtomicTag", "valueSource": "memory"}]}]}
# --------------------------------------------------------------------- tests
class ExchangeLintTest(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.tmp = tempfile.mkdtemp(prefix="exlint_test_")
cls.clean = build_project(cls.tmp, "sample-resource", clean=True)
cls.dirty = build_project(cls.tmp, "SampleResource_v2", clean=False)
@classmethod
def tearDownClass(cls):
shutil.rmtree(cls.tmp, ignore_errors=True)
def run_lint(self, proj, **kw):
kw.setdefault("tags", [])
kw.setdefault("sql", [])
kw.setdefault("resource", RESOURCE)
rep, _name, _res = el.lint_project(proj, **kw)
return rep
def rules(self, rep):
return {f[1] for f in rep.findings}
# -- the clean project must be silent ---------------------------------
def test_clean_project_has_no_findings(self):
rep = self.run_lint(self.clean)
self.assertEqual(rep.findings, [],
"conforming project produced findings:\n" +
"\n".join("%s %s %s: %s" % f for f in rep.findings))
def test_clean_project_exits_zero(self):
rc = el.main([self.clean, "--resource", RESOURCE, "--tags", "--sql"])
self.assertEqual(rc, 0)
# -- the dirty project must fire each rule ----------------------------
def test_dirty_project_fires_expected_rules(self):
got = self.rules(self.run_lint(self.dirty))
for rule in ("E-PROJ-NAME", "E-PROJ-TITLE", "E-PROJ-DESC",
"E-NS-ROOT", "E-COMP-NAME", "E-PROP-NAME",
"E-PAGE-ROOT", "E-STYLE-NAME", "E-MSG-NAME",
"E-STORE-SESS", "E-STORE-CTAG",
"E-PY-TABS", "E-PY-FUNC", "E-PY-VAR", "E-PY-DOC",
"E-PY-COMMENT", "E-UP-DOCS"):
self.assertIn(rule, got, "expected %s to fire" % rule)
def test_dirty_project_exits_nonzero(self):
rc = el.main([self.dirty, "--resource", RESOURCE, "--tags", "--sql"])
self.assertEqual(rc, 1)
# -- severity model ---------------------------------------------------
def test_namespace_and_storage_are_failures(self):
rep = self.run_lint(self.dirty)
fails = {f[1] for f in rep.fails}
for rule in ("E-NS-ROOT", "E-STORE-SESS", "E-STORE-CTAG", "E-PROJ-NAME"):
self.assertIn(rule, fails, "%s should be a FAIL, not a warning" % rule)
def test_naming_rules_are_warnings_by_default(self):
rep = self.run_lint(self.dirty)
warns = {f[1] for f in rep.warns}
for rule in ("E-COMP-NAME", "E-PY-FUNC", "E-PY-TABS"):
self.assertIn(rule, warns, "%s should warn by default" % rule)
def test_strict_promotes_warnings(self):
rep = self.run_lint(self.dirty, strict=True)
self.assertEqual(rep.warns, [], "--strict left warnings behind")
self.assertIn("E-COMP-NAME", {f[1] for f in rep.fails})
def test_ignore_suppresses_a_rule(self):
rep = self.run_lint(self.dirty, ignore={"E-COMP-NAME"})
self.assertNotIn("E-COMP-NAME", self.rules(rep))
# -- database and tag checks -----------------------------------------
def test_sql_clean_and_dirty(self):
clean_sql = os.path.join(self.tmp, "clean.sql")
dirty_sql = os.path.join(self.tmp, "dirty.sql")
write(clean_sql, SQL_CLEAN)
write(dirty_sql, SQL_DIRTY)
rep = el.Report()
el.check_database(rep, [clean_sql], RESOURCE)
self.assertEqual(rep.findings, [],
"conforming schema produced findings: %s" % rep.findings)
rep = el.Report()
el.check_database(rep, [dirty_sql], RESOURCE)
got = {f[1] for f in rep.findings}
for rule in ("E-DB-TABLE", "E-DB-COL", "E-DB-ID", "E-DB-FK"):
self.assertIn(rule, got, "expected %s to fire" % rule)
def test_tags_clean_and_dirty(self):
clean_tags = os.path.join(self.tmp, "clean-tags.json")
dirty_tags = os.path.join(self.tmp, "dirty-tags.json")
write_json(clean_tags, TAGS_CLEAN)
write_json(dirty_tags, TAGS_DIRTY)
rep = el.Report()
el.check_tags(rep, [clean_tags], RESOURCE)
self.assertEqual(rep.findings, [],
"conforming tag export produced findings: %s" % rep.findings)
rep = el.Report()
el.check_tags(rep, [dirty_tags], RESOURCE)
got = {f[1] for f in rep.findings}
self.assertIn("E-NS-ROOT", got)
self.assertIn("E-TAG-NAME", got)
# -- discovery --------------------------------------------------------
def test_discovers_both_projects_from_a_containing_folder(self):
found = el.discover_projects(self.tmp)
self.assertEqual(sorted(os.path.basename(p) for p in found),
["SampleResource_v2", "sample-resource"])
def test_project_folder_resolves_to_itself(self):
self.assertEqual(el.discover_projects(self.clean), [self.clean])
def test_zip_export_is_unpacked(self):
archive = shutil.make_archive(
os.path.join(self.tmp, "export"), "zip",
root_dir=os.path.dirname(self.clean),
base_dir=os.path.basename(self.clean))
found = el.discover_projects(archive)
self.assertEqual(len(found), 1)
self.assertTrue(os.path.exists(os.path.join(found[0], "project.json")))
def test_resource_name_inferred_from_namespace(self):
self.assertEqual(el.infer_resource(self.clean, "sample-resource"), RESOURCE)
# -- name predicates --------------------------------------------------
def test_name_predicates(self):
self.assertTrue(el.is_pascal("AreaCard"))
self.assertFalse(el.is_pascal("areaCard"))
self.assertTrue(el.is_camel("startMs"))
self.assertFalse(el.is_camel("start_ms"))
self.assertTrue(el.is_kebab("bad-actors"))
self.assertFalse(el.is_kebab("BadActors"))
self.assertTrue(el.is_title("Alarm Dashboard"))
self.assertTrue(el.is_title("Table of Contents"))
self.assertFalse(el.is_title("alarm dashboard"))
self.assertTrue(el.is_title_or_pascal("AlarmDashboard"))
def test_rule_table_ids_are_unique_and_referenced(self):
ids = [r[0] for r in el.RULES]
self.assertEqual(len(ids), len(set(ids)), "duplicate rule id in RULES")
with open(os.path.join(os.path.dirname(os.path.abspath(__file__)),
"exchange_lint.py")) as f:
source = f.read()
for rid in ids:
self.assertGreaterEqual(source.count('"%s"' % rid), 2,
"%s is declared but never raised" % rid)
if __name__ == "__main__":
unittest.main(verbosity=2)

View File

@@ -0,0 +1 @@
4a724368-db9a-4b8f-aac6-8657ca872bcf

View File

@@ -0,0 +1,16 @@
{
"scope": "A",
"version": 1,
"restricted": false,
"overridable": true,
"files": [
"keystore.pfx"
],
"attributes": {
"lastModification": {
"actor": "default",
"timestamp": "2026-07-16T15:30:39Z"
},
"lastModificationSignature": "a1143272a15b5dcfd7c91427149b5aa4d135755ac514638c509e31bcec644040"
}
}

View File

@@ -0,0 +1,16 @@
{
"scope": "A",
"version": 1,
"restricted": false,
"overridable": true,
"files": [
"keystore.pfx"
],
"attributes": {
"lastModification": {
"actor": "default",
"timestamp": "2026-07-16T15:30:39Z"
},
"lastModificationSignature": "77fe27be3c690d4cd7b7820cf97ac8e6257591f59cecde002911b1baec837630"
}
}

View File

@@ -0,0 +1,16 @@
{
"scope": "A",
"version": 1,
"restricted": false,
"overridable": true,
"files": [
"uuid.txt"
],
"attributes": {
"lastModification": {
"actor": "default",
"timestamp": "2026-07-16T15:30:38Z"
},
"lastModificationSignature": "2c44a3cb8f1fdab99edf64c9108ff97ab5be4b6e1d967c40aa19677f9b738c26"
}
}

View File

@@ -0,0 +1 @@
b098aacd-0fb6-4084-a86b-733d5c7c1014

View File

@@ -0,0 +1,7 @@
{
"title": "Local",
"description": "Gateway configuration resources that are unique to the local instance, and will not be synchronized with a redundant pair, if any.",
"enabled": true,
"inheritable": false,
"parent": "core"
}

View File

@@ -0,0 +1,917 @@
# Migration Log
Timestamp: Jul 16, 2026, 10:30:28 AM
Duration: 422 ms
## Summary
213 tables migrated from `/data/db/config.idb`, creating 4 new resources in `/usr/local/bin/ignition/data/config/resources`.
All tables migrated successfully. `config.idb` can now be safely archived.
## Migration Details
### Tables Migrated
- AgentGroup
- AlarmJournals
- AlarmNotificationProfiles
- AlertLog
- AlertNotificationProfileProperties_Basic
- AlertNotificationProfileProperties_Basic_EmailAddresses
- AlertNotificationProfileProperties_Distribution
- AlertNotificationProfiles
- AlertStorageProfileProperties_Datasource
- AlertStorageProfiles
- AuditEvents
- AuditProfileProperties_Datasource
- AuditProfileProperties_Local
- AuditProfiles
- AuthProfileProperties_AD
- AuthProfileProperties_ADHybrid
- AuthProfileProperties_ADtoDB
- AuthProfileProperties_DB
- AuthProfileProperties_Internal
- AuthProfileProperties_LocalFallbackCache
- AuthProfiles
- BASIC_SCHEDULES
- BacnetIpLocalDeviceSettings
- BacnetIpRemoteDeviceSettings
- CERTIFICATES
- COMPOSITE_SCHEDULES
- ClassicSmtpEmailProfiles
- CoBranding
- CompactLogixDriverSettings
- ControlLogixDriverSettings
- DATASOURCES
- DBTRANSLATORS
- DatabaseJournalSettings
- DeviceSettings
- Devices
- Dnp3DeviceSettings
- Dnp3DriverSettings
- DriverProperties
- EULAS
- EamAgentSettings
- EamControllerSettings
- EamLicense
- EdgeJournalSettings
- EdgeSyncSettings
- EdgeSystemProps
- EmailNotificationSettings
- EmailProfiles
- EventThreshold
- FinsTcpDeviceSettings
- FinsUdpDeviceSettings
- GanTagProviderSettings
- GeneralAlarmSettings
- HOLIDAYS
- IDP_ADAPTERS
- IDP_ADAPTER_USER_HISTORY
- IDP_ADAPTER_USER_HISTORY_BK
- Iec61850DeviceSettings
- Iec61850ReportEntryIds
- Images
- InternalAuthMappingTable
- InternalContactInfoTable
- InternalRoleTable
- InternalScheduleAdjustmentTable
- InternalTagProvider
- InternalUserExtraProps
- InternalUserTable
- JDBCDRIVERS
- KeyboardLayouts
- LocalFallbackCache_Entry
- LocalJournalSettings
- LogixDriverSettings
- MetricsDashboards
- Micro800DeviceSettings
- MicroLogixDriverSettings
- MitsubishiTcpDeviceSettings
- ModbusRtuDriverSettings
- ModbusTcpDriverSettings
- NjDriverSettings
- OAuth2ClientSettings
- OAuth2SmtpEmailProfiles
- OPCServers
- OneWayNotificationSettings
- OpcUaConnectionSettings
- OpcUaConnectionSettings2
- OpcUaServerSettings
- PLC5DriverSettings
- ProgrammableSimSettings
- REGIONSETTINGS
- REMEMBERED_SUBJECTS
- REMEMBERED_SUBJECTS_BK
- ROSTER
- ROSTER_ENTRY
- RemoteAgent
- RemoteAuditProfileProperties
- RemoteJournalSettings
- RemoteNotificationSettings
- S71200DriverSettings
- S71500DriverSettings
- S7300DriverSettings
- S7400DriverSettings
- SCHEDULES
- SCHEDULE_PROFILES
- SECURITY_LEVELS
- SLCDriverSettings
- ScriptSettingsRecord
- SecuredEntityPolicy
- SecuredEntityZonePolicy
- SimpleTagProviderProfile
- SipNotificationSettings
- SmsNotificationProfileSettings
- StoreAndForwardSysSettings
- SysProps
- TCPDriverSettings
- TagHistorianProviderSettings
- TagHistoryProviderEP
- TagHistorySplitterSettings
- TagProviderSettings
- TagReportSearch
- TaskEventLog
- TranslationSettings
- TranslationTerms
- UAConnectionSettings
- UDPDriverSettings
- WSChannelSettings
- WideDbHistorianProviderSettings
- XOPCSettings
- csvHistorianSettings
- eamactivatelicensesettings
- eambackuptasksettings
- eaminstallmodulesettings
- eamrestorebackupsettings
- eamsendtagssettings
- eamtasksettings
- edgeHistorianSettings
- gatewaytaskrecord
- remotehistoriansettings
- remoteupgrade
- securityZones
- sfcsettings
- startersteps
- tagconfig
- taggroups
- ws_proxyrules
- wsconnectionsettings
- wsincomingconnection
- wsqueue_overrides
### Strategies Executed
- **Gateway Network Websocket Channel Settings Migrator**
- Trigger table: WSCHANNELSETTINGS
- Duration: 1 ms
- Migrated tables:
- WSChannelSettings
- **Gateway Network Websocket Connection Settings Migrator**
- Trigger table: WSCONNECTIONSETTINGS
- Duration: 0 ms
- Migrated tables:
- wsconnectionsettings
- **Gateway Network Connection Migrator**
- Trigger table: WSINCOMINGCONNECTION
- Duration: 1 ms
- Migrated tables:
- wsincomingconnection
- **Gateway Network Queue Settings Migrator**
- Trigger table: WSQUEUE_OVERRIDES
- Duration: 58 ms
- Migrated tables:
- wsqueue_overrides
- New Resources:
- ignition/gateway-network-queue-settings, collection=core, size=1 kb
- **Gateway Network Proxy Configuration Migrator**
- Trigger table: WS_PROXYRULES
- Duration: 26 ms
- Migrated tables:
- ws_proxyrules
- New Resources:
- ignition/gateway-network-proxy-rules, collection=core, size=22 bytes
- **User Source (INTERNAL) Migrator**
- Trigger table: AUTHPROFILEPROPERTIES_INTERNAL
- Duration: 32 ms
- Migrated tables:
- AuthProfileProperties_Internal
- AuthProfiles
- InternalAuthMappingTable
- InternalContactInfoTable
- InternalRoleTable
- InternalScheduleAdjustmentTable
- InternalUserExtraProps
- InternalUserTable
- Messages:
- All records of type 'AuthProfileProperties_Internal' have been migrated
- All records of type 'AuthProfiles' have been migrated
- All records of type 'InternalUserTable' have been migrated
- All records of type 'InternalRoleTable' have been migrated
- All records of type 'InternalUserExtraProps' have been migrated
- All records of type 'InternalAuthMappingTable' have been migrated
- All records of type 'InternalContactInfoTable' have been migrated
- All records of type 'InternalScheduleAdjustmentTable' have been migrated
- **Realtime Tag Provider (STANDARD) Migrator**
- Trigger table: TAGPROVIDERSETTINGS
- Duration: 3 ms
- Migrated tables:
- InternalTagProvider
- TagProviderSettings
- **Realtime Tag Provider (gantagprovider) Migrator**
- Trigger table: TAGPROVIDERSETTINGS
- Duration: 0 ms
- Migrated tables:
- GanTagProviderSettings
- TagProviderSettings
- **Extension Point Cleanup (TagProviderSettings)**
- Trigger table: TAGPROVIDERSETTINGS
- Duration: 0 ms
- Migrated tables:
- TagProviderSettings
- **No-Op Migrator**
- Trigger table: AUDITEVENTS
- Duration: 0 ms
- Migrated tables:
- AuditEvents
- AuditProfileProperties_Local
- **Audit Profile (DATASOURCE) Migrator**
- Trigger table: AUDITPROFILES
- Duration: 1 ms
- Migrated tables:
- AuditProfileProperties_Datasource
- AuditProfiles
- **Audit Profile (EDGE) Migrator**
- Trigger table: AUDITPROFILES
- Duration: 0 ms
- Migrated tables:
- AuditProfiles
- **Audit Profile (LOCAL) Migrator**
- Trigger table: AUDITPROFILES
- Duration: 0 ms
- Migrated tables:
- AuditProfiles
- **Audit Profile (INTERNAL) Migrator**
- Trigger table: AUDITPROFILES
- Duration: 0 ms
- Migrated tables:
- AuditProfiles
- **Audit Profile (REMOTE) Migrator**
- Trigger table: AUDITPROFILES
- Duration: 0 ms
- Migrated tables:
- AuditProfiles
- RemoteAuditProfileProperties
- **Extension Point Cleanup (AuditProfiles)**
- Trigger table: AUDITPROFILES
- Duration: 0 ms
- Migrated tables:
- AuditProfiles
- **Remote Tag Sync settings Migrator**
- Trigger table: EDGESYNCSETTINGS
- Duration: 2 ms
- Migrated tables:
- EdgeSyncSettings
- **Alarm Notification Profile (AirlinkSms) Migrator**
- Trigger table: ALARMNOTIFICATIONPROFILES
- Duration: 1 ms
- Migrated tables:
- AlarmNotificationProfiles
- SmsNotificationProfileSettings
- **Alarm Notification Profile (SipNotification) Migrator**
- Trigger table: ALARMNOTIFICATIONPROFILES
- Duration: 0 ms
- Migrated tables:
- AlarmNotificationProfiles
- SipNotificationSettings
- **Alarm Notification Profile (EmailNotificationProfileType) Migrator**
- Trigger table: ALARMNOTIFICATIONPROFILES
- Duration: 0 ms
- Migrated tables:
- AlarmNotificationProfiles
- EmailNotificationSettings
- **Alarm Notification Profile (OneWayNotificationProfileType) Migrator**
- Trigger table: ALARMNOTIFICATIONPROFILES
- Duration: 0 ms
- Migrated tables:
- AlarmNotificationProfiles
- OneWayNotificationSettings
- **Alarm Notification Profile (RemoteNotificationProfileType) Migrator**
- Trigger table: ALARMNOTIFICATIONPROFILES
- Duration: 0 ms
- Migrated tables:
- AlarmNotificationProfiles
- RemoteNotificationSettings
- **Extension Point Cleanup (AlarmNotificationProfiles)**
- Trigger table: ALARMNOTIFICATIONPROFILES
- Duration: 0 ms
- Migrated tables:
- AlarmNotificationProfiles
- **User Source (ADHYBRID) Migrator**
- Trigger table: AUTHPROFILEPROPERTIES_ADHYBRID
- Duration: 2 ms
- Migrated tables:
- AuthProfileProperties_ADHybrid
- Messages:
- All records of type 'AuthProfileProperties_ADHybrid' have been migrated
- **EAM Task Configuration Migrator**
- Trigger table: GATEWAYTASKRECORD
- Duration: 0 ms
- Migrated tables:
- eambackuptasksettings
- gatewaytaskrecord
- **EAM Task Configuration Migrator**
- Trigger table: GATEWAYTASKRECORD
- Duration: 0 ms
- Migrated tables:
- eamrestorebackupsettings
- gatewaytaskrecord
- **EAM Task Configuration Migrator**
- Trigger table: GATEWAYTASKRECORD
- Duration: 0 ms
- Migrated tables:
- eamactivatelicensesettings
- gatewaytaskrecord
- **EAM Task Configuration Migrator**
- Trigger table: GATEWAYTASKRECORD
- Duration: 0 ms
- Migrated tables:
- eamtasksettings
- gatewaytaskrecord
- **EAM Task Configuration Migrator**
- Trigger table: GATEWAYTASKRECORD
- Duration: 0 ms
- Migrated tables:
- eamtasksettings
- gatewaytaskrecord
- **EAM Task Configuration Migrator**
- Trigger table: GATEWAYTASKRECORD
- Duration: 0 ms
- Migrated tables:
- eaminstallmodulesettings
- gatewaytaskrecord
- **EAM Task Configuration Migrator**
- Trigger table: GATEWAYTASKRECORD
- Duration: 0 ms
- Migrated tables:
- eamsendtagssettings
- gatewaytaskrecord
- **EAM Task Configuration Migrator**
- Trigger table: GATEWAYTASKRECORD
- Duration: 1 ms
- Migrated tables:
- eamtasksettings
- gatewaytaskrecord
- **EAM Task Configuration Migrator**
- Trigger table: GATEWAYTASKRECORD
- Duration: 0 ms
- Migrated tables:
- eamtasksettings
- gatewaytaskrecord
- **EAM Task Configuration Migrator**
- Trigger table: GATEWAYTASKRECORD
- Duration: 0 ms
- Migrated tables:
- eamtasksettings
- gatewaytaskrecord
- **EAM Task Configuration Migrator**
- Trigger table: GATEWAYTASKRECORD
- Duration: 0 ms
- Migrated tables:
- eamtasksettings
- gatewaytaskrecord
- **Database Translator Migrator**
- Trigger table: DBTRANSLATORS
- Duration: 2 ms
- Migrated tables:
- DBTRANSLATORS
- **No-Op Migrator**
- Trigger table: ALERTNOTIFICATIONPROFILES
- Duration: 0 ms
- Migrated tables:
- AlertLog
- AlertNotificationProfileProperties_Basic
- AlertNotificationProfileProperties_Basic_EmailAddresses
- AlertNotificationProfileProperties_Distribution
- AlertNotificationProfiles
- AlertStorageProfileProperties_Datasource
- AlertStorageProfiles
- **Keyboard Layout Migrator**
- Trigger table: KEYBOARDLAYOUTS
- Duration: 0 ms
- Migrated tables:
- KeyboardLayouts
- **Schedule (basic schedule) Migrator**
- Trigger table: SCHEDULE_PROFILES
- Duration: 1 ms
- Migrated tables:
- BASIC_SCHEDULES
- SCHEDULE_PROFILES
- **Schedule (composite schedule) Migrator**
- Trigger table: SCHEDULE_PROFILES
- Duration: 0 ms
- Migrated tables:
- COMPOSITE_SCHEDULES
- SCHEDULE_PROFILES
- **Extension Point Cleanup (SCHEDULE_PROFILES)**
- Trigger table: SCHEDULE_PROFILES
- Duration: 0 ms
- Migrated tables:
- SCHEDULE_PROFILES
- **Translations Migrator**
- Trigger table: TRANSLATIONSETTINGS
- Duration: 11 ms
- Migrated tables:
- REGIONSETTINGS
- TranslationSettings
- TranslationTerms
- New Resources:
- ignition/translations, collection=core, size=0 bytes
- **Device (TCPDriver) Migrator**
- Trigger table: DEVICESETTINGS
- Duration: 2 ms
- Migrated tables:
- DeviceSettings
- TCPDriverSettings
- **Device (UDPDriver) Migrator**
- Trigger table: DEVICESETTINGS
- Duration: 0 ms
- Migrated tables:
- DeviceSettings
- UDPDriverSettings
- **Device (com.inductiveautomation.FinsTcpDeviceType) Migrator**
- Trigger table: DEVICESETTINGS
- Duration: 0 ms
- Migrated tables:
- DeviceSettings
- FinsTcpDeviceSettings
- **Device (com.inductiveautomation.FinsUdpDeviceType) Migrator**
- Trigger table: DEVICESETTINGS
- Duration: 0 ms
- Migrated tables:
- DeviceSettings
- FinsUdpDeviceSettings
- **Device (com.inductiveautomation.omron.NjDriver) Migrator**
- Trigger table: DEVICESETTINGS
- Duration: 0 ms
- Migrated tables:
- DeviceSettings
- NjDriverSettings
- **Device (com.inductiveautomation.BacnetIpDeviceType) Migrator**
- Trigger table: DEVICESETTINGS
- Duration: 0 ms
- Migrated tables:
- BacnetIpRemoteDeviceSettings
- DeviceSettings
- **Device (S7300) Migrator**
- Trigger table: DEVICESETTINGS
- Duration: 0 ms
- Migrated tables:
- DeviceSettings
- S7300DriverSettings
- **Device (S7400) Migrator**
- Trigger table: DEVICESETTINGS
- Duration: 0 ms
- Migrated tables:
- DeviceSettings
- S7400DriverSettings
- **Device (S71200) Migrator**
- Trigger table: DEVICESETTINGS
- Duration: 1 ms
- Migrated tables:
- DeviceSettings
- S71200DriverSettings
- **Device (S71500) Migrator**
- Trigger table: DEVICESETTINGS
- Duration: 0 ms
- Migrated tables:
- DeviceSettings
- S71500DriverSettings
- **Device (com.inductiveautomation.MitsubishiTcpDeviceType) Migrator**
- Trigger table: DEVICESETTINGS
- Duration: 0 ms
- Migrated tables:
- DeviceSettings
- MitsubishiTcpDeviceSettings
- **Device (com.inductiveautomation.Micro800DeviceType) Migrator**
- Trigger table: DEVICESETTINGS
- Duration: 0 ms
- Migrated tables:
- DeviceSettings
- Micro800DeviceSettings
- **Device (ModbusTcp) Migrator**
- Trigger table: DEVICESETTINGS
- Duration: 0 ms
- Migrated tables:
- DeviceSettings
- ModbusTcpDriverSettings
- **Device (ModbusRtuOverTcp) Migrator**
- Trigger table: DEVICESETTINGS
- Duration: 0 ms
- Migrated tables:
- DeviceSettings
- ModbusTcpDriverSettings
- **Device (ModbusRtu) Migrator**
- Trigger table: DEVICESETTINGS
- Duration: 0 ms
- Migrated tables:
- DeviceSettings
- ModbusRtuDriverSettings
- **Device (ControlLogix) Migrator**
- Trigger table: DEVICESETTINGS
- Duration: 0 ms
- Migrated tables:
- ControlLogixDriverSettings
- DeviceSettings
- **Device (CompactLogix) Migrator**
- Trigger table: DEVICESETTINGS
- Duration: 1 ms
- Migrated tables:
- CompactLogixDriverSettings
- DeviceSettings
- **Device (MicroLogix) Migrator**
- Trigger table: DEVICESETTINGS
- Duration: 0 ms
- Migrated tables:
- DeviceSettings
- MicroLogixDriverSettings
- **Device (PLC5) Migrator**
- Trigger table: DEVICESETTINGS
- Duration: 0 ms
- Migrated tables:
- DeviceSettings
- PLC5DriverSettings
- **Device (SLC) Migrator**
- Trigger table: DEVICESETTINGS
- Duration: 0 ms
- Migrated tables:
- DeviceSettings
- SLCDriverSettings
- **Device (com.inductiveautomation.Iec61850DeviceType) Migrator**
- Trigger table: DEVICESETTINGS
- Duration: 0 ms
- Migrated tables:
- DeviceSettings
- Iec61850DeviceSettings
- **Device (DairyDemoSimulator) Migrator**
- Trigger table: DEVICESETTINGS
- Duration: 0 ms
- Migrated tables:
- DeviceSettings
- **Device (Simulator) Migrator**
- Trigger table: DEVICESETTINGS
- Duration: 0 ms
- Migrated tables:
- DeviceSettings
- **Device (SLCSimulator) Migrator**
- Trigger table: DEVICESETTINGS
- Duration: 0 ms
- Migrated tables:
- DeviceSettings
- **Device (ProgrammableSimulatorDevice) Migrator**
- Trigger table: DEVICESETTINGS
- Duration: 0 ms
- Migrated tables:
- DeviceSettings
- ProgrammableSimSettings
- **Device (Dnp3Driver) Migrator**
- Trigger table: DEVICESETTINGS
- Duration: 0 ms
- Migrated tables:
- DeviceSettings
- Dnp3DriverSettings
- **Device (LogixDriver) Migrator**
- Trigger table: DEVICESETTINGS
- Duration: 0 ms
- Migrated tables:
- DeviceSettings
- LogixDriverSettings
- **Device (com.inductiveautomation.Dnp3DeviceType) Migrator**
- Trigger table: DEVICESETTINGS
- Duration: 0 ms
- Migrated tables:
- DeviceSettings
- Dnp3DeviceSettings
- **Module Certificates Migrator**
- Trigger table: CERTIFICATES
- Duration: 0 ms
- Migrated tables:
- CERTIFICATES
- **CoBranding Migrator**
- Trigger table: COBRANDING
- Duration: 0 ms
- Migrated tables:
- CoBranding
- **TagHistoryProviderEP (EdgeHistorian) Migrator**
- Trigger table: TAGHISTORYPROVIDEREP
- Duration: 0 ms
- Migrated tables:
- TagHistoryProviderEP
- edgeHistorianSettings
- **TagHistoryProviderEP (RemoteHistorian) Migrator**
- Trigger table: TAGHISTORYPROVIDEREP
- Duration: 0 ms
- Migrated tables:
- TagHistoryProviderEP
- remotehistoriansettings
- **TagHistoryProviderEP (SplittingProvider) Migrator**
- Trigger table: TAGHISTORYPROVIDEREP
- Duration: 0 ms
- Migrated tables:
- TagHistoryProviderEP
- TagHistorySplitterSettings
- **TagHistoryProviderEP (widedb) Migrator**
- Trigger table: TAGHISTORYPROVIDEREP
- Duration: 0 ms
- Migrated tables:
- TagHistoryProviderEP
- WideDbHistorianProviderSettings
- **TagHistoryProviderEP (history_sim) Migrator**
- Trigger table: TAGHISTORYPROVIDEREP
- Duration: 0 ms
- Migrated tables:
- TagHistoryProviderEP
- **TagHistoryProviderEP (CsvHistorian) Migrator**
- Trigger table: TAGHISTORYPROVIDEREP
- Duration: 0 ms
- Migrated tables:
- TagHistoryProviderEP
- csvHistorianSettings
- **TagHistoryProviderEP (datasource) Migrator**
- Trigger table: TAGHISTORYPROVIDEREP
- Duration: 0 ms
- Migrated tables:
- TagHistorianProviderSettings
- TagHistoryProviderEP
- **No-Op Migrator**
- Trigger table: SCHEDULES
- Duration: 0 ms
- Migrated tables:
- SCHEDULES
- **GeneralAlarmSettings Migrator**
- Trigger table: GENERALALARMSETTINGS
- Duration: 1 ms
- Migrated tables:
- GeneralAlarmSettings
- **EamControllerSettings Migrator**
- Trigger table: EAMCONTROLLERSETTINGS
- Duration: 0 ms
- Migrated tables:
- EamControllerSettings
- **Saved Tag Reports Migrator**
- Trigger table: TAGREPORTSEARCH
- Duration: 0 ms
- Migrated tables:
- TagReportSearch
- **Holiday Migrator**
- Trigger table: HOLIDAYS
- Duration: 1 ms
- Migrated tables:
- HOLIDAYS
- **User Source (DATASOURCE) Migrator**
- Trigger table: AUTHPROFILEPROPERTIES_DB
- Duration: 2 ms
- Migrated tables:
- AuthProfileProperties_DB
- Messages:
- All records of type 'AuthProfileProperties_DB' have been migrated
- **No-Op Migrator**
- Trigger table: IEC61850REPORTENTRYIDS
- Duration: 0 ms
- Migrated tables:
- Iec61850ReportEntryIds
- **Security Zones Migrator**
- Trigger table: SECURITYZONES
- Duration: 0 ms
- Migrated tables:
- SecuredEntityPolicy
- SecuredEntityZonePolicy
- securityZones
- **Ephemeral User Data No-Op Migrator**
- Trigger table: REMEMBERED_SUBJECTS
- Duration: 0 ms
- Migrated tables:
- IDP_ADAPTER_USER_HISTORY
- IDP_ADAPTER_USER_HISTORY_BK
- REMEMBERED_SUBJECTS
- REMEMBERED_SUBJECTS_BK
- **Quick Start State Migrator**
- Trigger table: STARTERSTEPS
- Duration: 21 ms
- Migrated tables:
- startersteps
- New Resources:
- ignition/quickstart, collection=core, size=214 bytes
- **OAuth2ClientSettings Migrator**
- Trigger table: OAUTH2CLIENTSETTINGS
- Duration: 0 ms
- Migrated tables:
- OAuth2ClientSettings
- **Alarm Journal Profile (LOCAL) Migrator**
- Trigger table: ALARMJOURNALS
- Duration: 1 ms
- Migrated tables:
- AlarmJournals
- LocalJournalSettings
- **Alarm Journal Profile (DATASOURCE) Migrator**
- Trigger table: ALARMJOURNALS
- Duration: 0 ms
- Migrated tables:
- AlarmJournals
- DatabaseJournalSettings
- **Alarm Journal Profile (REMOTE) Migrator**
- Trigger table: ALARMJOURNALS
- Duration: 0 ms
- Migrated tables:
- AlarmJournals
- RemoteJournalSettings
- **Extension Point Cleanup (AlarmJournals)**
- Trigger table: ALARMJOURNALS
- Duration: 0 ms
- Migrated tables:
- AlarmJournals
- **EULAS Migrator**
- Trigger table: EULAS
- Duration: 0 ms
- Migrated tables:
- EULAS
- **sfcsettings Migrator**
- Trigger table: SFCSETTINGS
- Duration: 0 ms
- Migrated tables:
- sfcsettings
- **RemoteAgent Migrator**
- Trigger table: REMOTEAGENT
- Duration: 0 ms
- Migrated tables:
- RemoteAgent
- **AgentGroup Migrator**
- Trigger table: AGENTGROUP
- Duration: 0 ms
- Migrated tables:
- AgentGroup
- **OPC Connection (com.inductiveautomation.OpcUaServerType) Migrator**
- Trigger table: OPCSERVERS
- Duration: 2 ms
- Migrated tables:
- OPCServers
- OpcUaConnectionSettings
- **Local Device Migrator**
- Trigger table: BACNETIPLOCALDEVICESETTINGS
- Duration: 1 ms
- Migrated tables:
- BacnetIpLocalDeviceSettings
- **Email Profile (smtp.oauth2) Migrator**
- Trigger table: EMAILPROFILES
- Duration: 1 ms
- Migrated tables:
- EmailProfiles
- OAuth2SmtpEmailProfiles
- **Email Profile (smtp.classic) Migrator**
- Trigger table: EMAILPROFILES
- Duration: 0 ms
- Migrated tables:
- ClassicSmtpEmailProfiles
- EmailProfiles
- **No-Op Migrator**
- Trigger table: DEVICES
- Duration: 0 ms
- Migrated tables:
- Devices
- DriverProperties
- OpcUaConnectionSettings2
- UAConnectionSettings
- XOPCSettings
- **MetricsDashboards Migrator**
- Trigger table: METRICSDASHBOARDS
- Duration: 0 ms
- Migrated tables:
- MetricsDashboards
- **Tag Configuration Migrator**
- Trigger table: TAGCONFIG
- Duration: 8 ms
- Migrated tables:
- tagconfig
- **EamLicense Migrator**
- Trigger table: EAMLICENSE
- Duration: 0 ms
- Migrated tables:
- EamLicense
- **Security Levels Migrator**
- Trigger table: SECURITY_LEVELS
- Duration: 0 ms
- Migrated tables:
- SECURITY_LEVELS
- **EAM Event Threshold Configuration Migrator**
- Trigger table: EVENTTHRESHOLD
- Duration: 0 ms
- Migrated tables:
- EventThreshold
- **EAM Remote Upgrade Configuration Migrator**
- Trigger table: REMOTEUPGRADE
- Duration: 0 ms
- Migrated tables:
- remoteupgrade
- **Tag Group Configuration Migrator**
- Trigger table: TAGGROUPS
- Duration: 0 ms
- Migrated tables:
- taggroups
- **Managed Tag Provider Migrator**
- Trigger table: SIMPLETAGPROVIDERPROFILE
- Duration: 0 ms
- Migrated tables:
- SimpleTagProviderProfile
- **User Source (AD_DB_HYBRID) Migrator**
- Trigger table: AUTHPROFILEPROPERTIES_ADTODB
- Duration: 5 ms
- Migrated tables:
- AuthProfileProperties_ADtoDB
- Messages:
- All records of type 'AuthProfileProperties_ADtoDB' have been migrated
- **User Source (ADEASY) Migrator**
- Trigger table: AUTHPROFILEPROPERTIES_AD
- Duration: 1 ms
- Migrated tables:
- AuthProfileProperties_AD
- Messages:
- All records of type 'AuthProfileProperties_AD' have been migrated
- **EamAgentSettings Migrator**
- Trigger table: EAMAGENTSETTINGS
- Duration: 0 ms
- Migrated tables:
- EamAgentSettings
- **Image Migrator**
- Trigger table: IMAGES
- Duration: 1 ms
- Migrated tables:
- Images
- **EAM Task History Migrator**
- Trigger table: TASKEVENTLOG
- Duration: 1 ms
- Migrated tables:
- TaskEventLog
- **JDBC Driver Migrator**
- Trigger table: JDBCDRIVERS
- Duration: 0 ms
- Migrated tables:
- JDBCDRIVERS
- **Edge System Properties Migrator**
- Trigger table: EDGESYSTEMPROPS
- Duration: 0 ms
- Migrated tables:
- EdgeSystemProps
- **Call Script Migrator**
- Trigger table: SCRIPTSETTINGSRECORD
- Duration: 0 ms
- Migrated tables:
- ScriptSettingsRecord
- **Roster Migrator**
- Trigger table: ROSTER
- Duration: 0 ms
- Migrated tables:
- ROSTER
- ROSTER_ENTRY
- **User Source (FALLBK_CACHE) Migrator**
- Trigger table: AUTHPROFILEPROPERTIES_LOCALFALLBACKCACHE
- Duration: 1 ms
- Migrated tables:
- AuthProfileProperties_LocalFallbackCache
- Messages:
- All records of type 'AuthProfileProperties_LocalFallbackCache' have been migrated
- **Store And Forward Migrator**
- Trigger table: STOREANDFORWARDSYSSETTINGS
- Duration: 1 ms
- Migrated tables:
- StoreAndForwardSysSettings
- **DATASOURCES Migrator**
- Trigger table: DATASOURCES
- Duration: 0 ms
- Migrated tables:
- DATASOURCES
- **Identity Provider Migrator**
- Trigger table: IDP_ADAPTERS
- Duration: 1 ms
- Migrated tables:
- IDP_ADAPTERS
- **OPC UA Server Configuration Migrator**
- Trigger table: OPCUASERVERSETTINGS
- Duration: 0 ms
- Migrated tables:
- OpcUaServerSettings
- **com.inductiveautomation.ignition.gateway.alarming.journal.EdgeJournalSettingsMigrationStrategy@1062a775**
- Trigger table: EDGEJOURNALSETTINGS
- Duration: 0 ms
- Migrated tables:
- EdgeJournalSettings
- **No-Op Migrator**
- Trigger table: LOCALFALLBACKCACHE_ENTRY
- Duration: 0 ms
- Migrated tables:
- LocalFallbackCache_Entry
- **System Properties Migrator**
- Trigger table: SYSPROPS
- Duration: 0 ms
- Migrated tables:
- SysProps

View File

@@ -0,0 +1,917 @@
# Migration Log
Timestamp: Jul 16, 2026, 11:18:51 AM
Duration: 175 ms
## Summary
210 tables migrated from `/data/db/config.idb`, creating 0 new resources in `/usr/local/bin/ignition/data/config/resources`.
3 migration strategies were unable to complete due to errors.
3 tables remain to be migrated.
## Migration Details
### Tables Migrated
- AgentGroup
- AlarmJournals
- AlarmNotificationProfiles
- AlertLog
- AlertNotificationProfileProperties_Basic
- AlertNotificationProfileProperties_Basic_EmailAddresses
- AlertNotificationProfileProperties_Distribution
- AlertNotificationProfiles
- AlertStorageProfileProperties_Datasource
- AlertStorageProfiles
- AuditEvents
- AuditProfileProperties_Datasource
- AuditProfileProperties_Local
- AuditProfiles
- AuthProfileProperties_AD
- AuthProfileProperties_ADHybrid
- AuthProfileProperties_ADtoDB
- AuthProfileProperties_DB
- AuthProfileProperties_Internal
- AuthProfileProperties_LocalFallbackCache
- AuthProfiles
- BASIC_SCHEDULES
- BacnetIpLocalDeviceSettings
- BacnetIpRemoteDeviceSettings
- CERTIFICATES
- COMPOSITE_SCHEDULES
- ClassicSmtpEmailProfiles
- CoBranding
- CompactLogixDriverSettings
- ControlLogixDriverSettings
- DATASOURCES
- DBTRANSLATORS
- DatabaseJournalSettings
- DeviceSettings
- Devices
- Dnp3DeviceSettings
- Dnp3DriverSettings
- DriverProperties
- EULAS
- EamAgentSettings
- EamControllerSettings
- EamLicense
- EdgeJournalSettings
- EdgeSyncSettings
- EdgeSystemProps
- EmailNotificationSettings
- EmailProfiles
- EventThreshold
- FinsTcpDeviceSettings
- FinsUdpDeviceSettings
- GanTagProviderSettings
- GeneralAlarmSettings
- HOLIDAYS
- IDP_ADAPTERS
- IDP_ADAPTER_USER_HISTORY
- IDP_ADAPTER_USER_HISTORY_BK
- Iec61850DeviceSettings
- Iec61850ReportEntryIds
- Images
- InternalAuthMappingTable
- InternalContactInfoTable
- InternalRoleTable
- InternalScheduleAdjustmentTable
- InternalTagProvider
- InternalUserExtraProps
- InternalUserTable
- JDBCDRIVERS
- KeyboardLayouts
- LocalFallbackCache_Entry
- LocalJournalSettings
- LogixDriverSettings
- MetricsDashboards
- Micro800DeviceSettings
- MicroLogixDriverSettings
- MitsubishiTcpDeviceSettings
- ModbusRtuDriverSettings
- ModbusTcpDriverSettings
- NjDriverSettings
- OAuth2ClientSettings
- OAuth2SmtpEmailProfiles
- OPCServers
- OneWayNotificationSettings
- OpcUaConnectionSettings
- OpcUaConnectionSettings2
- OpcUaServerSettings
- PLC5DriverSettings
- ProgrammableSimSettings
- REGIONSETTINGS
- REMEMBERED_SUBJECTS
- REMEMBERED_SUBJECTS_BK
- ROSTER
- ROSTER_ENTRY
- RemoteAgent
- RemoteAuditProfileProperties
- RemoteJournalSettings
- RemoteNotificationSettings
- S71200DriverSettings
- S71500DriverSettings
- S7300DriverSettings
- S7400DriverSettings
- SCHEDULES
- SCHEDULE_PROFILES
- SECURITY_LEVELS
- SLCDriverSettings
- ScriptSettingsRecord
- SecuredEntityPolicy
- SecuredEntityZonePolicy
- SimpleTagProviderProfile
- SipNotificationSettings
- SmsNotificationProfileSettings
- StoreAndForwardSysSettings
- SysProps
- TCPDriverSettings
- TagHistorianProviderSettings
- TagHistoryProviderEP
- TagHistorySplitterSettings
- TagProviderSettings
- TagReportSearch
- TaskEventLog
- TranslationSettings
- TranslationTerms
- UAConnectionSettings
- UDPDriverSettings
- WSChannelSettings
- WideDbHistorianProviderSettings
- XOPCSettings
- csvHistorianSettings
- eamactivatelicensesettings
- eambackuptasksettings
- eaminstallmodulesettings
- eamrestorebackupsettings
- eamsendtagssettings
- eamtasksettings
- edgeHistorianSettings
- gatewaytaskrecord
- remotehistoriansettings
- remoteupgrade
- securityZones
- sfcsettings
- tagconfig
- taggroups
- wsconnectionsettings
- wsincomingconnection
### Strategies Executed
- **Gateway Network Websocket Channel Settings Migrator**
- Trigger table: WSCHANNELSETTINGS
- Duration: 0 ms
- Migrated tables:
- WSChannelSettings
- **Gateway Network Websocket Connection Settings Migrator**
- Trigger table: WSCONNECTIONSETTINGS
- Duration: 0 ms
- Migrated tables:
- wsconnectionsettings
- **Gateway Network Connection Migrator**
- Trigger table: WSINCOMINGCONNECTION
- Duration: 0 ms
- Migrated tables:
- wsincomingconnection
- **Gateway Network Queue Settings Migrator**
- Trigger table: WSQUEUE_OVERRIDES
- Duration: 8 ms
- Error: com.inductiveautomation.ignition.common.resourcecollection.PushConflictException: CREATE conflict: 'ResourceId{resourcePath=ignition/gateway-network-queue-settings, collectionName=core}' already exists
- **Gateway Network Proxy Configuration Migrator**
- Trigger table: WS_PROXYRULES
- Duration: 1 ms
- Error: com.inductiveautomation.ignition.common.resourcecollection.PushConflictException: CREATE conflict: 'ResourceId{resourcePath=ignition/gateway-network-proxy-rules, collectionName=core}' already exists
- **User Source (INTERNAL) Migrator**
- Trigger table: AUTHPROFILEPROPERTIES_INTERNAL
- Duration: 12 ms
- Migrated tables:
- AuthProfileProperties_Internal
- AuthProfiles
- InternalAuthMappingTable
- InternalContactInfoTable
- InternalRoleTable
- InternalScheduleAdjustmentTable
- InternalUserExtraProps
- InternalUserTable
- Messages:
- All records of type 'AuthProfileProperties_Internal' have been migrated
- All records of type 'AuthProfiles' have been migrated
- All records of type 'InternalUserTable' have been migrated
- All records of type 'InternalRoleTable' have been migrated
- All records of type 'InternalUserExtraProps' have been migrated
- All records of type 'InternalAuthMappingTable' have been migrated
- All records of type 'InternalContactInfoTable' have been migrated
- All records of type 'InternalScheduleAdjustmentTable' have been migrated
- **Realtime Tag Provider (STANDARD) Migrator**
- Trigger table: TAGPROVIDERSETTINGS
- Duration: 1 ms
- Migrated tables:
- InternalTagProvider
- TagProviderSettings
- **Realtime Tag Provider (gantagprovider) Migrator**
- Trigger table: TAGPROVIDERSETTINGS
- Duration: 0 ms
- Migrated tables:
- GanTagProviderSettings
- TagProviderSettings
- **Extension Point Cleanup (TagProviderSettings)**
- Trigger table: TAGPROVIDERSETTINGS
- Duration: 0 ms
- Migrated tables:
- TagProviderSettings
- **No-Op Migrator**
- Trigger table: AUDITEVENTS
- Duration: 0 ms
- Migrated tables:
- AuditEvents
- AuditProfileProperties_Local
- **Audit Profile (DATASOURCE) Migrator**
- Trigger table: AUDITPROFILES
- Duration: 0 ms
- Migrated tables:
- AuditProfileProperties_Datasource
- AuditProfiles
- **Audit Profile (EDGE) Migrator**
- Trigger table: AUDITPROFILES
- Duration: 0 ms
- Migrated tables:
- AuditProfiles
- **Audit Profile (LOCAL) Migrator**
- Trigger table: AUDITPROFILES
- Duration: 0 ms
- Migrated tables:
- AuditProfiles
- **Audit Profile (INTERNAL) Migrator**
- Trigger table: AUDITPROFILES
- Duration: 0 ms
- Migrated tables:
- AuditProfiles
- **Audit Profile (REMOTE) Migrator**
- Trigger table: AUDITPROFILES
- Duration: 0 ms
- Migrated tables:
- AuditProfiles
- RemoteAuditProfileProperties
- **Extension Point Cleanup (AuditProfiles)**
- Trigger table: AUDITPROFILES
- Duration: 0 ms
- Migrated tables:
- AuditProfiles
- **Remote Tag Sync settings Migrator**
- Trigger table: EDGESYNCSETTINGS
- Duration: 0 ms
- Migrated tables:
- EdgeSyncSettings
- **Alarm Notification Profile (AirlinkSms) Migrator**
- Trigger table: ALARMNOTIFICATIONPROFILES
- Duration: 0 ms
- Migrated tables:
- AlarmNotificationProfiles
- SmsNotificationProfileSettings
- **Alarm Notification Profile (SipNotification) Migrator**
- Trigger table: ALARMNOTIFICATIONPROFILES
- Duration: 0 ms
- Migrated tables:
- AlarmNotificationProfiles
- SipNotificationSettings
- **Alarm Notification Profile (EmailNotificationProfileType) Migrator**
- Trigger table: ALARMNOTIFICATIONPROFILES
- Duration: 0 ms
- Migrated tables:
- AlarmNotificationProfiles
- EmailNotificationSettings
- **Alarm Notification Profile (OneWayNotificationProfileType) Migrator**
- Trigger table: ALARMNOTIFICATIONPROFILES
- Duration: 0 ms
- Migrated tables:
- AlarmNotificationProfiles
- OneWayNotificationSettings
- **Alarm Notification Profile (RemoteNotificationProfileType) Migrator**
- Trigger table: ALARMNOTIFICATIONPROFILES
- Duration: 0 ms
- Migrated tables:
- AlarmNotificationProfiles
- RemoteNotificationSettings
- **Extension Point Cleanup (AlarmNotificationProfiles)**
- Trigger table: ALARMNOTIFICATIONPROFILES
- Duration: 0 ms
- Migrated tables:
- AlarmNotificationProfiles
- **User Source (ADHYBRID) Migrator**
- Trigger table: AUTHPROFILEPROPERTIES_ADHYBRID
- Duration: 2 ms
- Migrated tables:
- AuthProfileProperties_ADHybrid
- Messages:
- All records of type 'AuthProfileProperties_ADHybrid' have been migrated
- **EAM Task Configuration Migrator**
- Trigger table: GATEWAYTASKRECORD
- Duration: 0 ms
- Migrated tables:
- eambackuptasksettings
- gatewaytaskrecord
- **EAM Task Configuration Migrator**
- Trigger table: GATEWAYTASKRECORD
- Duration: 0 ms
- Migrated tables:
- eamrestorebackupsettings
- gatewaytaskrecord
- **EAM Task Configuration Migrator**
- Trigger table: GATEWAYTASKRECORD
- Duration: 0 ms
- Migrated tables:
- eamactivatelicensesettings
- gatewaytaskrecord
- **EAM Task Configuration Migrator**
- Trigger table: GATEWAYTASKRECORD
- Duration: 0 ms
- Migrated tables:
- eamtasksettings
- gatewaytaskrecord
- **EAM Task Configuration Migrator**
- Trigger table: GATEWAYTASKRECORD
- Duration: 0 ms
- Migrated tables:
- eamtasksettings
- gatewaytaskrecord
- **EAM Task Configuration Migrator**
- Trigger table: GATEWAYTASKRECORD
- Duration: 0 ms
- Migrated tables:
- eaminstallmodulesettings
- gatewaytaskrecord
- **EAM Task Configuration Migrator**
- Trigger table: GATEWAYTASKRECORD
- Duration: 0 ms
- Migrated tables:
- eamsendtagssettings
- gatewaytaskrecord
- **EAM Task Configuration Migrator**
- Trigger table: GATEWAYTASKRECORD
- Duration: 0 ms
- Migrated tables:
- eamtasksettings
- gatewaytaskrecord
- **EAM Task Configuration Migrator**
- Trigger table: GATEWAYTASKRECORD
- Duration: 0 ms
- Migrated tables:
- eamtasksettings
- gatewaytaskrecord
- **EAM Task Configuration Migrator**
- Trigger table: GATEWAYTASKRECORD
- Duration: 0 ms
- Migrated tables:
- eamtasksettings
- gatewaytaskrecord
- **EAM Task Configuration Migrator**
- Trigger table: GATEWAYTASKRECORD
- Duration: 0 ms
- Migrated tables:
- eamtasksettings
- gatewaytaskrecord
- **Database Translator Migrator**
- Trigger table: DBTRANSLATORS
- Duration: 1 ms
- Migrated tables:
- DBTRANSLATORS
- **No-Op Migrator**
- Trigger table: ALERTNOTIFICATIONPROFILES
- Duration: 0 ms
- Migrated tables:
- AlertLog
- AlertNotificationProfileProperties_Basic
- AlertNotificationProfileProperties_Basic_EmailAddresses
- AlertNotificationProfileProperties_Distribution
- AlertNotificationProfiles
- AlertStorageProfileProperties_Datasource
- AlertStorageProfiles
- **Keyboard Layout Migrator**
- Trigger table: KEYBOARDLAYOUTS
- Duration: 0 ms
- Migrated tables:
- KeyboardLayouts
- **Schedule (basic schedule) Migrator**
- Trigger table: SCHEDULE_PROFILES
- Duration: 0 ms
- Migrated tables:
- BASIC_SCHEDULES
- SCHEDULE_PROFILES
- **Schedule (composite schedule) Migrator**
- Trigger table: SCHEDULE_PROFILES
- Duration: 0 ms
- Migrated tables:
- COMPOSITE_SCHEDULES
- SCHEDULE_PROFILES
- **Extension Point Cleanup (SCHEDULE_PROFILES)**
- Trigger table: SCHEDULE_PROFILES
- Duration: 0 ms
- Migrated tables:
- SCHEDULE_PROFILES
- **Translations Migrator**
- Trigger table: TRANSLATIONSETTINGS
- Duration: 2 ms
- Migrated tables:
- REGIONSETTINGS
- TranslationSettings
- TranslationTerms
- **Device (TCPDriver) Migrator**
- Trigger table: DEVICESETTINGS
- Duration: 1 ms
- Migrated tables:
- DeviceSettings
- TCPDriverSettings
- **Device (UDPDriver) Migrator**
- Trigger table: DEVICESETTINGS
- Duration: 0 ms
- Migrated tables:
- DeviceSettings
- UDPDriverSettings
- **Device (com.inductiveautomation.FinsTcpDeviceType) Migrator**
- Trigger table: DEVICESETTINGS
- Duration: 0 ms
- Migrated tables:
- DeviceSettings
- FinsTcpDeviceSettings
- **Device (com.inductiveautomation.FinsUdpDeviceType) Migrator**
- Trigger table: DEVICESETTINGS
- Duration: 0 ms
- Migrated tables:
- DeviceSettings
- FinsUdpDeviceSettings
- **Device (com.inductiveautomation.omron.NjDriver) Migrator**
- Trigger table: DEVICESETTINGS
- Duration: 0 ms
- Migrated tables:
- DeviceSettings
- NjDriverSettings
- **Device (com.inductiveautomation.BacnetIpDeviceType) Migrator**
- Trigger table: DEVICESETTINGS
- Duration: 0 ms
- Migrated tables:
- BacnetIpRemoteDeviceSettings
- DeviceSettings
- **Device (S7300) Migrator**
- Trigger table: DEVICESETTINGS
- Duration: 0 ms
- Migrated tables:
- DeviceSettings
- S7300DriverSettings
- **Device (S7400) Migrator**
- Trigger table: DEVICESETTINGS
- Duration: 0 ms
- Migrated tables:
- DeviceSettings
- S7400DriverSettings
- **Device (S71200) Migrator**
- Trigger table: DEVICESETTINGS
- Duration: 0 ms
- Migrated tables:
- DeviceSettings
- S71200DriverSettings
- **Device (S71500) Migrator**
- Trigger table: DEVICESETTINGS
- Duration: 0 ms
- Migrated tables:
- DeviceSettings
- S71500DriverSettings
- **Device (com.inductiveautomation.MitsubishiTcpDeviceType) Migrator**
- Trigger table: DEVICESETTINGS
- Duration: 0 ms
- Migrated tables:
- DeviceSettings
- MitsubishiTcpDeviceSettings
- **Device (com.inductiveautomation.Micro800DeviceType) Migrator**
- Trigger table: DEVICESETTINGS
- Duration: 0 ms
- Migrated tables:
- DeviceSettings
- Micro800DeviceSettings
- **Device (ModbusTcp) Migrator**
- Trigger table: DEVICESETTINGS
- Duration: 0 ms
- Migrated tables:
- DeviceSettings
- ModbusTcpDriverSettings
- **Device (ModbusRtuOverTcp) Migrator**
- Trigger table: DEVICESETTINGS
- Duration: 0 ms
- Migrated tables:
- DeviceSettings
- ModbusTcpDriverSettings
- **Device (ModbusRtu) Migrator**
- Trigger table: DEVICESETTINGS
- Duration: 0 ms
- Migrated tables:
- DeviceSettings
- ModbusRtuDriverSettings
- **Device (ControlLogix) Migrator**
- Trigger table: DEVICESETTINGS
- Duration: 0 ms
- Migrated tables:
- ControlLogixDriverSettings
- DeviceSettings
- **Device (CompactLogix) Migrator**
- Trigger table: DEVICESETTINGS
- Duration: 0 ms
- Migrated tables:
- CompactLogixDriverSettings
- DeviceSettings
- **Device (MicroLogix) Migrator**
- Trigger table: DEVICESETTINGS
- Duration: 0 ms
- Migrated tables:
- DeviceSettings
- MicroLogixDriverSettings
- **Device (PLC5) Migrator**
- Trigger table: DEVICESETTINGS
- Duration: 0 ms
- Migrated tables:
- DeviceSettings
- PLC5DriverSettings
- **Device (SLC) Migrator**
- Trigger table: DEVICESETTINGS
- Duration: 0 ms
- Migrated tables:
- DeviceSettings
- SLCDriverSettings
- **Device (com.inductiveautomation.Iec61850DeviceType) Migrator**
- Trigger table: DEVICESETTINGS
- Duration: 0 ms
- Migrated tables:
- DeviceSettings
- Iec61850DeviceSettings
- **Device (DairyDemoSimulator) Migrator**
- Trigger table: DEVICESETTINGS
- Duration: 0 ms
- Migrated tables:
- DeviceSettings
- **Device (Simulator) Migrator**
- Trigger table: DEVICESETTINGS
- Duration: 0 ms
- Migrated tables:
- DeviceSettings
- **Device (SLCSimulator) Migrator**
- Trigger table: DEVICESETTINGS
- Duration: 0 ms
- Migrated tables:
- DeviceSettings
- **Device (ProgrammableSimulatorDevice) Migrator**
- Trigger table: DEVICESETTINGS
- Duration: 0 ms
- Migrated tables:
- DeviceSettings
- ProgrammableSimSettings
- **Device (Dnp3Driver) Migrator**
- Trigger table: DEVICESETTINGS
- Duration: 0 ms
- Migrated tables:
- DeviceSettings
- Dnp3DriverSettings
- **Device (LogixDriver) Migrator**
- Trigger table: DEVICESETTINGS
- Duration: 0 ms
- Migrated tables:
- DeviceSettings
- LogixDriverSettings
- **Device (com.inductiveautomation.Dnp3DeviceType) Migrator**
- Trigger table: DEVICESETTINGS
- Duration: 0 ms
- Migrated tables:
- DeviceSettings
- Dnp3DeviceSettings
- **Module Certificates Migrator**
- Trigger table: CERTIFICATES
- Duration: 0 ms
- Migrated tables:
- CERTIFICATES
- **CoBranding Migrator**
- Trigger table: COBRANDING
- Duration: 0 ms
- Migrated tables:
- CoBranding
- **TagHistoryProviderEP (EdgeHistorian) Migrator**
- Trigger table: TAGHISTORYPROVIDEREP
- Duration: 0 ms
- Migrated tables:
- TagHistoryProviderEP
- edgeHistorianSettings
- **TagHistoryProviderEP (RemoteHistorian) Migrator**
- Trigger table: TAGHISTORYPROVIDEREP
- Duration: 0 ms
- Migrated tables:
- TagHistoryProviderEP
- remotehistoriansettings
- **TagHistoryProviderEP (SplittingProvider) Migrator**
- Trigger table: TAGHISTORYPROVIDEREP
- Duration: 0 ms
- Migrated tables:
- TagHistoryProviderEP
- TagHistorySplitterSettings
- **TagHistoryProviderEP (widedb) Migrator**
- Trigger table: TAGHISTORYPROVIDEREP
- Duration: 0 ms
- Migrated tables:
- TagHistoryProviderEP
- WideDbHistorianProviderSettings
- **TagHistoryProviderEP (history_sim) Migrator**
- Trigger table: TAGHISTORYPROVIDEREP
- Duration: 0 ms
- Migrated tables:
- TagHistoryProviderEP
- **TagHistoryProviderEP (CsvHistorian) Migrator**
- Trigger table: TAGHISTORYPROVIDEREP
- Duration: 0 ms
- Migrated tables:
- TagHistoryProviderEP
- csvHistorianSettings
- **TagHistoryProviderEP (datasource) Migrator**
- Trigger table: TAGHISTORYPROVIDEREP
- Duration: 0 ms
- Migrated tables:
- TagHistorianProviderSettings
- TagHistoryProviderEP
- **No-Op Migrator**
- Trigger table: SCHEDULES
- Duration: 0 ms
- Migrated tables:
- SCHEDULES
- **GeneralAlarmSettings Migrator**
- Trigger table: GENERALALARMSETTINGS
- Duration: 1 ms
- Migrated tables:
- GeneralAlarmSettings
- **EamControllerSettings Migrator**
- Trigger table: EAMCONTROLLERSETTINGS
- Duration: 0 ms
- Migrated tables:
- EamControllerSettings
- **Saved Tag Reports Migrator**
- Trigger table: TAGREPORTSEARCH
- Duration: 0 ms
- Migrated tables:
- TagReportSearch
- **Holiday Migrator**
- Trigger table: HOLIDAYS
- Duration: 0 ms
- Migrated tables:
- HOLIDAYS
- **User Source (DATASOURCE) Migrator**
- Trigger table: AUTHPROFILEPROPERTIES_DB
- Duration: 1 ms
- Migrated tables:
- AuthProfileProperties_DB
- Messages:
- All records of type 'AuthProfileProperties_DB' have been migrated
- **No-Op Migrator**
- Trigger table: IEC61850REPORTENTRYIDS
- Duration: 0 ms
- Migrated tables:
- Iec61850ReportEntryIds
- **Security Zones Migrator**
- Trigger table: SECURITYZONES
- Duration: 0 ms
- Migrated tables:
- SecuredEntityPolicy
- SecuredEntityZonePolicy
- securityZones
- **Ephemeral User Data No-Op Migrator**
- Trigger table: REMEMBERED_SUBJECTS
- Duration: 0 ms
- Migrated tables:
- IDP_ADAPTER_USER_HISTORY
- IDP_ADAPTER_USER_HISTORY_BK
- REMEMBERED_SUBJECTS
- REMEMBERED_SUBJECTS_BK
- **Quick Start State Migrator**
- Trigger table: STARTERSTEPS
- Duration: 3 ms
- Error: com.inductiveautomation.ignition.common.resourcecollection.PushConflictException: CREATE conflict: 'ResourceId{resourcePath=ignition/quickstart, collectionName=core}' already exists
- **OAuth2ClientSettings Migrator**
- Trigger table: OAUTH2CLIENTSETTINGS
- Duration: 0 ms
- Migrated tables:
- OAuth2ClientSettings
- **Alarm Journal Profile (LOCAL) Migrator**
- Trigger table: ALARMJOURNALS
- Duration: 0 ms
- Migrated tables:
- AlarmJournals
- LocalJournalSettings
- **Alarm Journal Profile (DATASOURCE) Migrator**
- Trigger table: ALARMJOURNALS
- Duration: 0 ms
- Migrated tables:
- AlarmJournals
- DatabaseJournalSettings
- **Alarm Journal Profile (REMOTE) Migrator**
- Trigger table: ALARMJOURNALS
- Duration: 0 ms
- Migrated tables:
- AlarmJournals
- RemoteJournalSettings
- **Extension Point Cleanup (AlarmJournals)**
- Trigger table: ALARMJOURNALS
- Duration: 0 ms
- Migrated tables:
- AlarmJournals
- **EULAS Migrator**
- Trigger table: EULAS
- Duration: 0 ms
- Migrated tables:
- EULAS
- **sfcsettings Migrator**
- Trigger table: SFCSETTINGS
- Duration: 0 ms
- Migrated tables:
- sfcsettings
- **RemoteAgent Migrator**
- Trigger table: REMOTEAGENT
- Duration: 0 ms
- Migrated tables:
- RemoteAgent
- **AgentGroup Migrator**
- Trigger table: AGENTGROUP
- Duration: 0 ms
- Migrated tables:
- AgentGroup
- **OPC Connection (com.inductiveautomation.OpcUaServerType) Migrator**
- Trigger table: OPCSERVERS
- Duration: 0 ms
- Migrated tables:
- OPCServers
- OpcUaConnectionSettings
- **Local Device Migrator**
- Trigger table: BACNETIPLOCALDEVICESETTINGS
- Duration: 0 ms
- Migrated tables:
- BacnetIpLocalDeviceSettings
- **Email Profile (smtp.oauth2) Migrator**
- Trigger table: EMAILPROFILES
- Duration: 0 ms
- Migrated tables:
- EmailProfiles
- OAuth2SmtpEmailProfiles
- **Email Profile (smtp.classic) Migrator**
- Trigger table: EMAILPROFILES
- Duration: 0 ms
- Migrated tables:
- ClassicSmtpEmailProfiles
- EmailProfiles
- **No-Op Migrator**
- Trigger table: DEVICES
- Duration: 0 ms
- Migrated tables:
- Devices
- DriverProperties
- OpcUaConnectionSettings2
- UAConnectionSettings
- XOPCSettings
- **MetricsDashboards Migrator**
- Trigger table: METRICSDASHBOARDS
- Duration: 0 ms
- Migrated tables:
- MetricsDashboards
- **Tag Configuration Migrator**
- Trigger table: TAGCONFIG
- Duration: 2 ms
- Migrated tables:
- tagconfig
- **EamLicense Migrator**
- Trigger table: EAMLICENSE
- Duration: 0 ms
- Migrated tables:
- EamLicense
- **Security Levels Migrator**
- Trigger table: SECURITY_LEVELS
- Duration: 0 ms
- Migrated tables:
- SECURITY_LEVELS
- **EAM Event Threshold Configuration Migrator**
- Trigger table: EVENTTHRESHOLD
- Duration: 0 ms
- Migrated tables:
- EventThreshold
- **EAM Remote Upgrade Configuration Migrator**
- Trigger table: REMOTEUPGRADE
- Duration: 0 ms
- Migrated tables:
- remoteupgrade
- **Tag Group Configuration Migrator**
- Trigger table: TAGGROUPS
- Duration: 0 ms
- Migrated tables:
- taggroups
- **Managed Tag Provider Migrator**
- Trigger table: SIMPLETAGPROVIDERPROFILE
- Duration: 0 ms
- Migrated tables:
- SimpleTagProviderProfile
- **User Source (AD_DB_HYBRID) Migrator**
- Trigger table: AUTHPROFILEPROPERTIES_ADTODB
- Duration: 1 ms
- Migrated tables:
- AuthProfileProperties_ADtoDB
- Messages:
- All records of type 'AuthProfileProperties_ADtoDB' have been migrated
- **User Source (ADEASY) Migrator**
- Trigger table: AUTHPROFILEPROPERTIES_AD
- Duration: 1 ms
- Migrated tables:
- AuthProfileProperties_AD
- Messages:
- All records of type 'AuthProfileProperties_AD' have been migrated
- **EamAgentSettings Migrator**
- Trigger table: EAMAGENTSETTINGS
- Duration: 0 ms
- Migrated tables:
- EamAgentSettings
- **Image Migrator**
- Trigger table: IMAGES
- Duration: 15 ms
- Migrated tables:
- Images
- **EAM Task History Migrator**
- Trigger table: TASKEVENTLOG
- Duration: 1 ms
- Migrated tables:
- TaskEventLog
- **JDBC Driver Migrator**
- Trigger table: JDBCDRIVERS
- Duration: 0 ms
- Migrated tables:
- JDBCDRIVERS
- **Edge System Properties Migrator**
- Trigger table: EDGESYSTEMPROPS
- Duration: 0 ms
- Migrated tables:
- EdgeSystemProps
- **Call Script Migrator**
- Trigger table: SCRIPTSETTINGSRECORD
- Duration: 0 ms
- Migrated tables:
- ScriptSettingsRecord
- **Roster Migrator**
- Trigger table: ROSTER
- Duration: 0 ms
- Migrated tables:
- ROSTER
- ROSTER_ENTRY
- **User Source (FALLBK_CACHE) Migrator**
- Trigger table: AUTHPROFILEPROPERTIES_LOCALFALLBACKCACHE
- Duration: 0 ms
- Migrated tables:
- AuthProfileProperties_LocalFallbackCache
- Messages:
- All records of type 'AuthProfileProperties_LocalFallbackCache' have been migrated
- **Store And Forward Migrator**
- Trigger table: STOREANDFORWARDSYSSETTINGS
- Duration: 0 ms
- Migrated tables:
- StoreAndForwardSysSettings
- **DATASOURCES Migrator**
- Trigger table: DATASOURCES
- Duration: 0 ms
- Migrated tables:
- DATASOURCES
- **Identity Provider Migrator**
- Trigger table: IDP_ADAPTERS
- Duration: 0 ms
- Migrated tables:
- IDP_ADAPTERS
- **OPC UA Server Configuration Migrator**
- Trigger table: OPCUASERVERSETTINGS
- Duration: 0 ms
- Migrated tables:
- OpcUaServerSettings
- **com.inductiveautomation.ignition.gateway.alarming.journal.EdgeJournalSettingsMigrationStrategy@37fbb5d4**
- Trigger table: EDGEJOURNALSETTINGS
- Duration: 0 ms
- Migrated tables:
- EdgeJournalSettings
- **No-Op Migrator**
- Trigger table: LOCALFALLBACKCACHE_ENTRY
- Duration: 0 ms
- Migrated tables:
- LocalFallbackCache_Entry
- **System Properties Migrator**
- Trigger table: SYSPROPS
- Duration: 0 ms
- Migrated tables:
- SysProps
## Remaining Tables
The following tables in `config.idb` have not yet been migrated.
Ensure the relevant modules are installed to avoid losing configuration
data on future upgrades.
- STARTERSTEPS
- WSQUEUE_OVERRIDES
- WS_PROXYRULES

View File

@@ -0,0 +1,9 @@
{
"pages": {
"/": {
"title": "Alarm Analysis",
"viewPath": "AlarmDashboard/Dashboard"
}
},
"sharedDocks": {}
}

View File

@@ -0,0 +1,10 @@
{
"scope": "G",
"version": 1,
"restricted": false,
"overridable": true,
"files": [
"config.json"
],
"attributes": {}
}

View File

@@ -0,0 +1,95 @@
{
"custom": {
"AlarmDashboard": {
"query": {
"areas": [],
"endMs": 0,
"filterDow": -1,
"filterHour": -1,
"priorities": [],
"rangePreset": "8h",
"refreshToken": 0,
"search": "",
"startMs": 0,
"states": []
},
"ui": {
"badActorFocus": "",
"selectedTab": 0
}
}
},
"propConfig": {
"custom.AlarmDashboard": {
"persistent": true
},
"props.auth": {
"access": "PRIVATE",
"persistent": false
},
"props.device.accelerometer": {
"access": "SYSTEM",
"persistent": false
},
"props.device.identifier": {
"access": "SYSTEM",
"persistent": false
},
"props.device.timezone": {
"access": "SYSTEM",
"persistent": false
},
"props.device.type": {
"access": "SYSTEM",
"persistent": false
},
"props.device.userAgent": {
"access": "SYSTEM",
"persistent": false
},
"props.gateway": {
"access": "SYSTEM",
"persistent": false
},
"props.geolocation.data": {
"access": "SYSTEM",
"persistent": false
},
"props.geolocation.permissionGranted": {
"access": "SYSTEM",
"persistent": false
},
"props.host": {
"access": "SYSTEM",
"persistent": false
},
"props.id": {
"access": "SYSTEM",
"persistent": false
},
"props.lastActivity": {
"access": "SYSTEM",
"persistent": false
},
"props.offline.capable": {
"access": "SYSTEM",
"persistent": false
},
"props.offline.enabled": {
"access": "SYSTEM",
"persistent": false
},
"props.offline.lastSynced": {
"access": "SYSTEM",
"persistent": false
}
},
"props": {
"device": {},
"geolocation": {},
"locale": "en-US",
"offline": {},
"securityLevels": [],
"theme": "light-cool"
}
}

View File

@@ -0,0 +1,16 @@
{
"scope": "G",
"version": 1,
"restricted": false,
"overridable": true,
"files": [
"props.json"
],
"attributes": {
"lastModificationSignature": "3859d47a0f5515c27fb6706190bb2c747a6e6a071f652dcda6a017fcc9a97c88",
"lastModification": {
"actor": "admin",
"timestamp": "2026-08-11T20:12:19Z"
}
}
}

View File

@@ -0,0 +1,10 @@
{
"scope": "G",
"version": 1,
"restricted": false,
"overridable": true,
"files": [
"style.json"
],
"attributes": {}
}

View File

@@ -0,0 +1,10 @@
{
"base": {
"style": {
"backgroundColor": "var(--neutral-10)",
"borderRadius": "8px",
"boxShadow": "0 1px 3px rgba(0,0,0,0.12)",
"padding": "12px"
}
}
}

View File

@@ -0,0 +1,10 @@
{
"scope": "G",
"version": 1,
"restricted": false,
"overridable": true,
"files": [
"style.json"
],
"attributes": {}
}

View File

@@ -0,0 +1,12 @@
{
"base": {
"style": {
"color": "var(--neutral-70)",
"fontSize": "12px",
"fontWeight": "bold",
"letterSpacing": "0.08em",
"marginBottom": "8px",
"textTransform": "uppercase"
}
}
}

View File

@@ -0,0 +1,10 @@
{
"scope": "G",
"version": 1,
"restricted": false,
"overridable": true,
"files": [
"style.json"
],
"attributes": {}
}

View File

@@ -0,0 +1,13 @@
{
"base": {
"style": {
"backgroundColor": "#D6455026",
"borderRadius": "10px",
"color": "#D64550",
"fontSize": "11px",
"fontWeight": "bold",
"paddingLeft": "6px",
"paddingRight": "6px"
}
}
}

View File

@@ -0,0 +1,10 @@
{
"scope": "G",
"version": 1,
"restricted": false,
"overridable": true,
"files": [
"style.json"
],
"attributes": {}
}

View File

@@ -0,0 +1,13 @@
{
"base": {
"style": {
"backgroundColor": "var(--neutral-30)",
"borderRadius": "10px",
"color": "var(--neutral-70)",
"fontSize": "11px",
"fontWeight": "bold",
"paddingLeft": "6px",
"paddingRight": "6px"
}
}
}

View File

@@ -0,0 +1,10 @@
{
"scope": "G",
"version": 1,
"restricted": false,
"overridable": true,
"files": [
"style.json"
],
"attributes": {}
}

View File

@@ -0,0 +1,13 @@
{
"base": {
"style": {
"backgroundColor": "#2E9E5B26",
"borderRadius": "10px",
"color": "#2E9E5B",
"fontSize": "11px",
"fontWeight": "bold",
"paddingLeft": "6px",
"paddingRight": "6px"
}
}
}

View File

@@ -0,0 +1,10 @@
{
"scope": "G",
"version": 1,
"restricted": false,
"overridable": true,
"files": [
"style.json"
],
"attributes": {}
}

View File

@@ -0,0 +1,9 @@
{
"base": {
"style": {
"color": "var(--neutral-50)",
"fontStyle": "italic",
"textAlign": "center"
}
}
}

View File

@@ -0,0 +1,10 @@
{
"scope": "G",
"version": 1,
"restricted": false,
"overridable": true,
"files": [
"style.json"
],
"attributes": {}
}

View File

@@ -0,0 +1,10 @@
{
"base": {
"style": {
"backgroundColor": "#2E9E5B",
"borderRadius": "8px",
"color": "#FFFFFF",
"fontWeight": "bold"
}
}
}

View File

@@ -0,0 +1,10 @@
{
"scope": "G",
"version": 1,
"restricted": false,
"overridable": true,
"files": [
"style.json"
],
"attributes": {}
}

View File

@@ -0,0 +1,10 @@
{
"base": {
"style": {
"backgroundColor": "#7CB342",
"borderRadius": "8px",
"color": "#FFFFFF",
"fontWeight": "bold"
}
}
}

View File

@@ -0,0 +1,10 @@
{
"scope": "G",
"version": 1,
"restricted": false,
"overridable": true,
"files": [
"style.json"
],
"attributes": {}
}

View File

@@ -0,0 +1,10 @@
{
"base": {
"style": {
"backgroundColor": "#E5C453",
"borderRadius": "8px",
"color": "#FFFFFF",
"fontWeight": "bold"
}
}
}

View File

@@ -0,0 +1,10 @@
{
"scope": "G",
"version": 1,
"restricted": false,
"overridable": true,
"files": [
"style.json"
],
"attributes": {}
}

View File

@@ -0,0 +1,10 @@
{
"base": {
"style": {
"backgroundColor": "#E8883A",
"borderRadius": "8px",
"color": "#FFFFFF",
"fontWeight": "bold"
}
}
}

View File

@@ -0,0 +1,10 @@
{
"scope": "G",
"version": 1,
"restricted": false,
"overridable": true,
"files": [
"style.json"
],
"attributes": {}
}

View File

@@ -0,0 +1,10 @@
{
"base": {
"style": {
"backgroundColor": "#D64550",
"borderRadius": "8px",
"color": "#FFFFFF",
"fontWeight": "bold"
}
}
}

View File

@@ -0,0 +1,10 @@
{
"scope": "G",
"version": 1,
"restricted": false,
"overridable": true,
"files": [
"style.json"
],
"attributes": {}
}

View File

@@ -0,0 +1,10 @@
{
"base": {
"style": {
"backgroundColor": "#8A94A6",
"borderRadius": "8px",
"color": "#FFFFFF",
"fontWeight": "bold"
}
}
}

View File

@@ -0,0 +1,10 @@
{
"scope": "G",
"version": 1,
"restricted": false,
"overridable": true,
"files": [
"style.json"
],
"attributes": {}
}

View File

@@ -0,0 +1,8 @@
{
"base": {
"style": {
"backgroundColor": "var(--neutral-20)",
"padding": "12px"
}
}
}

View File

@@ -0,0 +1,10 @@
{
"scope": "G",
"version": 1,
"restricted": false,
"overridable": true,
"files": [
"style.json"
],
"attributes": {}
}

View File

@@ -0,0 +1,15 @@
{
"base": {
"style": {
"backgroundColor": "#D64550",
"borderRadius": "4px",
"color": "#FFFFFF",
"fontSize": "12px",
"fontWeight": "bold",
"paddingBottom": "2px",
"paddingLeft": "8px",
"paddingRight": "8px",
"paddingTop": "2px"
}
}
}

View File

@@ -0,0 +1,10 @@
{
"scope": "G",
"version": 1,
"restricted": false,
"overridable": true,
"files": [
"style.json"
],
"attributes": {}
}

View File

@@ -0,0 +1,15 @@
{
"base": {
"style": {
"backgroundColor": "#8A94A6",
"borderRadius": "4px",
"color": "#FFFFFF",
"fontSize": "12px",
"fontWeight": "bold",
"paddingBottom": "2px",
"paddingLeft": "8px",
"paddingRight": "8px",
"paddingTop": "2px"
}
}
}

View File

@@ -0,0 +1,10 @@
{
"scope": "G",
"version": 1,
"restricted": false,
"overridable": true,
"files": [
"style.json"
],
"attributes": {}
}

View File

@@ -0,0 +1,15 @@
{
"base": {
"style": {
"backgroundColor": "#E8883A",
"borderRadius": "4px",
"color": "#FFFFFF",
"fontSize": "12px",
"fontWeight": "bold",
"paddingBottom": "2px",
"paddingLeft": "8px",
"paddingRight": "8px",
"paddingTop": "2px"
}
}
}

View File

@@ -0,0 +1,10 @@
{
"scope": "G",
"version": 1,
"restricted": false,
"overridable": true,
"files": [
"style.json"
],
"attributes": {}
}

View File

@@ -0,0 +1,15 @@
{
"base": {
"style": {
"backgroundColor": "#5B9BD5",
"borderRadius": "4px",
"color": "#FFFFFF",
"fontSize": "12px",
"fontWeight": "bold",
"paddingBottom": "2px",
"paddingLeft": "8px",
"paddingRight": "8px",
"paddingTop": "2px"
}
}
}

View File

@@ -0,0 +1,10 @@
{
"scope": "G",
"version": 1,
"restricted": false,
"overridable": true,
"files": [
"style.json"
],
"attributes": {}
}

View File

@@ -0,0 +1,15 @@
{
"base": {
"style": {
"backgroundColor": "#E5C453",
"borderRadius": "4px",
"color": "#FFFFFF",
"fontSize": "12px",
"fontWeight": "bold",
"paddingBottom": "2px",
"paddingLeft": "8px",
"paddingRight": "8px",
"paddingTop": "2px"
}
}
}

View File

@@ -0,0 +1,10 @@
{
"scope": "G",
"version": 1,
"restricted": false,
"overridable": true,
"files": [
"style.json"
],
"attributes": {}
}

View File

@@ -0,0 +1,10 @@
{
"base": {
"style": {
"backgroundColor": "#D6455026",
"borderRadius": "6px",
"color": "#D64550",
"fontWeight": "bold"
}
}
}

View File

@@ -0,0 +1,10 @@
{
"scope": "G",
"version": 1,
"restricted": false,
"overridable": true,
"files": [
"style.json"
],
"attributes": {}
}

View File

@@ -0,0 +1,10 @@
{
"base": {
"style": {
"backgroundColor": "#8A94A626",
"borderRadius": "6px",
"color": "#8A94A6",
"fontWeight": "bold"
}
}
}

View File

@@ -0,0 +1,10 @@
{
"scope": "G",
"version": 1,
"restricted": false,
"overridable": true,
"files": [
"style.json"
],
"attributes": {}
}

View File

@@ -0,0 +1,10 @@
{
"base": {
"style": {
"backgroundColor": "#E8883A26",
"borderRadius": "6px",
"color": "#E8883A",
"fontWeight": "bold"
}
}
}

View File

@@ -0,0 +1,10 @@
{
"scope": "G",
"version": 1,
"restricted": false,
"overridable": true,
"files": [
"style.json"
],
"attributes": {}
}

View File

@@ -0,0 +1,10 @@
{
"base": {
"style": {
"backgroundColor": "#5B9BD526",
"borderRadius": "6px",
"color": "#5B9BD5",
"fontWeight": "bold"
}
}
}

View File

@@ -0,0 +1,10 @@
{
"scope": "G",
"version": 1,
"restricted": false,
"overridable": true,
"files": [
"style.json"
],
"attributes": {}
}

View File

@@ -0,0 +1,10 @@
{
"base": {
"style": {
"backgroundColor": "#E5C45326",
"borderRadius": "6px",
"color": "#E5C453",
"fontWeight": "bold"
}
}
}

View File

@@ -0,0 +1,10 @@
{
"scope": "G",
"version": 1,
"restricted": false,
"overridable": true,
"files": [
"style.json"
],
"attributes": {}
}

View File

@@ -0,0 +1,8 @@
{
"base": {
"style": {
"backgroundColor": "#3B7DD818",
"color": "#3B7DD8"
}
}
}

View File

@@ -0,0 +1,10 @@
{
"scope": "G",
"version": 1,
"restricted": false,
"overridable": true,
"files": [
"style.json"
],
"attributes": {}
}

View File

@@ -0,0 +1,9 @@
{
"base": {
"style": {
"backgroundColor": "#D6455018",
"color": "#D64550",
"fontWeight": "bold"
}
}
}

View File

@@ -0,0 +1,10 @@
{
"scope": "G",
"version": 1,
"restricted": false,
"overridable": true,
"files": [
"style.json"
],
"attributes": {}
}

View File

@@ -0,0 +1,8 @@
{
"base": {
"style": {
"backgroundColor": "#2E9E5B18",
"color": "#2E9E5B"
}
}
}

View File

@@ -0,0 +1,10 @@
{
"scope": "G",
"version": 1,
"restricted": false,
"overridable": true,
"files": [
"style.json"
],
"attributes": {}
}

View File

@@ -0,0 +1,16 @@
{
"base": {
"style": {
"borderBottomColor": "#3B7DD8",
"borderBottomStyle": "solid",
"borderBottomWidth": "3px",
"color": "var(--neutral-90)",
"cursor": "pointer",
"fontWeight": "bold",
"paddingBottom": "6px",
"paddingLeft": "16px",
"paddingRight": "16px",
"paddingTop": "8px"
}
}
}

View File

@@ -0,0 +1,10 @@
{
"scope": "G",
"version": 1,
"restricted": false,
"overridable": true,
"files": [
"style.json"
],
"attributes": {}
}

View File

@@ -0,0 +1,23 @@
{
"base": {
"style": {
"borderBottomColor": "rgba(0,0,0,0)",
"borderBottomStyle": "solid",
"borderBottomWidth": "3px",
"color": "var(--neutral-60)",
"cursor": "pointer",
"paddingBottom": "6px",
"paddingLeft": "16px",
"paddingRight": "16px",
"paddingTop": "8px"
}
},
"variants": [
{
"pseudo": "hover",
"style": {
"color": "var(--neutral-90)"
}
}
]
}

View File

@@ -0,0 +1,10 @@
{
"scope": "G",
"version": 1,
"restricted": false,
"overridable": true,
"files": [
"style.json"
],
"attributes": {}
}

View File

@@ -0,0 +1,9 @@
{
"base": {
"style": {
"color": "var(--neutral-90)",
"fontSize": "28px",
"fontWeight": "bold"
}
}
}

View File

@@ -0,0 +1,10 @@
{
"scope": "G",
"version": 1,
"restricted": false,
"overridable": true,
"files": [
"style.json"
],
"attributes": {}
}

View File

@@ -0,0 +1,9 @@
{
"base": {
"style": {
"color": "var(--neutral-90)",
"fontSize": "22px",
"fontWeight": "bold"
}
}
}

View File

@@ -0,0 +1,10 @@
{
"scope": "G",
"version": 1,
"restricted": false,
"overridable": true,
"files": [
"style.json"
],
"attributes": {}
}

View File

@@ -0,0 +1,8 @@
{
"base": {
"style": {
"color": "var(--neutral-60)",
"fontSize": "12px"
}
}
}

View File

@@ -0,0 +1,10 @@
{
"scope": "G",
"version": 1,
"restricted": false,
"overridable": true,
"files": [
"style.json"
],
"attributes": {}
}

View File

@@ -0,0 +1,10 @@
{
"base": {
"style": {
"backgroundColor": "var(--neutral-10)",
"borderRadius": "8px",
"boxShadow": "0 1px 3px rgba(0,0,0,0.12)",
"padding": "8px"
}
}
}

View File

@@ -0,0 +1,16 @@
{
"scope": "G",
"version": 1,
"restricted": false,
"overridable": true,
"files": [
"view.json"
],
"attributes": {
"lastModificationSignature": "a606602b42f664df14e62d1509a4368ffc33d03863d17bc20a0bc6ed5124b07b",
"lastModification": {
"actor": "admin",
"timestamp": "2026-08-11T20:06:43Z"
}
}
}

Some files were not shown because too many files have changed in this diff Show More