Publish curated Knowledge Hub source from 2ed833f290b439a7bc91e5fd19b042c269e3bee9
This commit is contained in:
1 parent
10f2edcd29
commit
59f15b2d08
218 files changed
+23995
-1849
No files matched your search
@@ -186,6 +186,13 @@ At minimum isolate policies for:
|
||||
|
||||
Return a generic response that does not confirm account/content existence.
|
||||
|
||||
The knowledge and unified-search endpoints share a 60-request/minute budget per
|
||||
effective remote IP. Job creation and the compatibility backup-creation endpoint
|
||||
share a 10-request/minute budget. Both use zero-length queues and return 429.
|
||||
Forwarded addresses are accepted only from configured trusted proxies. Durable
|
||||
job admission additionally enforces per-actor, workspace and rolling daily budgets
|
||||
inside PostgreSQL; see [background jobs](background-jobs.md).
|
||||
|
||||
## Security verification
|
||||
|
||||
Release requires:
|
||||
|
||||
+75
-12
@@ -16,7 +16,8 @@ A full backup set contains:
|
||||
5. Product, schema and container/image versions.
|
||||
6. File inventory with sizes and SHA-256 hashes.
|
||||
7. Backup job metadata and timestamps.
|
||||
8. Encryption metadata when operator-managed encryption is enabled.
|
||||
8. A signed integrity receipt authenticating the exact manifest.
|
||||
9. Encryption metadata when operator-managed encryption is enabled.
|
||||
|
||||
Do not include runtime session secrets or plaintext API tokens.
|
||||
|
||||
@@ -42,13 +43,46 @@ mutable files. Deletions use retention, preventing a blob from disappearing duri
|
||||
5. copy/link/archive required blobs without following unsafe paths;
|
||||
6. create manifest;
|
||||
7. verify dump readability and every file hash;
|
||||
8. optionally encrypt with an operator-supplied mechanism;
|
||||
9. atomically publish completed backup;
|
||||
10. apply retention only after a verified new backup exists;
|
||||
11. record audit and administrator-visible result.
|
||||
8. authenticate the manifest hash with HMAC-SHA-256;
|
||||
9. optionally encrypt with an operator-supplied mechanism;
|
||||
10. atomically publish completed backup;
|
||||
11. apply retention only after a verified new backup exists;
|
||||
12. record audit and administrator-visible result.
|
||||
|
||||
A failed backup never replaces the last known-good backup.
|
||||
|
||||
New backup directories and files use private Unix permissions (0700 and 0600).
|
||||
Permission failures stop creation before publication. On Windows, provision and
|
||||
verify restrictive NTFS ACLs for backups, keys and restore targets before use;
|
||||
Unix mode checks do not enforce Windows ACLs. HMAC authenticates backups but
|
||||
does not encrypt their contents, so archive/share access must remain restricted.
|
||||
|
||||
## Backup authenticity key and recovery
|
||||
|
||||
The `.integrity.json` receipt beside a backup is not trusted by itself. The application
|
||||
authenticates its backup ID, exact manifest hash, completion time and verification result
|
||||
with HMAC-SHA-256. The random 32-byte key is stored at
|
||||
`KnowledgeHub:Paths:Keys/backup-integrity.key`; in the supplied Compose deployment this
|
||||
is `/var/lib/knowledge-hub/keys/backup-integrity.key`. This location is outside
|
||||
`KnowledgeHub:Paths:Backups`, so someone who can rewrite a backup and its receipt cannot
|
||||
create a valid signature. The application creates the key when integrity evidence is
|
||||
first written and restricts it to the service account on Unix hosts.
|
||||
|
||||
Treat this key as essential recovery material. Copy the exact key file into an encrypted,
|
||||
access-controlled escrow location separate from the backup storage, and include it in
|
||||
the operator's disaster-recovery procedure. To restore on a replacement host, recover
|
||||
the key to the configured Keys directory before verifying or restoring backups. Never
|
||||
put the key in a backup directory or in the same backup archive as the signed receipts.
|
||||
Protect access to the escrow copy as carefully as access to the backups.
|
||||
|
||||
If the key is missing or unreadable, backup integrity is reported as unknown and restore
|
||||
is refused. Do not replace a lost key with a newly generated one and expect existing
|
||||
backups to validate: a new key authenticates only backups created after it. Recover the
|
||||
original key from escrow first. If no copy exists, the existing backups cannot be
|
||||
cryptographically authenticated by the application and must not be treated as verified.
|
||||
Backups made before authenticated receipts were introduced also remain unverified; create
|
||||
a new backup with this version before relying on application-verified restore.
|
||||
|
||||
## Retention defaults
|
||||
|
||||
Configurable starting policy:
|
||||
@@ -65,13 +99,26 @@ source.
|
||||
|
||||
Restore is deliberately separate from normal application startup.
|
||||
|
||||
1. Select backup.
|
||||
2. Verify manifest, hashes, version compatibility and encryption.
|
||||
3. Create clean PostgreSQL database and empty target directories.
|
||||
4. Restore database.
|
||||
1. Select backup. Copy read-only media to a writable recovery directory first,
|
||||
preserving the manifest, signed receipts and file bytes. Recover the exact
|
||||
`backup-integrity.key` from protected external escrow into the configured Keys
|
||||
directory; never take it from the backup archive. The application acquires an
|
||||
exclusive storage lease there to prevent retention from removing a restore source,
|
||||
and writes its restore receipt there. Keep the original media unchanged.
|
||||
2. Use the backup's recorded application image to create a clean PostgreSQL
|
||||
database at exactly the manifest's schema version. Create an existing, empty
|
||||
target attachments directory on writable storage with room for the full payload.
|
||||
3. Copy the manifest and every listed database and attachment file into a private
|
||||
staging directory inside the target attachments volume. Verify the HMAC against
|
||||
the staged manifest bytes and verify each staged file's size and SHA-256 before
|
||||
opening or changing the target database. Restore reads only from this verified
|
||||
snapshot, so source-volume changes after verification cannot change restored bytes.
|
||||
4. Restore database. The restore service refuses a target whose latest applied
|
||||
migration differs from the authenticated manifest, before inserting any rows.
|
||||
5. Restore attachment blobs by hash.
|
||||
6. Start the recorded/current compatible application in maintenance mode.
|
||||
7. Apply supported migrations.
|
||||
6. Verify the restored data with the recorded application version.
|
||||
7. Upgrade through supported migrations using the intended newer image, then
|
||||
repeat integrity checks before promotion.
|
||||
8. Run integrity checks:
|
||||
- FK/schema;
|
||||
- user/membership;
|
||||
@@ -87,6 +134,21 @@ Restore is deliberately separate from normal application startup.
|
||||
|
||||
Never test restore by overwriting the only production instance.
|
||||
|
||||
Pass target PostgreSQL credentials through `--target-connection-file=<path>`, which
|
||||
reads the connection string from a protected file. On Unix, the file must be readable
|
||||
only by the restore account (mode `0600` or `0400`); the restore command rejects files
|
||||
with group or other access. On Windows, apply an ACL that grants access only to the
|
||||
restore operator/service account. Do not put the connection string in command-line
|
||||
arguments. Restore errors deliberately omit provider exception details because they
|
||||
can contain connection data.
|
||||
|
||||
The real-backup drill also requires the exact integrity key recovered from protected
|
||||
external escrow. Pass only its file path through
|
||||
`KNOWLEDGE_HUB_BACKUP_INTEGRITY_KEY_FILE`; the script copies the key with mode `0600`
|
||||
into its temporary working directory, uses that directory as the restore Keys path,
|
||||
and removes the temporary copy during cleanup. The key contents are never placed in an
|
||||
argument or printed by the script.
|
||||
|
||||
## Backup encryption
|
||||
|
||||
The application may integrate an operator-supplied encryption command or library after
|
||||
@@ -125,11 +187,12 @@ isolated restore proves it is necessary and the operator has assessed newer writ
|
||||
|
||||
- create backup with representative data;
|
||||
- verify manifest;
|
||||
- reject a forged manifest and matching unsigned/invalid integrity receipt;
|
||||
- corrupt dump and blob tests;
|
||||
- clean-target restore;
|
||||
- compare logical counts and hashes;
|
||||
- restore older supported schema then migrate;
|
||||
- missing key/encrypted backup failure;
|
||||
- missing integrity key or encryption key failure;
|
||||
- interrupted backup cleanup;
|
||||
- retention policy;
|
||||
- no secret inclusion;
|
||||
|
||||
+454
-1
@@ -1,7 +1,7 @@
|
||||
openapi: 3.1.0
|
||||
info:
|
||||
title: ItWorx Knowledge Hub API
|
||||
version: 0.1.0-draft
|
||||
version: 1.0.0-rc.1
|
||||
description: |
|
||||
Versioned API for the ItWorx Knowledge Hub web client and scoped automation.
|
||||
This is not an MCP transport. The future MCP service will consume this API.
|
||||
@@ -29,6 +29,9 @@ tags:
|
||||
- name: Activity
|
||||
- name: Workspace
|
||||
- name: Ingestion proposals
|
||||
- name: Preferences
|
||||
- name: DataSources
|
||||
- name: Jobs
|
||||
security:
|
||||
- cookieAuth: []
|
||||
- bearerAuth: []
|
||||
@@ -777,6 +780,59 @@ paths:
|
||||
$ref: "#/components/schemas/SearchResponse"
|
||||
"401":
|
||||
$ref: "#/components/responses/Unauthorized"
|
||||
/unified-search:
|
||||
get:
|
||||
tags: [Search]
|
||||
operationId: searchWorkspaceContent
|
||||
description: >-
|
||||
Human reader session only. Searches visible knowledge, projects, reviewed
|
||||
services, runbooks and decisions in the current workspace. Trashed content
|
||||
and hidden or archived services are excluded. Counts cover all five kinds;
|
||||
`kind` filters the returned page. A `tag` filters exact, case-insensitive
|
||||
page tags on knowledge, runbook and decision results and excludes projects
|
||||
and services. An empty query returns no results unless `tag` is supplied. The
|
||||
endpoint shares the search rate limit of 60 requests per effective remote IP
|
||||
per minute. An invalid `kind` returns 422.
|
||||
parameters:
|
||||
- name: q
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
maxLength: 500
|
||||
- name: kind
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
enum: [knowledge, projects, services, runbooks, decisions]
|
||||
- name: tag
|
||||
in: query
|
||||
description: Exact, case-insensitive page tag; applies to knowledge, runbooks and decisions.
|
||||
schema:
|
||||
type: string
|
||||
maxLength: 64
|
||||
- $ref: "#/components/parameters/Cursor"
|
||||
- name: limit
|
||||
in: query
|
||||
schema:
|
||||
type: integer
|
||||
minimum: 1
|
||||
maximum: 100
|
||||
default: 25
|
||||
responses:
|
||||
"200":
|
||||
description: Permission-filtered, ranked results and complete grouped counts
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/UnifiedSearchResponse"
|
||||
"401":
|
||||
$ref: "#/components/responses/Unauthorized"
|
||||
"403":
|
||||
$ref: "#/components/responses/Forbidden"
|
||||
"422":
|
||||
description: Invalid kind
|
||||
"429":
|
||||
description: Search request budget exhausted
|
||||
/rag/publication-manifest:
|
||||
get:
|
||||
tags: [Rag]
|
||||
@@ -3077,6 +3133,180 @@ paths:
|
||||
application/problem+json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ProblemDetails"
|
||||
/me/preferences:
|
||||
get:
|
||||
tags: [Preferences]
|
||||
operationId: getMyPreferences
|
||||
security: [{ cookieAuth: [] }]
|
||||
responses:
|
||||
"200": { description: Persisted result, content: { application/json: { schema: { $ref: "#/components/schemas/UserPreferences" } } } }
|
||||
"401": { $ref: "#/components/responses/Unauthorized" }
|
||||
"403": { $ref: "#/components/responses/Forbidden" }
|
||||
put:
|
||||
tags: [Preferences]
|
||||
operationId: updateMyPreferences
|
||||
security: [{ cookieAuth: [] }]
|
||||
requestBody: { required: true, content: { application/json: { schema: { $ref: "#/components/schemas/UserPreferencesRequest" } } } }
|
||||
responses:
|
||||
"200": { description: Persisted result, content: { application/json: { schema: { $ref: "#/components/schemas/UserPreferences" } } } }
|
||||
"401": { $ref: "#/components/responses/Unauthorized" }
|
||||
"403": { $ref: "#/components/responses/Forbidden" }
|
||||
"412": { $ref: "#/components/responses/PreconditionFailed" }
|
||||
"428": { $ref: "#/components/responses/PreconditionRequired" }
|
||||
/admin/platform-settings:
|
||||
get:
|
||||
tags: [Admin]
|
||||
operationId: getPlatformSettings
|
||||
security: [{ cookieAuth: [] }]
|
||||
responses:
|
||||
"200": { description: Persisted result, content: { application/json: { schema: { $ref: "#/components/schemas/PlatformSettings" } } } }
|
||||
"401": { $ref: "#/components/responses/Unauthorized" }
|
||||
"403": { $ref: "#/components/responses/Forbidden" }
|
||||
put:
|
||||
tags: [Admin]
|
||||
operationId: updatePlatformSettings
|
||||
security: [{ cookieAuth: [] }]
|
||||
requestBody: { required: true, content: { application/json: { schema: { $ref: "#/components/schemas/PlatformSettingsRequest" } } } }
|
||||
responses:
|
||||
"200": { description: Persisted result, content: { application/json: { schema: { $ref: "#/components/schemas/PlatformSettings" } } } }
|
||||
"401": { $ref: "#/components/responses/Unauthorized" }
|
||||
"403": { $ref: "#/components/responses/Forbidden" }
|
||||
"412": { $ref: "#/components/responses/PreconditionFailed" }
|
||||
"428": { $ref: "#/components/responses/PreconditionRequired" }
|
||||
/admin/data-sources:
|
||||
get:
|
||||
tags: [DataSources]
|
||||
operationId: listDataSources
|
||||
security: [{ cookieAuth: [] }]
|
||||
responses:
|
||||
"200": { description: Persisted result, content: { application/json: { schema: { $ref: "#/components/schemas/DataSourceList" } } } }
|
||||
"401": { $ref: "#/components/responses/Unauthorized" }
|
||||
"403": { $ref: "#/components/responses/Forbidden" }
|
||||
post:
|
||||
tags: [DataSources]
|
||||
operationId: registerDataSource
|
||||
security: [{ cookieAuth: [] }]
|
||||
requestBody: { required: true, content: { application/json: { schema: { $ref: "#/components/schemas/DataSourceRegistration" } } } }
|
||||
responses:
|
||||
"200": { description: Persisted result, content: { application/json: { schema: { $ref: "#/components/schemas/DataSource" } } } }
|
||||
"401": { $ref: "#/components/responses/Unauthorized" }
|
||||
"403": { $ref: "#/components/responses/Forbidden" }
|
||||
/admin/data-sources/{id}:
|
||||
put:
|
||||
tags: [DataSources]
|
||||
operationId: configureDataSource
|
||||
security: [{ cookieAuth: [] }]
|
||||
parameters: [{ name: id, in: path, required: true, schema: { type: string, format: uuid } }]
|
||||
requestBody: { required: true, content: { application/json: { schema: { $ref: "#/components/schemas/DataSourceConfiguration" } } } }
|
||||
responses:
|
||||
"200": { description: Persisted result, content: { application/json: { schema: { $ref: "#/components/schemas/DataSource" } } } }
|
||||
"401": { $ref: "#/components/responses/Unauthorized" }
|
||||
"403": { $ref: "#/components/responses/Forbidden" }
|
||||
"412": { $ref: "#/components/responses/PreconditionFailed" }
|
||||
"428": { $ref: "#/components/responses/PreconditionRequired" }
|
||||
/admin/data-sources/{id}/observations:
|
||||
post:
|
||||
tags: [DataSources]
|
||||
operationId: reportDataSourceObservation
|
||||
security: [{ cookieAuth: [] }]
|
||||
parameters: [{ name: id, in: path, required: true, schema: { type: string, format: uuid } }]
|
||||
requestBody: { required: true, content: { application/json: { schema: { $ref: "#/components/schemas/DataSourceObservation" } } } }
|
||||
responses:
|
||||
"200": { description: Persisted result, content: { application/json: { schema: { $ref: "#/components/schemas/DataSource" } } } }
|
||||
"401": { $ref: "#/components/responses/Unauthorized" }
|
||||
"403": { $ref: "#/components/responses/Forbidden" }
|
||||
/admin/backups/status:
|
||||
get:
|
||||
tags: [Backups]
|
||||
operationId: getBackupStatus
|
||||
security: [{ cookieAuth: [] }]
|
||||
responses:
|
||||
"200": { description: Persisted result, content: { application/json: { schema: { $ref: "#/components/schemas/BackupStatus" } } } }
|
||||
"401": { $ref: "#/components/responses/Unauthorized" }
|
||||
"403": { $ref: "#/components/responses/Forbidden" }
|
||||
/admin/backups/{backupId}/protection:
|
||||
put:
|
||||
tags: [Backups]
|
||||
operationId: setBackupProtection
|
||||
security: [{ cookieAuth: [] }]
|
||||
parameters: [{ name: backupId, in: path, required: true, schema: { type: string } }]
|
||||
requestBody: { required: true, content: { application/json: { schema: { $ref: "#/components/schemas/BackupProtectionRequest" } } } }
|
||||
responses:
|
||||
"200": { description: Persisted result, content: { application/json: { schema: { $ref: "#/components/schemas/BackupEvidence" } } } }
|
||||
"401": { $ref: "#/components/responses/Unauthorized" }
|
||||
"403": { $ref: "#/components/responses/Forbidden" }
|
||||
"412": { $ref: "#/components/responses/PreconditionFailed" }
|
||||
"428": { $ref: "#/components/responses/PreconditionRequired" }
|
||||
/admin/audit-log:
|
||||
get:
|
||||
tags: [Admin]
|
||||
operationId: listAdminAuditLog
|
||||
security: [{ cookieAuth: [] }]
|
||||
responses:
|
||||
"200": { description: Persisted result, content: { application/json: { schema: { $ref: "#/components/schemas/AdminAuditList" } } } }
|
||||
"401": { $ref: "#/components/responses/Unauthorized" }
|
||||
"403": { $ref: "#/components/responses/Forbidden" }
|
||||
/jobs:
|
||||
get:
|
||||
tags: [Jobs]
|
||||
operationId: listBackgroundJobs
|
||||
security: [{ cookieAuth: [] }]
|
||||
responses:
|
||||
"200": { description: Persisted result, content: { application/json: { schema: { $ref: "#/components/schemas/BackgroundJobList" } } } }
|
||||
"401": { $ref: "#/components/responses/Unauthorized" }
|
||||
"403": { $ref: "#/components/responses/Forbidden" }
|
||||
/jobs/course-tagging:
|
||||
post:
|
||||
tags: [Jobs]
|
||||
operationId: queueCourseTaggingJob
|
||||
security: [{ cookieAuth: [] }]
|
||||
parameters: [{ $ref: "#/components/parameters/RequiredIdempotencyKey" }]
|
||||
requestBody: { required: true, content: { application/json: { schema: { $ref: "#/components/schemas/QueueCourseTaggingJobRequest" } } } }
|
||||
responses:
|
||||
"202": { description: Persisted result, content: { application/json: { schema: { $ref: "#/components/schemas/BackgroundJob" } } } }
|
||||
"401": { $ref: "#/components/responses/Unauthorized" }
|
||||
"403": { $ref: "#/components/responses/Forbidden" }
|
||||
/jobs/backups:
|
||||
post:
|
||||
tags: [Jobs]
|
||||
operationId: queueBackupJob
|
||||
security: [{ cookieAuth: [] }]
|
||||
parameters: [{ $ref: "#/components/parameters/RequiredIdempotencyKey" }]
|
||||
requestBody: { required: true, content: { application/json: { schema: { $ref: "#/components/schemas/QueueBackupJobRequest" } } } }
|
||||
responses:
|
||||
"202": { description: Persisted result, content: { application/json: { schema: { $ref: "#/components/schemas/BackgroundJob" } } } }
|
||||
"401": { $ref: "#/components/responses/Unauthorized" }
|
||||
"403": { $ref: "#/components/responses/Forbidden" }
|
||||
/jobs/{jobId}:
|
||||
get:
|
||||
tags: [Jobs]
|
||||
operationId: getBackgroundJob
|
||||
security: [{ cookieAuth: [] }]
|
||||
parameters: [{ name: jobId, in: path, required: true, schema: { type: string, format: uuid } }]
|
||||
responses:
|
||||
"200": { description: Persisted result, content: { application/json: { schema: { $ref: "#/components/schemas/BackgroundJob" } } } }
|
||||
"401": { $ref: "#/components/responses/Unauthorized" }
|
||||
"403": { $ref: "#/components/responses/Forbidden" }
|
||||
/jobs/{jobId}/cancel:
|
||||
post:
|
||||
tags: [Jobs]
|
||||
operationId: cancelBackgroundJob
|
||||
security: [{ cookieAuth: [] }]
|
||||
parameters: [{ name: jobId, in: path, required: true, schema: { type: string, format: uuid } }]
|
||||
responses:
|
||||
"200": { description: Persisted result, content: { application/json: { schema: { $ref: "#/components/schemas/BackgroundJob" } } } }
|
||||
"401": { $ref: "#/components/responses/Unauthorized" }
|
||||
"403": { $ref: "#/components/responses/Forbidden" }
|
||||
/jobs/{jobId}/events:
|
||||
get:
|
||||
tags: [Jobs]
|
||||
operationId: streamBackgroundJobEvents
|
||||
security: [{ cookieAuth: [] }]
|
||||
parameters: [{ name: jobId, in: path, required: true, schema: { type: string, format: uuid } }]
|
||||
responses:
|
||||
"200": { description: Job and progress Server-Sent Events with heartbeat and terminal close, content: { text/event-stream: { schema: { type: string } } } }
|
||||
"401": { $ref: "#/components/responses/Unauthorized" }
|
||||
"403": { $ref: "#/components/responses/Forbidden" }
|
||||
components:
|
||||
securitySchemes:
|
||||
cookieAuth:
|
||||
@@ -3248,6 +3478,194 @@ components:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ProblemDetails"
|
||||
schemas:
|
||||
UserPreferencesRequest:
|
||||
type: object
|
||||
required: [theme, density, primaryNavigationCollapsed, contextPanelOpen, navigationOrder, dashboardWidgetOrder, hiddenDashboardWidgets, defaultLandingPage, catalogView, contextPanelWidth, navigationWidth]
|
||||
properties:
|
||||
theme: { type: string, enum: [system, light, dark] }
|
||||
density: { type: string, enum: [comfortable, compact] }
|
||||
primaryNavigationCollapsed: { type: boolean }
|
||||
contextPanelOpen: { type: boolean }
|
||||
navigationOrder: { type: array, items: { type: string } }
|
||||
dashboardWidgetOrder: { type: array, items: { type: string } }
|
||||
hiddenDashboardWidgets: { type: array, items: { type: string } }
|
||||
defaultLandingPage: { type: string }
|
||||
catalogView: { type: string, enum: [grid, list] }
|
||||
contextPanelWidth: { type: integer, minimum: 256, maximum: 440 }
|
||||
navigationWidth: { type: integer, minimum: 192, maximum: 320 }
|
||||
UserPreferences:
|
||||
allOf:
|
||||
- $ref: "#/components/schemas/UserPreferencesRequest"
|
||||
- type: object
|
||||
required: [schemaVersion, eTag, isDefault, updatedAt]
|
||||
properties:
|
||||
schemaVersion: { type: integer, enum: [1] }
|
||||
eTag: { type: string }
|
||||
isDefault: { type: boolean }
|
||||
updatedAt: { type: [string, "null"], format: date-time }
|
||||
PlatformSettingsRequest:
|
||||
type: object
|
||||
required: [displayName, primaryLocale, timeZone, ingestionProposals, backupPolicyExpectation, inventoryFreshnessPolicy]
|
||||
properties:
|
||||
displayName: { type: string, minLength: 1, maxLength: 200 }
|
||||
primaryLocale: { type: string }
|
||||
timeZone: { type: string }
|
||||
ingestionProposals: { type: object, required: [enabled, maximumSourceCount, maximumSourceBytes, failedRetryLimit], properties: { enabled: { type: boolean }, maximumSourceCount: { type: integer, minimum: 1, maximum: 20 }, maximumSourceBytes: { type: integer, format: int64 }, failedRetryLimit: { type: integer, minimum: 1, maximum: 3 } } }
|
||||
backupPolicyExpectation: { type: object, required: [expectedIntervalHours, scheduleDescription, dailyRetentionCount, weeklyRetentionCount, monthlyRetentionCount, protectedBackupCount, restoreVerificationMaxAgeDays], properties: { expectedIntervalHours: { type: integer }, scheduleDescription: { type: string }, dailyRetentionCount: { type: integer }, weeklyRetentionCount: { type: integer }, monthlyRetentionCount: { type: integer }, protectedBackupCount: { type: integer }, restoreVerificationMaxAgeDays: { type: integer } } }
|
||||
inventoryFreshnessPolicy: { type: object, required: [warningAfterHours], properties: { warningAfterHours: { type: integer } } }
|
||||
PlatformSettings:
|
||||
allOf:
|
||||
- $ref: "#/components/schemas/PlatformSettingsRequest"
|
||||
- type: object
|
||||
required: [schemaVersion, eTag, isDefault, updatedAt]
|
||||
properties:
|
||||
schemaVersion: { type: integer, enum: [1] }
|
||||
eTag: { type: string }
|
||||
isDefault: { type: boolean }
|
||||
updatedAt: { type: [string, "null"], format: date-time }
|
||||
backupPolicyExpectation: { type: object, description: Includes enforcement External; configured schedule is an expectation, not a scheduler run. }
|
||||
DataSourceRegistration:
|
||||
type: object
|
||||
required: [sourceKey, displayName, kind, expectedIntervalHours]
|
||||
properties:
|
||||
sourceKey: { type: string, maxLength: 200 }
|
||||
displayName: { type: string, maxLength: 120 }
|
||||
kind: { type: string, enum: [Inventory, Gitea, KnowledgeClient, ManualIntake, LeafWiki, CuratedPackage] }
|
||||
expectedIntervalHours: { type: integer, minimum: 1, maximum: 8760 }
|
||||
DataSourceConfiguration:
|
||||
type: object
|
||||
required: [displayName, enabled, expectedIntervalHours]
|
||||
properties:
|
||||
displayName: { type: string, maxLength: 120 }
|
||||
enabled: { type: boolean }
|
||||
expectedIntervalHours: { type: integer, minimum: 1, maximum: 8760 }
|
||||
DataSourceObservation:
|
||||
type: object
|
||||
required: [success]
|
||||
properties:
|
||||
success: { type: boolean }
|
||||
recordCount: { type: [integer, "null"], format: int64 }
|
||||
errorCode: { type: [string, "null"] }
|
||||
correlationId: { type: [string, "null"], maxLength: 100 }
|
||||
DataSource:
|
||||
type: object
|
||||
required: [id, sourceKey, displayName, kind, enabled, state, configurationState, syncAuthority, expectedIntervalHours, eTag, actions]
|
||||
properties:
|
||||
id: { type: string, format: uuid }
|
||||
sourceKey: { type: string }
|
||||
displayName: { type: string }
|
||||
kind: { type: string }
|
||||
enabled: { type: boolean }
|
||||
state: { type: string, enum: [Unknown, Current, Stale, Failed, Disabled] }
|
||||
configurationState: { type: string }
|
||||
syncAuthority: { type: string }
|
||||
expectedIntervalHours: { type: integer }
|
||||
lastAttemptAt: { type: [string, "null"], format: date-time }
|
||||
lastSuccessAt: { type: [string, "null"], format: date-time }
|
||||
lastFailureAt: { type: [string, "null"], format: date-time }
|
||||
dataObservedAt: { type: [string, "null"], format: date-time }
|
||||
recordCount: { type: [integer, "null"], format: int64 }
|
||||
lastErrorCode: { type: [string, "null"] }
|
||||
lastCorrelationId: { type: [string, "null"] }
|
||||
eTag: { type: string }
|
||||
actions: { type: array, items: { type: string } }
|
||||
DataSourceList:
|
||||
type: object
|
||||
required: [items, nextCursor]
|
||||
properties:
|
||||
items: { type: array, items: { $ref: "#/components/schemas/DataSource" } }
|
||||
nextCursor: { type: [string, "null"] }
|
||||
BackupProtectionRequest:
|
||||
type: object
|
||||
required: [protected]
|
||||
properties:
|
||||
protected: { type: boolean }
|
||||
BackupEvidence:
|
||||
type: object
|
||||
required: [protected, protectionVersion]
|
||||
properties:
|
||||
protected: { type: boolean }
|
||||
protectionVersion: { type: integer }
|
||||
integrityVerifiedAt: { type: [string, "null"], format: date-time }
|
||||
restoreVerifiedAt: { type: [string, "null"], format: date-time }
|
||||
lastRestoreSucceeded: { type: [boolean, "null"] }
|
||||
BackupStatus:
|
||||
type: object
|
||||
required: [state, scheduleAuthority, scheduleDescription, restoreConfidence, storedBackupBytes, protectedCount, expectedIntervalHours, applicationRetentionCount, dailyRetentionExpectation, weeklyRetentionExpectation, monthlyRetentionExpectation, restoreVerificationMaxAgeDays]
|
||||
properties:
|
||||
state: { type: string, enum: [Unknown, Current, Stale] }
|
||||
scheduleAuthority: { type: string, enum: [External] }
|
||||
scheduleDescription: { type: string }
|
||||
nextScheduledRun: { type: [string, "null"], format: date-time }
|
||||
nextExpectedBy: { type: [string, "null"], format: date-time }
|
||||
lastSuccessfulBackupAt: { type: [string, "null"], format: date-time }
|
||||
lastIntegrityVerifiedAt: { type: [string, "null"], format: date-time }
|
||||
lastVerifiedRestoreAt: { type: [string, "null"], format: date-time }
|
||||
restoreConfidence: { type: string, enum: [Unknown, Stale, Verified] }
|
||||
storedBackupBytes: { type: integer, format: int64, description: Sum of stored backup manifest sizes; not filesystem capacity. }
|
||||
protectedCount: { type: integer }
|
||||
expectedIntervalHours: { type: integer }
|
||||
applicationRetentionCount: { type: integer }
|
||||
dailyRetentionExpectation: { type: integer }
|
||||
weeklyRetentionExpectation: { type: integer }
|
||||
monthlyRetentionExpectation: { type: integer }
|
||||
restoreVerificationMaxAgeDays: { type: integer }
|
||||
AdminAuditList:
|
||||
type: object
|
||||
required: [items, nextCursor]
|
||||
properties:
|
||||
items: { type: array, items: { $ref: "#/components/schemas/AdminAuditItem" } }
|
||||
nextCursor: { type: [integer, "null"], format: int64 }
|
||||
AdminAuditItem:
|
||||
type: object
|
||||
required: [id, actorKind, source, action, summary, occurredAt]
|
||||
properties:
|
||||
id: { type: integer, format: int64 }
|
||||
actorKind: { type: string }
|
||||
actorId: { type: [string, "null"], format: uuid }
|
||||
source: { type: string }
|
||||
action: { type: string }
|
||||
targetType: { type: [string, "null"] }
|
||||
targetId: { type: [string, "null"] }
|
||||
summary: { type: string }
|
||||
occurredAt: { type: string, format: date-time }
|
||||
correlationId: { type: [string, "null"] }
|
||||
QueueCourseTaggingJobRequest:
|
||||
type: object
|
||||
required: [rootPageId, tag]
|
||||
properties:
|
||||
rootPageId: { type: string, format: uuid }
|
||||
tag: { type: string }
|
||||
QueueBackupJobRequest: { type: object, description: Empty object; the job snapshots the current server-side backup policy. }
|
||||
BackgroundJob:
|
||||
type: object
|
||||
required: [id, workspaceId, actorId, type, status, createdAt, cancelRequested, progress, total, processed, succeeded, skipped, failed, errors, version]
|
||||
properties:
|
||||
id: { type: string, format: uuid }
|
||||
workspaceId: { type: string, format: uuid }
|
||||
actorId: { type: string, format: uuid }
|
||||
type: { type: string }
|
||||
status: { type: string, enum: [queued, running, succeeded, partially_succeeded, failed, cancelled] }
|
||||
createdAt: { type: string, format: date-time }
|
||||
startedAt: { type: [string, "null"], format: date-time }
|
||||
completedAt: { type: [string, "null"], format: date-time }
|
||||
cancelRequestedAt: { type: [string, "null"], format: date-time }
|
||||
cancelRequested: { type: boolean }
|
||||
progress: { type: integer, minimum: 0, maximum: 100 }
|
||||
total: { type: integer }
|
||||
processed: { type: integer }
|
||||
succeeded: { type: integer }
|
||||
skipped: { type: integer }
|
||||
failed: { type: integer }
|
||||
errors: { type: array, items: { type: object, properties: { code: { type: string }, message: { type: string }, itemId: { type: [string, "null"], format: uuid } } } }
|
||||
result: { type: [object, "null"] }
|
||||
correlationId: { type: [string, "null"] }
|
||||
version: { type: integer, format: int64 }
|
||||
BackgroundJobList:
|
||||
type: object
|
||||
required: [items]
|
||||
properties:
|
||||
items: { type: array, items: { $ref: "#/components/schemas/BackgroundJob" } }
|
||||
CreateIngestionProposalRequest:
|
||||
type: object
|
||||
required: [manifest]
|
||||
@@ -5243,6 +5661,41 @@ components:
|
||||
$ref: "#/components/schemas/SearchResultItem"
|
||||
nextCursor:
|
||||
type: [string, "null"]
|
||||
UnifiedSearchResponse:
|
||||
type: object
|
||||
required: [items, counts, nextCursor]
|
||||
properties:
|
||||
items:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/UnifiedSearchResultItem"
|
||||
counts:
|
||||
$ref: "#/components/schemas/UnifiedSearchCounts"
|
||||
nextCursor:
|
||||
type: [string, "null"]
|
||||
UnifiedSearchCounts:
|
||||
type: object
|
||||
required: [knowledge, projects, services, runbooks, decisions, total]
|
||||
properties:
|
||||
knowledge: { type: integer, minimum: 0 }
|
||||
projects: { type: integer, minimum: 0 }
|
||||
services: { type: integer, minimum: 0 }
|
||||
runbooks: { type: integer, minimum: 0 }
|
||||
decisions: { type: integer, minimum: 0 }
|
||||
total: { type: integer, minimum: 0 }
|
||||
UnifiedSearchResultItem:
|
||||
type: object
|
||||
required: [kind, id, title, summary, snippet, updatedAt, href]
|
||||
properties:
|
||||
kind:
|
||||
type: string
|
||||
enum: [knowledge, projects, services, runbooks, decisions]
|
||||
id: { type: string, format: uuid }
|
||||
title: { type: string }
|
||||
summary: { type: string }
|
||||
snippet: { type: string }
|
||||
updatedAt: { type: string, format: date-time }
|
||||
href: { type: string, description: Same-origin relative route to the result. }
|
||||
RagPublicationManifestItem:
|
||||
type: object
|
||||
description: >-
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
# Durable background jobs
|
||||
|
||||
Course tagging and manual backup creation run as PostgreSQL-backed jobs. The admin
|
||||
workspace shows persisted progress and supports cancellation. SSE sends the same
|
||||
safe snapshot as the polling endpoint and closes on a terminal state; the browser
|
||||
falls back to polling after an interrupted stream.
|
||||
|
||||
Only an enabled workspace administrator can create or execute a job. Mutation
|
||||
endpoints require cookie authentication and CSRF validation. The worker rechecks
|
||||
the actor's membership before work and before each course page. An idempotency key
|
||||
is scoped to the workspace and actor; reusing it with a different payload returns
|
||||
409. Retrying the same payload returns the existing job, including at capacity.
|
||||
The job row and its `job.queued` audit event commit together. Cancellation requests
|
||||
and their `job.cancel_requested` audit event also commit together; repeating a
|
||||
running-job cancellation returns the current request without adding another audit
|
||||
event.
|
||||
|
||||
The database serializes admission per workspace. Defaults under `KnowledgeHub:Jobs`
|
||||
are `MaxPendingPerWorkspace=10`, `MaxPendingPerActor=3`, and
|
||||
`MaxCreatedPerDay=100` (a rolling 24-hour window). Capacity rejection returns 429
|
||||
before inserting a job or page snapshot. HTTP creation also has a shared budget
|
||||
of ten requests per effective remote IP per minute. Only configured trusted
|
||||
proxies can affect that address. These limits bound distinct-key floods across
|
||||
application restarts and replicas as well as ordinary bursts.
|
||||
|
||||
Course tagging snapshots at most `MaxCoursePages=100000` page IDs in PostgreSQL.
|
||||
The worker updates one current page at a time with its ETag, preserves content,
|
||||
skips existing tags, and records safe per-page failures. A lease is renewed while
|
||||
work is active; expired leases can be reclaimed with bounded retries. Default
|
||||
worker concurrency is two. Cancellation stops future items and preserves the
|
||||
already completed outcomes.
|
||||
|
||||
Terminal jobs retain their counters, bounded errors, result, actor and audit trail.
|
||||
Their page-ID recovery snapshots are removed in the same transaction as the final
|
||||
state. This limits large item-table growth without discarding the job history.
|
||||
The daily admission budget bounds growth of that compact history. Source data is
|
||||
never removed by this maintenance.
|
||||
|
||||
The job `Version` is a monotonic optimistic-concurrency token for persisted worker
|
||||
updates and SSE event IDs. If a cancellation update wins a race with a tracked worker
|
||||
save, the worker retries against the current version while keeping the cancellation
|
||||
request. SSE always emits a terminal event and closes when it observes a terminal
|
||||
job, including when that snapshot has the same version as the last progress event.
|
||||
@@ -65,20 +65,46 @@ af.
|
||||
|
||||
### Stap 2 — herstellen
|
||||
|
||||
Maak eerst een **beschrijfbare herstelkopie** van de gekozen back-up; herstel neemt een
|
||||
exclusieve opslaglease en schrijft achteraf een herstelbewijs naast het manifest. Geef
|
||||
de CLI de doelconnectie via een door de operator beheerd geheimenbestand, niet via een
|
||||
argument met het wachtwoord erin. Op Unix moet dat bestand alleen voor de restoregebruiker
|
||||
leesbaar zijn (`chmod 600` of `chmod 400`); op Windows beperk je de ACL tot de restore-
|
||||
operator/serviceaccount. Het bestand bevat één PostgreSQL-connectiereeks. De
|
||||
integriteitssleutel moet eveneens beschikbaar zijn in de geconfigureerde Keys-map.
|
||||
Haal de exacte `backup-integrity.key` uit beschermde externe escrow; herstel vertrouwt
|
||||
geen sleutel die alleen in de back-upkopie zit. De restore verifieert de manifest-
|
||||
handtekening en kopieert de vermelde bestanden eerst naar een privé-stagingmap binnen
|
||||
het lege doelbijlagenvolume. Reserveer daar vrije ruimte voor ten minste de volledige
|
||||
back-upinhoud. De restore gebruikt uitsluitend die geverifieerde snapshot.
|
||||
|
||||
```bash
|
||||
docker run --rm \
|
||||
--network itworx-knowledge-hub_knowledge-hub-internal \
|
||||
-v knowledge-hub-backups:/var/lib/knowledge-hub/backups:ro \
|
||||
-v <beschrijfbare-herstelkopie>:/var/lib/knowledge-hub/backups:rw \
|
||||
-e KnowledgeHub__Paths__Backups=/var/lib/knowledge-hub/backups \
|
||||
-v <map-met-backup-integriteitssleutel>:/var/lib/knowledge-hub/keys:ro \
|
||||
-e KnowledgeHub__Paths__Keys=/var/lib/knowledge-hub/keys \
|
||||
-v <beveiligd-doelconnectiebestand>:/run/secrets/restore-target-connection:ro \
|
||||
-v <doel-bijlagen-volume>:/var/lib/knowledge-hub/attachments \
|
||||
itworx/knowledge-hub:local \
|
||||
dotnet ItWorx.KnowledgeHub.Api.dll \
|
||||
--restore \
|
||||
--backup-id=<back-up-id> \
|
||||
--target-connection="Host=<doel-db-host>;Port=5432;Database=<doel-db>;Username=<gebruiker>;Password=<wachtwoord>" \
|
||||
--target-connection-file=/run/secrets/restore-target-connection \
|
||||
--target-attachments=/var/lib/knowledge-hub/attachments
|
||||
```
|
||||
|
||||
De echte herstel-drill haalt de back-up via SSH op, maar leest de integriteitssleutel
|
||||
uit een lokaal door de operator beheerd escrowbestand. Geef alleen het bestandspad mee;
|
||||
de drill maakt tijdens uitvoering een tijdelijke kopie met modus `0600` en verwijdert
|
||||
die bij opruimen:
|
||||
|
||||
```bash
|
||||
KNOWLEDGE_HUB_BACKUP_INTEGRITY_KEY_FILE=/secure/escrow/backup-integrity.key \
|
||||
bash scripts/v2-r4-real-backup-restore-drill.sh <back-up-id>
|
||||
```
|
||||
|
||||
De doeldatabase moet **leeg** zijn — herstel weigert expliciet als er al rijen in
|
||||
staan, om nooit per ongeluk bestaande gegevens te vermengen met herstelde gegevens.
|
||||
|
||||
@@ -86,7 +112,8 @@ Het proces rapporteert per tabel het aantal herstelde versus verwachte rijen, he
|
||||
aantal herstelde bijlagen, en sluit af met `GESLAAGD` of `MISLUKT`
|
||||
(afsluitcode 0 respectievelijk 1). Een `MISLUKT`-resultaat laat expliciet zien welke
|
||||
tabel niet overeenkomt — er wordt nooit stilzwijgend een onvolledig herstel als
|
||||
geslaagd gerapporteerd.
|
||||
geslaagd gerapporteerd. Providerfoutdetails worden niet afgedrukt omdat die
|
||||
connectiegegevens kunnen bevatten.
|
||||
|
||||
## Opstartgedrag en migraties
|
||||
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
# Operationele metrieken
|
||||
|
||||
De applicatie exporteert alleen naar OTLP als `OTEL_EXPORTER_OTLP_ENDPOINT` expliciet is gezet. De optionele Compose-overlay `deploy/observability/compose.yaml` schakelt een Collector, Prometheus en Grafana in op een intern Docker-netwerk. Grafana bindt uitsluitend aan de loopback-interface van de host; kies een privaat beheerderswachtwoord via `KNOWLEDGE_HUB_GRAFANA_PASSWORD` buiten Git. Start met beide Compose-bestanden in hetzelfde project nadat de configuratie en opslaglocaties zijn gecontroleerd.
|
||||
|
||||
De Collector neemt OTLP/HTTP op poort 4318 aan en biedt de metrieken aan Prometheus op 8889. Grafana leest Prometheus als geprovisioneerde gegevensbron. De applicatie heeft vaste labels voor operatie, uitkomst, jobstatus en bewijstype. SQL, paginatitels, actoren, zoektermen, tokens en bron-URL's zijn geen metrieklabels.
|
||||
|
||||
De overlay gebruikt vastgelegde image-digests. De collector-default is de
|
||||
beschikbare amd64-build van 0.162.0 voor de Unraid-host. Kies op een andere
|
||||
architectuur expliciet een geverifieerde image via `KNOWLEDGE_HUB_COLLECTOR_IMAGE`.
|
||||
De ongekwalificeerde tag `0.162.0` was tijdens de controle niet beschikbaar.
|
||||
|
||||
Gebruik in Prometheus onder meer `platform_operations_total`, `platform_operation_duration_seconds`, `platform_jobs`, `platform_queue_age_seconds` en `platform_evidence_age_seconds`. Een ontbrekende meting betekent onbekend: na een databasefout worden oudere snapshots uit de gauges verwijderd. De ouderdom van backups telt pas als het nieuwste opgeslagen manifest een geldig integriteitsbewijs heeft. De `inventory`-ouderdom is gebaseerd op de capturetijd van de nieuwste snapshot, niet op de latere importtijd. De applicatie toont het beleid voor externe backups als verwachting, zonder een planner-run te verzinnen.
|
||||
|
||||
Controleer na start een echte taak of zoekactie, vraag dan binnen een export- en scrape-interval `platform_operations_total` op via de Prometheus API en controleer de tijdreeks ook in Grafana. Een lege tijdreeks is geen bewijs van een gezonde pipeline. De geïsoleerde geautomatiseerde OTLP-test verstuurt een echte protobuf-aanvraag naar een lokale receiver en controleert dat private labels ontbreken; de volledige Collector → Prometheus → Grafana-pipeline moet in de beoogde omgeving apart worden geverifieerd.
|
||||
|
||||
Bronnen voor de gebruikte exporterconfiguratie: [OpenTelemetry Collector Prometheus exporter](https://github.com/open-telemetry/opentelemetry-collector-contrib/blob/main/exporter/prometheusexporter/README.md) en [OTLP exporter configuratie](https://opentelemetry.io/docs/languages/sdk-configuration/otlp-exporter/).
|
||||
@@ -0,0 +1,60 @@
|
||||
# Voorkeuren en platformbeheer
|
||||
|
||||
## Persoonlijke instellingen
|
||||
|
||||
Op `/settings` kies je thema, datadichtheid, panelen, landingspagina en de volgorde
|
||||
van navigatie en dashboardwidgets. Aangemelde gebruikers bewaren deze keuzes bij
|
||||
hun account. Het scherm toont laden, opslaan, gesynchroniseerd, offline of een
|
||||
versieconflict. Bij een conflict kun je de servervoorkeuren laden of je lokale
|
||||
keuze opnieuw aanbieden. Gebruik de verplaatsknoppen met toetsenbord of touch;
|
||||
slepen is een extra mogelijkheid.
|
||||
|
||||
## Beheerwerkruimten
|
||||
|
||||
`/admin` bevat het overzicht. De afzonderlijke onderdelen behandelen gebruikers,
|
||||
toegang, integraties, inventaris, kennis, classificaties, achtergrondtaken,
|
||||
back-ups, audit, platforminstellingen en systeeminformatie. Alleen bevoegde
|
||||
beheerders hebben toegang. Een verborgen navigatielink vervangt geen API-controle.
|
||||
|
||||
Op `/admin/platform` bewaar je weergavenaam, locale, tijdzone, intakebeleid,
|
||||
inventarisouderdom en verwachtingen voor externe backups. Verouderde formulieren
|
||||
kunnen geen nieuwere serverversie overschrijven. Laad bij een conflict de actuele
|
||||
waarden opnieuw voordat je wijzigingen opnieuw toepast.
|
||||
|
||||
De backupplanning en dagelijkse, wekelijkse en maandelijkse bewaaraantallen zijn
|
||||
verwachtingen over operatorbeheer. Dit scherm start geen externe planner en voert
|
||||
geen externe retentie uit. Handmatige backups verschijnen als duurzame taken op
|
||||
`/admin/jobs`; de lokale backupdienst hanteert daarnaast haar geconfigureerde
|
||||
retentie en respecteert beschermde backups. Zie [backup en herstel](18-backup-restore.md).
|
||||
|
||||
## Externe bronnen
|
||||
|
||||
Op `/admin/integrations` registreer je een unieke broncode, type, naam en verwacht
|
||||
synchronisatie-interval. Inventarisbronnen worden automatisch geregistreerd bij
|
||||
een geslaagde synchronisatie. Configuratie en waarnemingen zijn afzonderlijk:
|
||||
een ingeschakelde bron is nog geen bewijs dat de laatste run geslaagd is.
|
||||
|
||||
De broncode `inventory:default` is gereserveerd voor de inventarisimporter en kan
|
||||
niet handmatig worden geregistreerd. Uitschakelen van deze bron blokkeert volgende
|
||||
imports. De importer controleert en vergrendelt de bron binnen dezelfde transactie
|
||||
als de inventariswijziging: een eerder bevestigde uitschakeling wordt ook door een
|
||||
reeds gestarte aanvraag gerespecteerd.
|
||||
|
||||
Een operator kan een geslaagde of mislukte externe run rapporteren met een
|
||||
bekende recordtelling en correlatie-ID. Een onbekend aantal blijft leeg. Zo'n
|
||||
rapportage is geen uitgevoerde verbindingstest. De hub voert externe synchronisatie
|
||||
niet zelf uit en bewaart hier geen bronwachtwoorden. De interface toont de laatste
|
||||
poging, laatste geslaagde poging en het tijdstip waarop gegevens zijn waargenomen.
|
||||
|
||||
## Taakvoortgang en operationeel bewijs
|
||||
|
||||
Een cursustag- of back-uptaak blijft op de server bestaan als je het tabblad sluit.
|
||||
De detail-URL bevat het taak-ID. Annuleren stopt toekomstig werk zodra dat veilig
|
||||
kan; reeds afgeronde pagina-updates blijven bewaard. Fouten en aantallen komen
|
||||
uit opgeslagen serverstatus. Zie [achtergrondtaken](background-jobs.md).
|
||||
|
||||
Systeem- en deploymentinformatie tonen uitsluitend gemelde versie- en
|
||||
runtimegegevens. Ontbrekend bewijs blijft onbekend. Gebruik het broncommit, de
|
||||
image-identiteit en de toegepaste migraties samen wanneer je een release of
|
||||
herstel beoordeelt. Een gezonde applicatiecontrole bewijst op zichzelf niet dat
|
||||
de productieversie gelijk is aan de nieuwste repositoryversie.
|
||||
Reference in new issue
Block a user