Skip to content

Statistics tab and cleanup for datapoints that are no longer logged (#247) - #569

Merged
GermanBluefox merged 3 commits into
masterfrom
feat/cleanup-and-statistics
Oct 2, 2026
Merged

GermanBluefox merged 3 commits into
masterfrom
feat/cleanup-and-statistics

Conversation

@GermanBluefox

Copy link
Copy Markdown
Contributor

Implements #247, open since 2022. The reporter had over a million values left behind by states he had deleted or stopped logging, and the only way to find them was a forum script.

Adapter: two new messages

getDpStatistics — one row per ID in datapoints: storage type, exact number of values, oldest and newest timestamp, estimated size, status, plus totals. One GROUP BY id and one size query per table, not per datapoint; a long-running database holds thousands of IDs.

cleanupOrphaned — removes the values of orphaned datapoints. Without confirm: true it deletes nothing and reports only what a confirmed run would remove. Both messages share one collection routine, so the preview and the deletion cannot disagree about what counts as orphaned.

One decision I made beyond the request

The request said "all IDs that no longer exist or are not logged". Those are two very different things, so the status keeps them apart:

status meaning in cleanup
objectMissing the ioBroker state is gone default — nobody can chart this data any more
loggingDisabled state exists, logging switched off only on explicit request, with a warning
active logged normally never

A state whose logging is merely switched off still has reachable history that may be kept on purpose. Deleting it by default would be a data-loss trap, so selectForCleanup defaults to the safe half even when the caller passes no scope at all.

About the size column

No SQL dialect can report the bytes of a single datapoint. The column is the row count multiplied by the average row width the database reports — MySQL information_schema, PostgreSQL pg_total_relation_size/reltuples, MS SQL dm_db_partition_stats, SQLite dbstat. It is labelled an estimate in the UI, and where a width cannot be determined (dbstat is not compiled into every sqlite3 build, a restricted role may not reach the catalog) the answer carries null rather than a fabricated number. A total that would be partial is null too, because a partial sum presented as a total understates the footprint.

Two details that are easy to get wrong

  • Cleanup deletes from every ts_* table, not only the one the stored type suggests. A datapoint whose storage type was changed over the years has rows in more than one, and the test covers exactly that case.
  • It also removes the datapoints row. Otherwise the ID would keep showing up in the statistics with zero values after a cleanup.

Admin UI

A new Statistics panel: sortable, filterable table with a summary line and status chips, and a Clean up button that runs the dry run first and opens a confirmation dialog listing every affected ID with its counts and estimated size.

Three new SQL builders per dialect: getIdCounts, getTableSize, deleteDatapoint.

Verification

test/testStatistics.js, 17 cases, in the CI unit list. 163 unit tests pass in total; check:ts, the src-admin type check and prettier --check are clean. The pure helpers live in src/lib/statistics.ts rather than main.ts so they are reachable without importing @iobroker/adapter-core — the constraint #567 established.

The SQL is exercised against a real SQLite database, not just asserted as strings: counts and time ranges per ID, and a cleanup of a datapoint with rows in two tables showing both are emptied, its datapoints row removed, and the surviving datapoint's values untouched.

The admin bundle is rebuilt and committed; the Statistics component is verified present in admin/custom/assets/.

Not covered by automated tests: the React component itself. There is no component test setup in this repository, so the table, sorting and the confirmation dialog were verified by type checking and a successful federation build, not by rendering. Worth a click-through before release.

🤖 Generated with Claude Code

GermanBluefox and others added 2 commits October 2, 2026 21:21
…logged (#247)

Years of enabling and deleting states leave a SQL database full of values
nobody can reach any more - the reporter had over a million. Until now the only
way to find and remove them was a forum script.

Adapter side, two new messages:

- `getDpStatistics` returns one row per ID in the `datapoints` table with its
  storage type, exact number of values, oldest and newest timestamp, an
  estimated size and a status, plus totals. It costs one `GROUP BY id` and one
  size query per time series table rather than one query per datapoint, because
  a long-running database holds thousands of IDs.
- `cleanupOrphaned` removes the values of orphaned datapoints. Without
  `confirm: true` it deletes nothing and only reports what a confirmed run
  would remove, so the dialog can show the list and the counts first. Both
  messages share one collection routine, so the preview and the deletion cannot
  disagree about what is orphaned.

The status deliberately separates two cases that the request lumped together.
A state that no longer exists in ioBroker cannot be charted by anyone, so
dropping its history is safe and that is the default. A state that still exists
but has logging switched off is different: its history is reachable and may be
wanted on purpose, so it is only included when the caller asks for it
explicitly, and the dialog warns about it.

Cleanup deletes from every `ts_*` table, not only the one the stored type
suggests: a datapoint whose storage type was changed over the years has rows in
more than one. It also removes the `datapoints` row, otherwise the ID would
keep appearing in the statistics with zero values.

Three new SQL builders per dialect: `getIdCounts`, `getTableSize` and
`deleteDatapoint`.

About the size column: no SQL dialect can report the bytes of a single
datapoint, so it is the row count multiplied by the average row width the
database reports - MySQL `information_schema`, PostgreSQL
`pg_total_relation_size`/`reltuples`, MS SQL `dm_db_partition_stats`, SQLite
`dbstat`. It is labelled as an estimate throughout, and when a width cannot be
determined - `dbstat` is not in every sqlite3 build, a restricted role may not
reach the catalog - the answer carries null rather than a fabricated number,
and a total that would be partial is null too.

Admin side: a `Statistics` panel holding a sortable, filterable table with the
summary, and a Clean up button that runs the dry run first and opens a
confirmation dialog listing every affected ID with its counts.

Tests: test/testStatistics.js, 17 cases, in the CI unit list. The pure helpers
live in src/lib/statistics.ts rather than main.ts so they are reachable without
importing @iobroker/adapter-core. The SQL is additionally exercised against a
real SQLite database, including a datapoint with rows in two tables, to show
that the cleanup empties both and leaves the surviving datapoint untouched.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`npm run lint` was not run before pushing - only prettier and the type checks -
so check-and-lint caught both, and every adapter-tests job was skipped because
it needs that one.

- `sortField: 'count' as SortField` tripped no-unnecessary-type-assertion. The
  literal is already assignable; the assertion only hid that.
- `super.componentDidMount()` was left floating. DataBrowser awaits it, which is
  what this should have done from the start.

Bundle rebuilt so the committed output matches the source.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@GermanBluefox

Copy link
Copy Markdown
Contributor Author

Pushed a fix for the check-and-lint failure. It was my own miss: I verified this branch with prettier --check and both type checks but not with npm run lint, which is the project's lint command. Two findings in src-admin/src/Statistics.tsx:

  • sortField: 'count' as SortField tripped @typescript-eslint/no-unnecessary-type-assertion. The literal is already assignable to the state type; the assertion only hid that. (error)
  • super.componentDidMount() was left floating. DataBrowser awaits it, which is what this should have done from the start. (warning)

Worth noting for the record: the error alone failed the job, and because every adapter-tests-* job declares needs: [check-and-lint], all of them were skipped rather than run — so the green/red picture showed 6 skipped jobs and said nothing about whether the feature works. The next run is the first real test of the four dialects against this branch.

npm run lint is now clean for both passes (adapter and src-admin), both type checks pass, 163 unit tests pass, and the bundle was rebuilt so the committed output matches the source.

…atistics

# Conflicts:
#	README.md
#	build/main.js.map
@GermanBluefox
GermanBluefox merged commit 7317faa into master Oct 2, 2026
15 of 16 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant