Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions docs/features/licence-seats.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<!--
- SPDX-FileCopyrightText: 2026 Conduction B.V. <info@conduction.nl>
- SPDX-License-Identifier: EUPL-1.2
-->

# Licence seats

A licence contract records how the licence is measured and how many licences were bought and are in use. Stackiq sets the two numbers against each other on the contract and on the License posture page, so you see where use runs over what was bought.

Specification: [`openspec/specs/licence-seats/spec.md`](https://github.com/ConductionNL/stackiq/blob/development/openspec/specs/licence-seats/spec.md).

## Recording the licence on a contract

Open a contract and edit it. Three fields describe the licence:

- **Licence metric**: per named user, per concurrent user, per device, per inhabitant, per organisation, or other.
- **Licences bought**: the number the contract pays for.
- **Licences in use**: the number in use today.

All three are optional. A count cannot be negative.

## The licences panel on a contract

The contract page shows a **Licences** panel with the metric, both counts, a bar of in use against bought, and one of these states:

- **Within licence**: in use is at or below bought.
- **Over licence by N**: in use is N above bought. The bar turns red.
- **Unknown**: one of the counts is empty.
- **Not counted**: the metric is per organisation or other, so there is nothing to count and no bar.

The panel also shows the date the contract was last changed, so you can judge how current the count is.

## The Seats section on the License posture page

Open **License posture** in the navigation menu. The **Seats** section has one row per contract with a counted metric and a number of licences bought. Each row names the application, the organisation, the metric, bought, in use and the state. Contracts over their licence come first, the furthest over at the top.

You see the contracts you may read; the section uses the same access rules as the rest of the page.

Screenshots of the panel and the section follow once the feature runs on the demo instance.
22 changes: 22 additions & 0 deletions l10n/en.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,28 @@
OC.L10N.register(
"stackiq",
{
"Licence metric": "Licence metric",
"What one licence covers, as the contract with the supplier states it.": "What one licence covers, as the contract with the supplier states it.",
"Licences bought": "Licences bought",
"The number of licences this contract buys.": "The number of licences this contract buys.",
"Licences in use": "Licences in use",
"The number of licences in use today, as the application owner counted them.": "The number of licences in use today, as the application owner counted them.",
"Per named user": "Per named user",
"Per concurrent user": "Per concurrent user",
"Per device": "Per device",
"Per inhabitant": "Per inhabitant",
"Per organisation": "Per organisation",
"Other": "Other",
"Licences": "Licences",
"Loading licences": "Loading licences",
"The licences could not be loaded.": "The licences could not be loaded.",
"Last changed on {date}": "Last changed on {date}",
"{inUse} of {bought} in use": "{inUse} of {bought} in use",
"Within licence": "Within licence",
"Over licence by {count}": "Over licence by {count}",
"Not counted": "Not counted",
"Seats": "Seats",
"No licence contracts with counts": "No licence contracts with counts",
"AMEF elements": "AMEF elements",
"AMEF standards": "AMEF standards",
"ArchiMate element": "ArchiMate element",
Expand Down
22 changes: 22 additions & 0 deletions l10n/en.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,27 @@
{
"translations": {
"Licence metric": "Licence metric",
"What one licence covers, as the contract with the supplier states it.": "What one licence covers, as the contract with the supplier states it.",
"Licences bought": "Licences bought",
"The number of licences this contract buys.": "The number of licences this contract buys.",
"Licences in use": "Licences in use",
"The number of licences in use today, as the application owner counted them.": "The number of licences in use today, as the application owner counted them.",
"Per named user": "Per named user",
"Per concurrent user": "Per concurrent user",
"Per device": "Per device",
"Per inhabitant": "Per inhabitant",
"Per organisation": "Per organisation",
"Other": "Other",
"Licences": "Licences",
"Loading licences": "Loading licences",
"The licences could not be loaded.": "The licences could not be loaded.",
"Last changed on {date}": "Last changed on {date}",
"{inUse} of {bought} in use": "{inUse} of {bought} in use",
"Within licence": "Within licence",
"Over licence by {count}": "Over licence by {count}",
"Not counted": "Not counted",
"Seats": "Seats",
"No licence contracts with counts": "No licence contracts with counts",
"AMEF elements": "AMEF elements",
"AMEF standards": "AMEF standards",
"ArchiMate element": "ArchiMate element",
Expand Down
22 changes: 22 additions & 0 deletions l10n/nl.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,28 @@
OC.L10N.register(
"stackiq",
{
"Licence metric": "Licentiemetriek",
"What one licence covers, as the contract with the supplier states it.": "Waar één licentie voor geldt, zoals het contract met de leverancier het vastlegt.",
"Licences bought": "Gekochte licenties",
"The number of licences this contract buys.": "Het aantal licenties dat dit contract koopt.",
"Licences in use": "Licenties in gebruik",
"The number of licences in use today, as the application owner counted them.": "Het aantal licenties dat vandaag in gebruik is, zoals de applicatie-eigenaar ze telde.",
"Per named user": "Per benoemde gebruiker",
"Per concurrent user": "Per gelijktijdige gebruiker",
"Per device": "Per apparaat",
"Per inhabitant": "Per inwoner",
"Per organisation": "Per organisatie",
"Other": "Overig",
"Licences": "Licenties",
"Loading licences": "Licenties laden",
"The licences could not be loaded.": "De licenties konden niet worden geladen.",
"Last changed on {date}": "Laatst gewijzigd op {date}",
"{inUse} of {bought} in use": "{inUse} van {bought} in gebruik",
"Within licence": "Binnen de licentie",
"Over licence by {count}": "{count} boven de licentie",
"Not counted": "Niet geteld",
"Seats": "Licentiegebruik",
"No licence contracts with counts": "Geen licentiecontracten met aantallen",
"Load example data?": "Voorbeeldgegevens laden?",
"Example data fills the lists, detail pages and dashboards so you can see the app working straight away. Pick \"None\" on a production install.": "Voorbeeldgegevens vullen de lijsten, detailpagina’s en dashboards, zodat je de app meteen ziet werken. Kies \"Geen\" op een productieomgeving.",
"Load the example data": "Laad de voorbeeldgegevens",
Expand Down
22 changes: 22 additions & 0 deletions l10n/nl.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,27 @@
{
"translations": {
"Licence metric": "Licentiemetriek",
"What one licence covers, as the contract with the supplier states it.": "Waar één licentie voor geldt, zoals het contract met de leverancier het vastlegt.",
"Licences bought": "Gekochte licenties",
"The number of licences this contract buys.": "Het aantal licenties dat dit contract koopt.",
"Licences in use": "Licenties in gebruik",
"The number of licences in use today, as the application owner counted them.": "Het aantal licenties dat vandaag in gebruik is, zoals de applicatie-eigenaar ze telde.",
"Per named user": "Per benoemde gebruiker",
"Per concurrent user": "Per gelijktijdige gebruiker",
"Per device": "Per apparaat",
"Per inhabitant": "Per inwoner",
"Per organisation": "Per organisatie",
"Other": "Overig",
"Licences": "Licenties",
"Loading licences": "Licenties laden",
"The licences could not be loaded.": "De licenties konden niet worden geladen.",
"Last changed on {date}": "Laatst gewijzigd op {date}",
"{inUse} of {bought} in use": "{inUse} van {bought} in gebruik",
"Within licence": "Binnen de licentie",
"Over licence by {count}": "{count} boven de licentie",
"Not counted": "Niet geteld",
"Seats": "Licentiegebruik",
"No licence contracts with counts": "Geen licentiecontracten met aantallen",
"Load example data?": "Voorbeeldgegevens laden?",
"Example data fills the lists, detail pages and dashboards so you can see the app working straight away. Pick \"None\" on a production install.": "Voorbeeldgegevens vullen de lijsten, detailpagina’s en dashboards, zodat je de app meteen ziet werken. Kies \"Geen\" op een productieomgeving.",
"Load the example data": "Laad de voorbeeldgegevens",
Expand Down
50 changes: 50 additions & 0 deletions lib/Settings/register.d/contracts-licence-seats.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
{
"components": {
"schemas": {
"catalogContract": {
"version": "0.1.3",
"properties": {
"licenceMetric": {
"type": "string",
"enum": [
"Per named user",
"Per concurrent user",
"Per device",
"Per inhabitant",
"Per organisation",
"Other"
],
"x-enum-labels": {
"Per named user": "Per named user",
"Per concurrent user": "Per concurrent user",
"Per device": "Per device",
"Per inhabitant": "Per inhabitant",
"Per organisation": "Per organisation",
"Other": "Other"
},
"title": "Licence metric",
"description": "What one licence covers, as the contract with the supplier states it.",
"facetable": true,
"order": 14
},
"licencesBought": {
"type": "integer",
"minimum": 0,
"title": "Licences bought",
"description": "The number of licences this contract buys.",
"facetable": false,
"order": 15
},
"licencesInUse": {
"type": "integer",
"minimum": 0,
"title": "Licences in use",
"description": "The number of licences in use today, as the application owner counted them.",
"facetable": false,
"order": 16
}
}
}
}
}
}
80 changes: 78 additions & 2 deletions lib/Settings/stackiq_mock_register.json
Original file line number Diff line number Diff line change
Expand Up @@ -1334,6 +1334,45 @@
},
"referenceType": "decision",
"x-allow-create": true
},
"licenceMetric": {
"type": "string",
"enum": [
"Per named user",
"Per concurrent user",
"Per device",
"Per inhabitant",
"Per organisation",
"Other"
],
"x-enum-labels": {
"Per named user": "Per named user",
"Per concurrent user": "Per concurrent user",
"Per device": "Per device",
"Per inhabitant": "Per inhabitant",
"Per organisation": "Per organisation",
"Other": "Other"
},
"title": "Licence metric",
"description": "What one licence covers, as the contract with the supplier states it.",
"facetable": true,
"order": 14
},
"licencesBought": {
"type": "integer",
"minimum": 0,
"title": "Licences bought",
"description": "The number of licences this contract buys.",
"facetable": false,
"order": 15
},
"licencesInUse": {
"type": "integer",
"minimum": 0,
"title": "Licences in use",
"description": "The number of licences in use today, as the application owner counted them.",
"facetable": false,
"order": 16
}
},
"required": [
Expand Down Expand Up @@ -1387,7 +1426,7 @@
},
"title": "Contract",
"description": "Een formele overeenkomst voor het inzetten van een Dienst op een Gebruik. Beschrijft de inkooprelatie achter een Gebruik (welke organisatie welke module onder welke voorwaarden gebruikt): looptijd, kosten, contracttype en status.",
"version": "0.1.2",
"version": "0.1.3",
"omschrijving": "",
"icon": "FileSign",
"archive": [],
Expand Down Expand Up @@ -7664,7 +7703,10 @@
"opmerkingen": "Voorbeeld Opmerkingen 2",
"decisions": [
"00000000-0000-4000-8000-000000000001"
]
],
"licenceMetric": "Per named user",
"licencesBought": 400,
"licencesInUse": 460
},
{
"@self": {
Expand Down Expand Up @@ -7697,6 +7739,40 @@
"00000000-0000-4000-8000-000000000002"
]
},
{
"@self": {
"register": "stackiq",
"schema": "catalogContract",
"slug": "contract-seats-5"
},
"service": {},
"usage": {},
"startDate": "2026-03-02",
"contractNumber": "Voorbeeld Contractnumber 5",
"contractType": "Licence",
"status": "Active",
"approvalDecisionId": "Voorbeeld Approvaldecisionid 2",
"approvalState": "none",
"endDate": "2026-03-02",
"cost": 2.0,
"costPeriod": "Annually",
"contactPersonProvider": {
"name": "Voorbeeld Name 2",
"email": "Voorbeeld Email 2"
},
"contactPersonUser": {
"name": "Voorbeeld Name 2",
"email": "Voorbeeld Email 2"
},
"documentReference": "Voorbeeld Documentreference 2",
"opmerkingen": "Voorbeeld Opmerkingen 2",
"decisions": [
"00000000-0000-4000-8000-000000000001"
],
"licenceMetric": "Per inhabitant",
"licencesBought": 58000,
"licencesInUse": 57120
},
{
"@self": {
"register": "stackiq",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ An application owner records how a licence contract is measured and how many lic
- **AND** the bar SHALL use the error variant

#### Scenario: A site licence is not counted
@e2e exclude Covered by tests/vitest/licensePosture.spec.js, which asserts seatPosition() returns not-counted for Per organisation and Other.
@e2e exclude Covered by tests/vitest/licenceSeats.spec.js, which asserts seatPosition() returns not-counted for Per organisation and Other.

- **GIVEN** a contract with Per organisation and 1 bought
- **WHEN** the seats panel renders
Expand All @@ -62,7 +62,7 @@ The `LicensePosture` page at `/license-posture` SHALL have a Seats section with
- **AND** it SHALL list the other contract as Within licence

#### Scenario: Contracts without counts stay out of the section
@e2e exclude Covered by tests/vitest/licensePosture.spec.js, which asserts seatRows() skips contracts without licencesBought or with an uncounted metric.
@e2e exclude Covered by tests/vitest/licenceSeats.spec.js, which asserts seatRows() skips contracts without licencesBought or with an uncounted metric.

- **GIVEN** an SLA contract without any licence fields
- **WHEN** the Seats section renders
Expand Down
Loading
Loading