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
104 changes: 66 additions & 38 deletions lib/Service/ConnectionReportService.php
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,26 @@ class ConnectionReportService {
*/
public const KEY_EOL = 'eol-feed';

/**
* The pull reason FederationService records for switched-off federation, which is never reported.
*
* The `federation` switch in lib/Settings/connections.json reads
* `federation_enabled`, so integriq shows `disabled` without a report
* (hydra connection-registry D4 rule 2b).
*
* @var string
*/
public const PULL_REASON_SWITCHED_OFF = 'federation disabled';

/**
* The EOL sync degrade reason for a switched-off sync, which is never reported.
*
* The `eol-feed` switch reads `enabled` inside `eol_sync_config` itself.
*
* @var string
*/
public const EOL_REASON_SWITCHED_OFF = 'disabled';

/**
* The longest failure reason a message carries.
*
Expand All @@ -93,15 +113,13 @@ class ConnectionReportService {
/**
* What each EOL sync degrade reason means for the row, as status and message.
*
* The reasons are the ones EolSyncService::degrade() records.
* The reasons are the ones EolSyncService::degrade() records, apart from
* `disabled`: the `eol-feed` switch in lib/Settings/connections.json reads
* a switched-off sync itself, so that reason reports nothing.
*
* @var array<string, array{0: string, 1: string}>
*/
public const EOL_REASONS = [
'disabled' => [
'unconfigured',
'End-of-life sync is switched off. Switch it on in the End-of-life feed sync section.',
],
'openregister-not-installed' => [
'unavailable',
'The end-of-life sync needs OpenRegister, and it is not installed.',
Expand Down Expand Up @@ -203,6 +221,9 @@ public function describeEmail(array $configStatus, array $transportLabels): arra
*
* A ready federation gets no report: only a pull can tell whether the peers
* answer, so the row reads the declared "Not checked yet" until then.
* Switched-off federation gets none either: the `federation` switch in
* lib/Settings/connections.json reads `federation_enabled` itself, and
* integriq shows `disabled`.
*
* @param array<string, mixed> $status The result of FederationService::getStatus().
*
Expand All @@ -215,9 +236,13 @@ public function federationPeersChanged(array $status): bool {
return false;
}

$available = ($status['available'] ?? false) === true;
if ($available === true && ($status['enabled'] ?? false) !== true) {
return false;
}

$blocked = $this->federationBlocker(
available: ($status['available'] ?? false) === true,
enabled: ($status['enabled'] ?? false) === true,
available: $available,
peerCount: count((array) ($status['peers'] ?? []))
);
if ($blocked === null) {
Expand All @@ -237,7 +262,12 @@ public function federationPeersChanged(array $status): bool {
* @spec openspec/changes/adopt-connection-registry/specs/admin-integrations/spec.md#requirement-req-stackiq-conn-002-a-save-asks-integriq-to-look-again-and-a-run-reports-what-it-met
*/
public function federationPulled(array $pull): bool {
[$status, $message] = $this->describePull(pull: $pull);
$described = $this->describePull(pull: $pull);
if ($described === null) {
return false;
}

[$status, $message] = $described;

return $this->report(key: self::KEY_FEDERATION, status: $status, message: $message);
}//end federationPulled()
Expand All @@ -247,16 +277,19 @@ public function federationPulled(array $pull): bool {
*
* @param array<string, mixed> $pull The result of FederationService::pullAllPeers().
*
* @return array{0: string, 1: string} The status and the message.
* @return array{0: string, 1: string}|null The status and the message, or null when federation is switched off.
*
* @spec openspec/changes/adopt-connection-registry/specs/admin-integrations/spec.md#requirement-req-stackiq-conn-002-a-save-asks-integriq-to-look-again-and-a-run-reports-what-it-met
*/
public function describePull(array $pull): array {
public function describePull(array $pull): ?array {
$reason = (string) ($pull['reason'] ?? '');
if (($pull['ok'] ?? false) !== true) {
if ($reason === self::PULL_REASON_SWITCHED_OFF) {
return null;
}

$blocked = $this->federationBlocker(
available: $reason !== 'OpenCatalogi unavailable',
enabled: $reason !== 'federation disabled',
peerCount: 1
);

Expand Down Expand Up @@ -291,26 +324,18 @@ public function describePull(array $pull): array {
}//end describePull()

/**
* After an EOL sync settings save: refresh, then report a switched-off sync.
* After an EOL sync settings save: refresh, and send no report.
*
* @param array<string, mixed> $config The configuration as saved.
* The save may have switched the sync on or off, and the `eol-feed` switch
* in lib/Settings/connections.json reads `enabled` inside
* `eol_sync_config` itself. A run reports what the sync met.
*
* @return bool True when a report was sent.
* @return bool True when the refresh was sent.
*
* @spec openspec/changes/adopt-connection-registry/specs/admin-integrations/spec.md#requirement-req-stackiq-conn-002-a-save-asks-integriq-to-look-again-and-a-run-reports-what-it-met
*/
public function eolSyncConfigSaved(array $config): bool {
if ($this->refresh(key: self::KEY_EOL) === false) {
return false;
}

if (($config['enabled'] ?? false) === true) {
return false;
}

[$status, $message] = self::EOL_REASONS['disabled'];

return $this->report(key: self::KEY_EOL, status: $status, message: $message);
public function eolSyncConfigSaved(): bool {
return $this->refresh(key: self::KEY_EOL);
}//end eolSyncConfigSaved()

/**
Expand All @@ -323,7 +348,12 @@ public function eolSyncConfigSaved(array $config): bool {
* @spec openspec/changes/adopt-connection-registry/specs/admin-integrations/spec.md#requirement-req-stackiq-conn-002-a-save-asks-integriq-to-look-again-and-a-run-reports-what-it-met
*/
public function eolSyncRan(array $runStatus): bool {
[$status, $message] = $this->describeEolRun(runStatus: $runStatus);
$described = $this->describeEolRun(runStatus: $runStatus);
if ($described === null) {
return false;
}

[$status, $message] = $described;

return $this->report(key: self::KEY_EOL, status: $status, message: $message);
}//end eolSyncRan()
Expand All @@ -333,11 +363,11 @@ public function eolSyncRan(array $runStatus): bool {
*
* @param array<string, mixed> $runStatus The status EolSyncService::run() recorded.
*
* @return array{0: string, 1: string} The status and the message.
* @return array{0: string, 1: string}|null The status and the message, or null when the sync is switched off.
*
* @spec openspec/changes/adopt-connection-registry/specs/admin-integrations/spec.md#requirement-req-stackiq-conn-002-a-save-asks-integriq-to-look-again-and-a-run-reports-what-it-met
*/
public function describeEolRun(array $runStatus): array {
public function describeEolRun(array $runStatus): ?array {
if (($runStatus['available'] ?? false) === true) {
return [
'configured',
Expand All @@ -347,6 +377,9 @@ public function describeEolRun(array $runStatus): array {
}

$reason = (string) ($runStatus['reason'] ?? '');
if ($reason === self::EOL_REASON_SWITCHED_OFF) {
return null;
}

return (self::EOL_REASONS[$reason] ?? ['error', 'The last end-of-life sync stopped: ' . $this->shorten(text: $reason)]);
}//end describeEolRun()
Expand Down Expand Up @@ -416,24 +449,19 @@ protected function resolveEventClass(string $eventClass): ?string {
/**
* The state that keeps federation from pulling at all, or null when none does.
*
* Switched-off federation is not a blocker here: the callers leave it to the
* row's switch, which integriq reads itself.
*
* @param bool $available Whether OpenCatalogi is installed.
* @param bool $enabled Whether federation_enabled is on.
* @param int $peerCount How many peers are configured.
*
* @return array{0: string, 1: string}|null The status and the message, or null.
*/
private function federationBlocker(bool $available, bool $enabled, int $peerCount): ?array {
private function federationBlocker(bool $available, int $peerCount): ?array {
if ($available === false) {
return ['unavailable', 'Federation needs the OpenCatalogi app, and it is not installed.'];
}

if ($enabled === false) {
return [
'unconfigured',
'Federation is switched off. Run occ config:app:set stackiq federation_enabled --value=true --type=boolean.',
];
}

if ($peerCount === 0) {
return ['unconfigured', 'Federation is on, and no peer catalog is added yet.'];
}
Expand Down
2 changes: 1 addition & 1 deletion lib/Service/EolSyncService.php
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ public function getConfig(): array {
*/
public function updateConfig(array $data): array {
$result = $this->settingsService->updateEolSyncConfig($data);
$this->connectionReports?->eolSyncConfigSaved(config: (array) ($result['config'] ?? []));
$this->connectionReports?->eolSyncConfigSaved();

return $result;
}//end updateConfig()
Expand Down
9 changes: 9 additions & 0 deletions lib/Settings/connections.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@
"order": 20,
"settingsUrl": "/settings/admin/stackiq#section-federation",
"reportedOnly": true,
"switch": {
"configKey": "federation_enabled"
},
"disabledMessage": "Federation is switched off. Set the federation_enabled app setting to true with occ to switch it on.",
"unconfiguredMessage": "Not checked yet. Choose Pull now in the Catalog federation section to check the peers."
},
{
Expand All @@ -30,6 +34,11 @@
"order": 30,
"settingsUrl": "/settings/admin/stackiq#section-eol-sync",
"reportedOnly": true,
"switch": {
"configKey": "eol_sync_config",
"jsonPath": "enabled"
},
"disabledMessage": "End-of-life sync is switched off. Switch it on in the End-of-life feed sync section.",
"sourceTemplate": "endoflife-date",
"unconfiguredMessage": "Not checked yet. Choose Sync now in the End-of-life feed sync section."
}
Expand Down
16 changes: 8 additions & 8 deletions openspec/changes/adopt-connection-registry/design.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ The `null` transport still reads Simulated: rule 3 sits above every report.
| Stackiq sees | Status | Message |
|---|---|---|
| OpenCatalogi not installed | `unavailable` | "Federation needs the OpenCatalogi app, and it is not installed." |
| `federation_enabled` off | `unconfigured` | names the `occ` command |
| `federation_enabled` off | nothing: the refresh alone, and the switch makes integriq read `disabled` with the declared message | |
| No peers | `unconfigured` | "Federation is on, and no peer catalog is added yet." |
| Ready | nothing | the refresh alone, so the row reads the declared "Not checked yet" |

Expand All @@ -56,14 +56,14 @@ The `null` transport still reads Simulated: rule 3 sits above every report.

A message names a peer by host only, never by its full URL, and cuts a failure reason at 160 characters.

**End-of-life feed, on an EOL sync settings save.** A refresh, and `unconfigured` when `enabled` is off. Otherwise the row reads "Not checked yet" until the next run.
**End-of-life feed, on an EOL sync settings save.** A refresh and no report. When `enabled` is off the switch makes integriq read `disabled`. Otherwise the row reads "Not checked yet" until the next run.

**End-of-life feed, after a run** (Sync now, or `EolSyncJob`). `EolSyncService::run()` already records a status. The report maps its `reason`:

| Reason | Status |
|---|---|
| none, the run completed | `configured`, with the matched and skipped counts |
| `disabled` | `unconfigured` |
| `disabled` | nothing, the switch says it |
| `openregister-not-installed` | `unavailable` |
| `object-service-unavailable` | `error` |
| `module-schema-not-configured` | `unconfigured` |
Expand All @@ -78,15 +78,15 @@ A message names a peer by host only, never by its full URL, and cuts a failure r

- `src/manifest.d/connection-registry.json`: an `index` page `Integrations` at `/settings/integrations`, `requiresApp` integriq, `permission: admin`, `showAdd: false`, and the columns connection, status, status message, last checked and settings.
- Its menu entry `IntegrationsMenu` sits in the settings gear with `query: {app: stackiq}`, `permission: admin` and `visibleIf.appInstalled: integriq`.
- `src/services/connectionRegistry.js` holds the two formatters and `openIntegriqConnections`.
- `App.vue` passes the formatters through CnAppRoot's `formatters` prop. It passed none before this change. `src/customComponents.js` carries the handler, because CnIndexPage resolves a header action's handler against `customComponents`.
- `src/services/connectionRegistry.js` holds `openIntegriqConnections`.
- `App.vue` passes no `formatters`: CnAppRoot supplies the two built-ins. `src/customComponents.js` carries the handler, because CnIndexPage resolves a header action's handler against `customComponents`.

**Formatters.** The installed `@conduction/nextcloud-vue` 2.39.0 ships no `connectionStatus` built-in, so stackiq carries a local copy with all six labels, `limited` included.
**Formatters.** `@conduction/nextcloud-vue` 3.2.0 ships `connectionStatus` and `connectionSettingsLabel` as built-ins, `disabled` included (nextcloud-vue#1173). Stackiq carried a local copy while it resolved 2.39.0, and dropped it on moving to 3.2.0.

## D4. Contract misfits

- **A boolean app-config key.** `federation_enabled` is typed boolean. Integriq's reader answers `typed` for a type conflict, which counts as filled, so a `requiredConfig` on it would read Configured while federation is off. The contract has no way to say "filled and true". `reportedOnly` works around it.
- **A flag inside a blob.** `eol_sync_config` holds `{"enabled": false, …}`. `adapter.jsonPath` reads inside a blob, but only rule 3 uses it, and "switched off" is not "simulated". A `requiredConfig` with a JSON path would fit this row.
- **A boolean app-config key.** `federation_enabled` is typed boolean, and a stored `false` counted as filled. Resolved by hydra#676 (`false` reads empty) and hydra#677: the row declares `switch: {"configKey": "federation_enabled"}` and reads `disabled` while it is off.
- **A flag inside a blob.** `eol_sync_config` holds `{"enabled": false, …}`. Resolved by hydra#677: the row declares `switch: {"configKey": "eol_sync_config", "jsonPath": "enabled"}`. An unset blob has no `enabled` and reads off, which matches `SettingsService::getEolSyncConfig()`'s default.
- **Completeness that depends on the adapter.** Email needs different keys per transport. `requiredConfig` is one fixed list.
- **Gate 116's vendored schema is behind integriq.** `hydra-gates/scripts/schemas/connections.schema.json` on `.github` `main` has no `jsonPath`, `simulatedValues` or `reportedOnly`, so gate 116 warns on every file that uses the hydra#673 fields. The file validates against integriq's own schema on `development`.

Expand Down
4 changes: 2 additions & 2 deletions openspec/changes/adopt-connection-registry/proposal.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,14 @@ Hydra change `connection-registry` (hydra#667, amended in hydra#673 and hydra#67

- New `lib/Settings/connections.json` with three connections: `email`, `federation` and `eol-feed`.
- `email` names `email_transport_type` as its adapter key, and only `null` reads Simulated. An empty value is not simulated: stackiq falls back to SMTP.
- `federation` and `eol-feed` are `reportedOnly`. Only stackiq can see OpenCatalogi, `federation_enabled` (a boolean key) and the sync outcome.
- `federation` and `eol-feed` are `reportedOnly`. Only stackiq can see OpenCatalogi, the peers and the sync outcome. Each declares its on/off setting as a `switch` (hydra#677): `federation_enabled`, and `enabled` inside `eol_sync_config`, so a switched-off feature reads Switched off.
- `eol-feed` offers integriq's `endoflife-date` source as its template.
- The three settings sections get stable ids: `section-email`, `section-federation` and `section-eol-sync`.
- An email settings save, a peer add or remove, and an EOL sync settings save send `ConnectionRefreshRequestedEvent` for that connection, then report what stackiq can see.
- A federation pull and an EOL sync run report their outcome. Both run on a schedule or on the admin's button, never on a page request.
- An Integrations page under the settings gear, over integriq's `app_connection` schema, preset to `app=stackiq`, admin only, and only shown when integriq is installed.
- Add integration opens `/apps/integriq/connections?app=stackiq&link=1`.
- Local `connectionStatus` and `connectionSettingsLabel` formatters with all six statuses, and the strings in English and Dutch.
- The `connectionStatus` and `connectionSettingsLabel` formatters come from `@conduction/nextcloud-vue` 3.2.0, which labels all seven statuses. The page strings are in English and Dutch.

## Depends on

Expand Down
Loading
Loading