Skip to content

Conversation

@mherwege
Copy link
Contributor

@mherwege mherwege commented Jan 15, 2026

This PR adds a section on persistence in item detail screen. It will:

  1. Show the different persistence services and indicate if the current item is configured to be persisted and the strategies used.
  2. Provides a link to the specific service configuration if editable.
  3. Show a general link to persistence service configurations.
  4. Shows a badge indicating if the item is actually in the persistence store (with a count of persisted values). This badge will only show if the persistence service has implemented the getItemInfo() method, and not just returns an empty list. This is a contraint of the persistence service implementations, to be revisited in the the persistence addons.

Here is a screenshot:
image

@relativeci
Copy link

relativeci bot commented Jan 15, 2026

#4480 Bundle Size — 12.78MiB (+0.02%).

668282b(current) vs 5f64317 main#4472(baseline)

Warning

Bundle contains 2 duplicate packages – View duplicate packages

Bundle metrics  Change 1 change
                 Current
#4480
     Baseline
#4472
No change  Initial JS 1.58MiB 1.58MiB
No change  Initial CSS 0B 0B
No change  Cache Invalidation 6.7% 6.7%
No change  Chunks 601 601
No change  Assets 682 682
Change  Modules 2585(+0.08%) 2583
No change  Duplicate Modules 0 0
No change  Duplicate Code 0% 0%
No change  Packages 123 123
No change  Duplicate Packages 1 1
Bundle size by type  Change 2 changes Regression 2 regressions
                 Current
#4480
     Baseline
#4472
Regression  JS 11.06MiB (+0.03%) 11.06MiB
Regression  CSS 895.08KiB (~+0.01%) 895.03KiB
No change  Fonts 526.1KiB 526.1KiB
No change  Media 295.6KiB 295.6KiB
No change  IMG 45.73KiB 45.73KiB
No change  Other 847B 847B

Bundle analysis reportBranch mherwege:item_persistence_statusProject dashboard


Generated by RelativeCIDocumentationReport issue

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds a persistence status section to the item detail screen, showing which persistence services are configured for an item, the strategies being used, and whether the item is actually persisted with a count of stored values.

Changes:

  • Added a new Persistence section to the item details page showing persistence service configurations
  • Created a new component to display persistence service details including configuration status, strategies, and persisted item counts
  • Integrated the component with links to persistence service configurations

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 7 comments.

File Description
bundles/org.openhab.ui/web/src/pages/settings/items/item-details.vue Adds the persistence section to the item details page with conditional rendering for non-Group items or Groups with a groupType
bundles/org.openhab.ui/web/src/components/persistence/item-persistence-details.vue New component that fetches and displays persistence service information, including matching strategies and persisted status with badges

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.


return {
...service,
configs: serviceConfig.configs || [],
Copy link

Copilot AI Jan 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's an extra space before the 'or' operator in this line. While this doesn't affect functionality, it's inconsistent with common JavaScript formatting conventions.

Suggested change
configs: serviceConfig.configs || [],
configs: serviceConfig.configs || [],

Copilot uses AI. Check for mistakes.
async load () {
this.loaded = false
const services = await this.$oh.api.get('/rest/persistence')
this.ready = false
Copy link

Copilot AI Jan 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The variable 'ready' is being set but is never declared in the data() function. This will create an undeclared reactive property and could lead to unexpected behavior. The 'ready' property should either be added to the data() function or this line should be removed if it's not needed.

Suggested change
this.ready = false

Copilot uses AI. Check for mistakes.
this.services = await Promise.all(
services.map((service) => this.loadService(service))
)
this.currentState = this.item.state
Copy link

Copilot AI Jan 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The 'currentState' property is set but never used anywhere in the component. Consider removing this unused property to keep the code clean and maintainable.

Copilot uses AI. Check for mistakes.
configs: serviceConfig.configs || [],
aliases: serviceConfig.aliases,
editable: serviceConfig.editable === undefined ? true : serviceConfig.editable,
persisted: itemsPersisted.findIndex((item) => item.name === this.item.name) >= 0
Copy link

Copilot AI Jan 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The 'persisted' property is incorrectly set to a boolean value based on whether the item is found in the array. However, the 'persistedBadges' computed property expects 'persisted' to be an object with a 'count' property (line 53). This mismatch will cause the badge to always show 'values' instead of the actual count when an item is persisted. The API response from '/rest/persistence/items' should be stored with its full structure, not just a boolean indicating presence.

Suggested change
persisted: itemsPersisted.findIndex((item) => item.name === this.item.name) >= 0
persisted: itemsPersisted.find((item) => item.name === this.item.name) || null

Copilot uses AI. Check for mistakes.
Comment on lines 47 to 49
configuredServices () {
return this.services.filter((service) => this.strategies[service.id]?.length)
},
Copy link

Copilot AI Jan 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The computed property 'configuredServices' is defined but never used in the component. Consider removing this unused computed property to reduce code complexity and improve maintainability.

Suggested change
configuredServices () {
return this.services.filter((service) => this.strategies[service.id]?.length)
},

Copilot uses AI. Check for mistakes.
Comment on lines 104 to 116
serviceStrategies (serviceId) {
let strategies = []
const persistenceConfig = this.services.find((persistence) => persistence.id === serviceId)
persistenceConfig?.configs?.forEach((config) => {
const items = config.items
if (items && config.strategies?.length) {
let match = false
// First find all positive matches for the item
if (items.includes('*') || items.includes(this.item.name) || items.filter((configItem) => !configItem.startsWith('!') && configItem.endsWith('*')).map((configItem) => configItem.slice(0, -1)).find((groupName) => this.item.groupNames.includes(groupName))) {
match = true
}
// Remove negative matches
if (items.includes('!' + this.item.name) || items.filter((configItem) => configItem.startsWith('!') && configItem.endsWith('*')).map((configItem) => configItem.slice(1, -1)).find((groupName) => this.item.groupNames.includes(groupName))) {
Copy link

Copilot AI Jan 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This complex matching logic is difficult to read and maintain due to multiple chained method calls on a single line. The logic for matching group names with wildcards should be extracted into a separate helper method to improve readability and testability. Additionally, the same pattern is repeated on line 116 for negative matches, which creates code duplication.

Suggested change
serviceStrategies (serviceId) {
let strategies = []
const persistenceConfig = this.services.find((persistence) => persistence.id === serviceId)
persistenceConfig?.configs?.forEach((config) => {
const items = config.items
if (items && config.strategies?.length) {
let match = false
// First find all positive matches for the item
if (items.includes('*') || items.includes(this.item.name) || items.filter((configItem) => !configItem.startsWith('!') && configItem.endsWith('*')).map((configItem) => configItem.slice(0, -1)).find((groupName) => this.item.groupNames.includes(groupName))) {
match = true
}
// Remove negative matches
if (items.includes('!' + this.item.name) || items.filter((configItem) => configItem.startsWith('!') && configItem.endsWith('*')).map((configItem) => configItem.slice(1, -1)).find((groupName) => this.item.groupNames.includes(groupName))) {
matchesPositiveItems (items, itemName, groupNames) {
if (!items || !Array.isArray(items)) {
return false
}
if (items.includes('*') || items.includes(itemName)) {
return true
}
const positiveGroupPatterns = items.filter((configItem) => !configItem.startsWith('!') && configItem.endsWith('*'))
return positiveGroupPatterns
.map((configItem) => configItem.slice(0, -1))
.some((groupName) => groupNames.includes(groupName))
},
matchesNegativeItems (items, itemName, groupNames) {
if (!items || !Array.isArray(items)) {
return false
}
if (items.includes('!' + itemName)) {
return true
}
const negativeGroupPatterns = items.filter((configItem) => configItem.startsWith('!') && configItem.endsWith('*'))
return negativeGroupPatterns
.map((configItem) => configItem.slice(1, -1))
.some((groupName) => groupNames.includes(groupName))
},
serviceStrategies (serviceId) {
let strategies = []
const persistenceConfig = this.services.find((persistence) => persistence.id === serviceId)
const itemName = this.item.name
const groupNames = this.item.groupNames
persistenceConfig?.configs?.forEach((config) => {
const items = config.items
if (items && config.strategies?.length) {
let match = false
// First find all positive matches for the item
if (this.matchesPositiveItems(items, itemName, groupNames)) {
match = true
}
// Remove negative matches
if (this.matchesNegativeItems(items, itemName, groupNames)) {

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe there still is a lot of code duplication in your proposal and the names and arguments of the match methods do not clearly convey the meaning. I have created an alternative.

Comment on lines 112 to 116
if (items.includes('*') || items.includes(this.item.name) || items.filter((configItem) => !configItem.startsWith('!') && configItem.endsWith('*')).map((configItem) => configItem.slice(0, -1)).find((groupName) => this.item.groupNames.includes(groupName))) {
match = true
}
// Remove negative matches
if (items.includes('!' + this.item.name) || items.filter((configItem) => configItem.startsWith('!') && configItem.endsWith('*')).map((configItem) => configItem.slice(1, -1)).find((groupName) => this.item.groupNames.includes(groupName))) {
Copy link

Copilot AI Jan 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code assumes 'this.item.groupNames' exists and is an array, but it may be undefined for items that don't belong to any groups. This will cause a runtime error when calling '.includes()' on undefined. The code should use optional chaining or a nullish coalescing operator to handle this case (e.g., 'this.item.groupNames?.includes(groupName)' or 'this.item.groupNames || []').

Suggested change
if (items.includes('*') || items.includes(this.item.name) || items.filter((configItem) => !configItem.startsWith('!') && configItem.endsWith('*')).map((configItem) => configItem.slice(0, -1)).find((groupName) => this.item.groupNames.includes(groupName))) {
match = true
}
// Remove negative matches
if (items.includes('!' + this.item.name) || items.filter((configItem) => configItem.startsWith('!') && configItem.endsWith('*')).map((configItem) => configItem.slice(1, -1)).find((groupName) => this.item.groupNames.includes(groupName))) {
if (items.includes('*') || items.includes(this.item.name) || items.filter((configItem) => !configItem.startsWith('!') && configItem.endsWith('*')).map((configItem) => configItem.slice(0, -1)).find((groupName) => this.item.groupNames?.includes(groupName))) {
match = true
}
// Remove negative matches
if (items.includes('!' + this.item.name) || items.filter((configItem) => configItem.startsWith('!') && configItem.endsWith('*')).map((configItem) => configItem.slice(1, -1)).find((groupName) => this.item.groupNames?.includes(groupName))) {

Copilot uses AI. Check for mistakes.
Signed-off-by: Mark Herwege <[email protected]>
Signed-off-by: Mark Herwege <[email protected]>
Signed-off-by: Mark Herwege <[email protected]>
Signed-off-by: Mark Herwege <[email protected]>
Signed-off-by: Mark Herwege <[email protected]>
Signed-off-by: Mark Herwege <[email protected]>
@mherwege mherwege force-pushed the item_persistence_status branch from 5eb197f to bea9622 Compare January 19, 2026 08:29
Signed-off-by: Mark Herwege <[email protected]>
Signed-off-by: Mark Herwege <[email protected]>
Signed-off-by: Mark Herwege <[email protected]>
@mherwege mherwege requested a review from peuter as a code owner January 19, 2026 10:06
Signed-off-by: Mark Herwege <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request main ui Main UI

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants