chore: sync GovOPlaN module split state

This commit is contained in:
2026-07-10 12:51:16 +02:00
parent 0f2a9beca7
commit 970625edff
43 changed files with 2959 additions and 518 deletions

23
.gitignore vendored Normal file
View File

@@ -0,0 +1,23 @@
__pycache__/
*.py[cod]
*.egg-info/
.pytest_cache/
.mypy_cache/
.ruff_cache/
.venv/
build/
dist/
node_modules/
webui/node_modules/
webui/dist/
*.tsbuildinfo
.component-test-build/
.module-test-build/
.policy-test-build/
.template-preview-test-build/
.import-test-build/
webui/.component-test-build/
webui/.module-test-build/
webui/.policy-test-build/
webui/.template-preview-test-build/
webui/.import-test-build/

View File

@@ -3,9 +3,10 @@
`govoplan-admin` owns generic system administration API and WebUI contributions
during the GovOPlaN module split.
This repository owns the live `governance_templates` and
`governance_template_assignments` tables while preserving their historical
table names. It contributes the stable
This repository owns the live `admin_governance_templates` and
`admin_governance_template_assignments` tables. Core migrations rename the
historical unprefixed governance tables for existing development databases. It
contributes the stable
`/api/v1/admin/system/governance-templates` routes and owns governance-template
CRUD plus materialization into access-owned tenant groups and roles.
@@ -23,3 +24,22 @@ section entries through core's `admin.sections` UI capability:
The route shell in access collects those sections at runtime, applies their
scope requirements, and renders them without importing admin package internals.
## Module Lifecycle Administration
The admin module owns the operator surfaces for module lifecycle management:
- installed/enabled/desired module state
- runtime activation and deactivation of installed modules
- signed catalog install planning
- non-destructive uninstall planning, with explicit `destroy_data` retirement
options where a module provides a retirement provider
- installer preflight status, maintenance-mode blockers, migration/restart
checklist entries, and rendered operator commands
- installer daemon request queue, cancellation/retry, recent run summaries,
rollback status, run IDs, request IDs, and trace IDs
Package mutation is intentionally not executed inside the FastAPI request. The
admin UI records operator intent and queues or renders commands for the trusted
installer process described in
`/mnt/DATA/git/govoplan-core/docs/MODULE_ARCHITECTURE.md`.

View File

@@ -19,7 +19,7 @@
],
"peerDependencies": {
"@govoplan/core-webui": "^0.1.6",
"lucide-react": "^0.555.0",
"lucide-react": "^1.23.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-router-dom": "^7.1.1"

View File

@@ -1,35 +0,0 @@
Metadata-Version: 2.4
Name: govoplan-admin
Version: 0.1.4
Summary: GovOPlaN generic administration module.
Author: GovOPlaN
Requires-Python: >=3.12
Description-Content-Type: text/markdown
Requires-Dist: govoplan-core>=0.1.4
Requires-Dist: govoplan-access>=0.1.4
# GovOPlaN Admin
`govoplan-admin` owns generic system administration API and WebUI contributions
during the GovOPlaN module split.
This repository owns the live `governance_templates` and
`governance_template_assignments` tables while preserving their historical
table names. It contributes the stable
`/api/v1/admin/system/governance-templates` routes and owns governance-template
CRUD plus materialization into access-owned tenant groups and roles.
## WebUI Package
The repository root and `webui/` directory both expose the package
`@govoplan/admin-webui`. The package does not contribute the `/admin` route
itself; `govoplan-access` owns the route shell. Instead, admin contributes
section entries through core's `admin.sections` UI capability:
- overview
- system settings
- governance template tenant roles
- governance template groups
The route shell in access collects those sections at runtime, applies their
scope requirements, and renders them without importing admin package internals.

View File

@@ -1,19 +0,0 @@
README.md
pyproject.toml
src/govoplan_admin/__init__.py
src/govoplan_admin/py.typed
src/govoplan_admin.egg-info/PKG-INFO
src/govoplan_admin.egg-info/SOURCES.txt
src/govoplan_admin.egg-info/dependency_links.txt
src/govoplan_admin.egg-info/entry_points.txt
src/govoplan_admin.egg-info/requires.txt
src/govoplan_admin.egg-info/top_level.txt
src/govoplan_admin/backend/__init__.py
src/govoplan_admin/backend/governance.py
src/govoplan_admin/backend/manifest.py
src/govoplan_admin/backend/api/__init__.py
src/govoplan_admin/backend/api/v1/__init__.py
src/govoplan_admin/backend/api/v1/routes.py
src/govoplan_admin/backend/api/v1/schemas.py
src/govoplan_admin/backend/db/__init__.py
src/govoplan_admin/backend/db/models.py

View File

@@ -1,2 +0,0 @@
[govoplan.modules]
admin = govoplan_admin.backend.manifest:get_manifest

View File

@@ -1,2 +0,0 @@
govoplan-core>=0.1.4
govoplan-access>=0.1.4

View File

@@ -1 +0,0 @@
govoplan_admin

View File

@@ -2,17 +2,33 @@ from __future__ import annotations
import os
from pathlib import Path
from typing import Any
from fastapi import APIRouter, Depends, HTTPException, Query, Request, status
from sqlalchemy.orm import Session
from govoplan_admin.backend.governance import create_template, delete_template, update_template
from govoplan_core.admin.settings import get_system_settings
from govoplan_access.backend.auth.dependencies import ApiPrincipal, has_scope, require_any_scope, require_scope
from govoplan_core.audit.logging import audit_from_principal
from govoplan_access.auth import ApiPrincipal, has_scope, require_any_scope, require_scope
from govoplan_core.audit.logging import audit_from_principal, audit_operation_context
from govoplan_admin.backend.db.models import GovernanceTemplate, GovernanceTemplateAssignment
from govoplan_core.admin.common import AdminConflictError, AdminValidationError
from govoplan_core.core.access import CAPABILITY_ACCESS_ADMINISTRATION, AccessAdministration
from govoplan_core.core.change_sequence import (
decode_sequence_watermark,
encode_sequence_watermark,
max_sequence_id,
record_change,
sequence_entries_since,
sequence_watermark_is_expired,
)
from govoplan_core.core.pagination import KeysetCursorError, decode_keyset_cursor, encode_keyset_cursor, keyset_query_fingerprint
from govoplan_core.core.configuration_control import (
ConfigurationChangeApproval,
ConfigurationControlError,
ensure_configuration_change_allowed,
record_configuration_change_applied,
)
from govoplan_core.core.module_management import (
PROTECTED_MODULES,
ModuleInstallPlanItem,
@@ -41,13 +57,20 @@ from govoplan_core.core.module_installer import (
read_module_installer_request,
retry_module_installer_request,
)
from govoplan_core.core.module_license import module_license_decision
from govoplan_core.core.module_license import module_license_decision, module_license_diagnostics
from govoplan_core.core.module_package_catalog import record_module_package_catalog_acceptance, validate_module_package_catalog
from govoplan_core.core.maintenance import MAINTENANCE_ACCESS_SCOPE, MaintenanceMode, saved_maintenance_mode, save_maintenance_mode
from govoplan_core.core.lifecycle import ModuleLifecycleManager
from govoplan_core.core.registry import PlatformRegistry
from govoplan_core.core.runtime import get_registry
from govoplan_core.db.session import get_session
from govoplan_core.i18n import (
i18n_settings,
normalize_enabled_language_codes,
normalize_language_packages,
system_i18n_payload,
update_i18n_settings,
)
from govoplan_core.privacy.retention import privacy_policy_from_settings, set_privacy_policy
from govoplan_core.security.permissions import ALL_PERMISSIONS, effective_permission_count
from govoplan_core.server.registry import available_module_manifests
@@ -59,6 +82,7 @@ from .schemas import (
AdminOverviewResponse,
GovernanceTemplateCreateRequest,
GovernanceTemplateItem,
GovernanceTemplateListDeltaResponse,
GovernanceTemplateListResponse,
GovernanceTemplateUpdateRequest,
MaintenanceModeItem,
@@ -76,14 +100,33 @@ from .schemas import (
ModuleInstallPlanUpdateRequest,
ModulePackageCatalogItem,
ModulePackageCatalogResponse,
ModuleLicenseDiagnostics,
ModuleStateUpdateRequest,
PrivacyRetentionPolicyItem,
SystemSettingsDeltaResponse,
SystemSettingsItem,
SystemSettingsUpdateRequest,
)
router = APIRouter(prefix="/admin", tags=["admin"])
ADMIN_MODULE_ID = "admin"
SYSTEM_SETTINGS_COLLECTION = "admin.system_settings"
SYSTEM_SETTINGS_RESOURCE = "system_settings_section"
GOVERNANCE_TEMPLATES_COLLECTION = "admin.governance_templates"
GOVERNANCE_TEMPLATE_RESOURCE = "governance_template"
INSTALLER_RUNS_CURSOR_SCOPE = "admin.installer.runs.v1"
INSTALLER_REQUESTS_CURSOR_SCOPE = "admin.installer.requests.v1"
DEFAULT_INSTALLER_HISTORY_PAGE_SIZE = 25
SYSTEM_SETTINGS_SECTIONS = (
"defaults",
"tenant_capabilities",
"languages",
"privacy_retention_policy",
"maintenance_mode",
"settings",
)
def _access_administration() -> AccessAdministration:
registry = get_registry()
@@ -113,10 +156,136 @@ def _http_admin_error(exc: Exception) -> HTTPException:
if isinstance(exc, AdminConflictError):
return HTTPException(status_code=status.HTTP_409_CONFLICT, detail=str(exc))
if isinstance(exc, AdminValidationError):
return HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=str(exc))
return HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_CONTENT, detail=str(exc))
return HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc))
def _configuration_control_http_error(exc: ConfigurationControlError) -> HTTPException:
return HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail={"code": exc.code, "message": str(exc), "plan": exc.plan},
)
def _admin_delta_watermark(session: Session, collections: tuple[str, ...]) -> str:
return encode_sequence_watermark(max_sequence_id(session, module_id=ADMIN_MODULE_ID, collections=collections))
def _admin_delta_entries(session: Session, *, collections: tuple[str, ...], since: str, limit: int):
try:
since_sequence = decode_sequence_watermark(since)
except ValueError as exc:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc)) from exc
if sequence_watermark_is_expired(
session,
since=since_sequence,
module_id=ADMIN_MODULE_ID,
collections=collections,
):
return None, False
entries_plus_one = sequence_entries_since(
session,
since=since_sequence,
module_id=ADMIN_MODULE_ID,
collections=collections,
limit=limit + 1,
)
has_more = len(entries_plus_one) > limit
return entries_plus_one[:limit], has_more
def _admin_delta_response_watermark(session: Session, *, collections: tuple[str, ...], entries, has_more: bool) -> str:
return encode_sequence_watermark(entries[-1].id) if has_more and entries else _admin_delta_watermark(session, collections)
def _history_cursor_fingerprint(scope: str, *, page_size: int) -> str:
return keyset_query_fingerprint(scope, {"page_size": page_size, "sort": "id.desc"})
def _window_history_records(records: list[dict[str, object]], *, id_key: str, scope: str, page_size: int, cursor: str | None):
start = 0
full = False
fingerprint = _history_cursor_fingerprint(scope, page_size=page_size)
if cursor:
try:
values = decode_keyset_cursor(scope, cursor, fingerprint=fingerprint)
except KeysetCursorError as exc:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc)) from exc
after_id = values.get(id_key) if values else None
if not isinstance(after_id, str):
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid pagination cursor")
matching_index = next((index for index, item in enumerate(records) if str(item.get(id_key) or "") == after_id), None)
if matching_index is None:
full = True
else:
start = matching_index + 1
page = records[start:start + page_size]
next_cursor = None
if start + page_size < len(records) and page:
next_cursor = encode_keyset_cursor(scope, fingerprint=fingerprint, values={id_key: str(page[-1].get(id_key) or ""), "page_size": page_size})
return page, next_cursor, full
def _system_settings_item(session: Session) -> SystemSettingsItem:
item = get_system_settings(session)
policy = privacy_policy_from_settings(item)
maintenance_mode = saved_maintenance_mode(session)
i18n_payload = system_i18n_payload(item)
return SystemSettingsItem(
default_locale=item.default_locale,
allow_tenant_custom_groups=item.allow_tenant_custom_groups,
allow_tenant_custom_roles=item.allow_tenant_custom_roles,
allow_tenant_api_keys=item.allow_tenant_api_keys,
privacy_retention_policy=PrivacyRetentionPolicyItem.model_validate(policy.model_dump(mode="json")),
maintenance_mode=MaintenanceModeItem.model_validate(maintenance_mode.as_dict()),
available_languages=i18n_payload["available_languages"],
enabled_language_codes=i18n_payload["enabled_languages"],
settings=item.settings or {},
)
def _system_settings_sections(item: SystemSettingsItem) -> dict[str, Any]:
payload = item.model_dump(mode="json")
return {
"defaults": {"default_locale": payload["default_locale"]},
"tenant_capabilities": {
"allow_tenant_custom_groups": payload["allow_tenant_custom_groups"],
"allow_tenant_custom_roles": payload["allow_tenant_custom_roles"],
"allow_tenant_api_keys": payload["allow_tenant_api_keys"],
},
"languages": {
"available_languages": payload["available_languages"],
"enabled_language_codes": payload["enabled_language_codes"],
},
"privacy_retention_policy": payload["privacy_retention_policy"],
"maintenance_mode": payload["maintenance_mode"],
"settings": payload["settings"],
}
def _record_system_settings_section_changes(
session: Session,
*,
before: dict[str, Any],
after: dict[str, Any],
principal: ApiPrincipal,
) -> None:
for section in SYSTEM_SETTINGS_SECTIONS:
if before.get(section) == after.get(section):
continue
record_change(
session,
module_id=ADMIN_MODULE_ID,
collection=SYSTEM_SETTINGS_COLLECTION,
resource_type=SYSTEM_SETTINGS_RESOURCE,
resource_id=section,
operation="updated",
actor_type="user",
actor_id=principal.user.id,
payload={"section": section},
)
def _governance_template_item(session: Session, item: GovernanceTemplate) -> GovernanceTemplateItem:
assignments = session.query(GovernanceTemplateAssignment).filter(
GovernanceTemplateAssignment.template_id == item.id
@@ -136,6 +305,28 @@ def _governance_template_item(session: Session, item: GovernanceTemplate) -> Gov
)
def _record_governance_template_change(session: Session, *, item: GovernanceTemplate, operation: str, principal: ApiPrincipal) -> None:
record_change(
session,
module_id=ADMIN_MODULE_ID,
collection=GOVERNANCE_TEMPLATES_COLLECTION,
resource_type=GOVERNANCE_TEMPLATE_RESOURCE,
resource_id=item.id,
operation=operation,
actor_type="user",
actor_id=principal.user.id,
payload={"kind": item.kind, "slug": item.slug, "name": item.name, "is_active": item.is_active},
)
def _governance_template_deleted_entries(entries, visible_template_ids: set[str]):
return [
{"id": entry.resource_id, "resource_type": entry.resource_type or GOVERNANCE_TEMPLATE_RESOURCE}
for entry in entries
if entry.resource_id and (entry.operation == "deleted" or entry.resource_id not in visible_template_ids)
]
def _module_catalog_response(session: Session, request: Request, *, notes: list[str] | None = None) -> ModuleCatalogResponse:
registry = _request_registry(request)
current_enabled = [manifest.id for manifest in registry.manifests()]
@@ -251,7 +442,7 @@ def _catalog_plan_item(module_id: str) -> tuple[ModuleInstallPlanItem, dict[str,
result = validate_module_package_catalog()
if not result.get("valid"):
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
detail=str(result.get("error") or "Module package catalog is invalid."),
)
for raw_item in result.get("modules", []):
@@ -302,6 +493,14 @@ def _module_package_catalog_item(raw_item: dict[str, object]) -> ModulePackageCa
return ModulePackageCatalogItem.model_validate(payload)
def _catalog_required_license_features(result: dict[str, object]) -> list[str]:
required: list[str] = []
for raw_item in result.get("modules", []):
if isinstance(raw_item, dict):
required.extend(_catalog_license_features(raw_item))
return list(dict.fromkeys(required))
@router.get("/overview", response_model=AdminOverviewResponse)
def admin_overview(
session: Session = Depends(get_session),
@@ -355,16 +554,23 @@ def read_module_install_plan(
@router.get("/system/modules/install-runs", response_model=ModuleInstallerRunListResponse)
def list_module_install_runs(
page_size: int = Query(default=DEFAULT_INSTALLER_HISTORY_PAGE_SIZE, ge=1, le=100),
cursor: str | None = None,
principal: ApiPrincipal = Depends(require_scope("system:settings:read")),
):
del principal
runtime_dir = default_installer_runtime_dir(core_settings.database_url)
records = list(list_module_installer_runs(runtime_dir=runtime_dir, limit=1000))
page, next_cursor, full = _window_history_records(records, id_key="run_id", scope=INSTALLER_RUNS_CURSOR_SCOPE, page_size=page_size, cursor=cursor)
return ModuleInstallerRunListResponse(
runs=[
ModuleInstallerRunSummary.model_validate(item)
for item in list_module_installer_runs(runtime_dir=runtime_dir)
for item in page
],
lock=ModuleInstallerLockStatus.model_validate(module_installer_lock_status(runtime_dir=runtime_dir)),
cursor=cursor,
next_cursor=next_cursor,
full=full,
)
@@ -383,16 +589,23 @@ def read_module_install_run_detail(
@router.get("/system/modules/install-requests", response_model=ModuleInstallerRequestListResponse)
def list_module_install_requests(
page_size: int = Query(default=DEFAULT_INSTALLER_HISTORY_PAGE_SIZE, ge=1, le=100),
cursor: str | None = None,
principal: ApiPrincipal = Depends(require_scope("system:settings:read")),
):
del principal
runtime_dir = default_installer_runtime_dir(core_settings.database_url)
records = list(list_module_installer_requests(runtime_dir=runtime_dir, limit=1000))
page, next_cursor, full = _window_history_records(records, id_key="request_id", scope=INSTALLER_REQUESTS_CURSOR_SCOPE, page_size=page_size, cursor=cursor)
return ModuleInstallerRequestListResponse(
requests=[
ModuleInstallerRequestItem.model_validate(item)
for item in list_module_installer_requests(runtime_dir=runtime_dir)
for item in page
],
daemon=ModuleInstallerDaemonStatus.model_validate(module_installer_daemon_status(runtime_dir=runtime_dir)),
cursor=cursor,
next_cursor=next_cursor,
full=full,
)
@@ -433,7 +646,12 @@ def create_module_install_request(
scope="system",
object_type="module_install_request",
object_id=str(request["request_id"]),
details={"options": payload.options.model_dump(exclude_none=True)},
details=audit_operation_context(
request_id=request.get("request_id"),
outcome=request.get("status"),
trace=request.get("trace") if isinstance(request.get("trace"), dict) else None,
options=payload.options.model_dump(exclude_none=True),
),
)
session.commit()
return ModuleInstallerRequestItem.model_validate(request)
@@ -458,7 +676,7 @@ def cancel_module_install_request(
cancelled_by=principal.user.id,
)
except ModuleInstallerError as exc:
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=str(exc)) from exc
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_CONTENT, detail=str(exc)) from exc
audit_from_principal(
session,
principal,
@@ -466,7 +684,12 @@ def cancel_module_install_request(
scope="system",
object_type="module_install_request",
object_id=request_id,
details={},
details=audit_operation_context(
request_id=request_id,
outcome=request.get("status"),
trace=request.get("trace") if isinstance(request.get("trace"), dict) else None,
cancelled_by=request.get("cancelled_by"),
),
)
session.commit()
return ModuleInstallerRequestItem.model_validate(request)
@@ -491,7 +714,7 @@ def retry_module_install_request(
requested_by=principal.user.id,
)
except ModuleInstallerError as exc:
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=str(exc)) from exc
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_CONTENT, detail=str(exc)) from exc
audit_from_principal(
session,
principal,
@@ -499,7 +722,13 @@ def retry_module_install_request(
scope="system",
object_type="module_install_request",
object_id=request_id,
details={"new_request_id": request["request_id"]},
details=audit_operation_context(
request_id=request_id,
outcome=request.get("status"),
trace=request.get("trace") if isinstance(request.get("trace"), dict) else None,
retry_of=request.get("retry_of"),
new_request_id=request.get("request_id"),
),
)
session.commit()
return ModuleInstallerRequestItem.model_validate(request)
@@ -511,6 +740,7 @@ def read_module_package_catalog(
):
del principal
result = validate_module_package_catalog()
license_diagnostics = module_license_diagnostics(required_features=_catalog_required_license_features(result))
return ModulePackageCatalogResponse(
modules=[_module_package_catalog_item(item) for item in result["modules"] if isinstance(item, dict)],
configured=bool(result["configured"]),
@@ -518,13 +748,17 @@ def read_module_package_catalog(
path=result["path"] if isinstance(result["path"], str) else None,
source=result["source"] if isinstance(result.get("source"), str) else None,
source_type=result["source_type"] if isinstance(result.get("source_type"), str) else None,
cache_used=bool(result.get("cache_used")),
cache_path=result["cache_path"] if isinstance(result.get("cache_path"), str) else None,
channel=result["channel"] if isinstance(result["channel"], str) else None,
sequence=result["sequence"] if isinstance(result.get("sequence"), int) else None,
generated_at=result["generated_at"] if isinstance(result.get("generated_at"), str) else None,
not_before=result["not_before"] if isinstance(result.get("not_before"), str) else None,
expires_at=result["expires_at"] if isinstance(result.get("expires_at"), str) else None,
signed=bool(result["signed"]),
trusted=bool(result["trusted"]),
key_id=result["key_id"] if isinstance(result["key_id"], str) else None,
license=ModuleLicenseDiagnostics.model_validate(license_diagnostics),
warnings=[str(item) for item in result["warnings"]] if isinstance(result["warnings"], list) else [],
error=result["error"] if isinstance(result["error"], str) else None,
)
@@ -542,7 +776,7 @@ def plan_module_install_from_catalog(
plan = _upsert_install_plan_item(session, item)
record_module_package_catalog_acceptance(validation)
except ModuleManagementError as exc:
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=str(exc)) from exc
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_CONTENT, detail=str(exc)) from exc
audit_from_principal(
session,
principal,
@@ -550,7 +784,16 @@ def plan_module_install_from_catalog(
scope="system",
object_type="module_install_plan",
object_id=module_id,
details={"items": [item.as_dict() for item in plan.items]},
details=audit_operation_context(
module_id=module_id,
outcome="planned",
items=[item.as_dict() for item in plan.items],
catalog_validation={
"valid": validation.get("valid"),
"channel": validation.get("channel"),
"key_id": validation.get("key_id"),
},
),
)
session.commit()
return _module_install_plan_response(session, request, notes=[f"Catalog install entry planned for {module_id}."])
@@ -569,14 +812,14 @@ def plan_module_uninstall(
configured_enabled = configured_enabled_modules(core_settings.enabled_modules)
desired_enabled = set(saved_desired_enabled_modules(session, configured_enabled))
if module_id in PROTECTED_MODULES:
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail="Protected platform modules cannot be uninstalled.")
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_CONTENT, detail="Protected platform modules cannot be uninstalled.")
if module_id in current_enabled or module_id in desired_enabled:
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="Disable the module before planning package uninstall.")
try:
item = module_uninstall_plan_item(module_id, lifecycle.available_modules)
plan = _upsert_install_plan_item(session, item)
except ModuleManagementError as exc:
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=str(exc)) from exc
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_CONTENT, detail=str(exc)) from exc
audit_from_principal(
session,
principal,
@@ -584,7 +827,11 @@ def plan_module_uninstall(
scope="system",
object_type="module_install_plan",
object_id=module_id,
details={"items": [item.as_dict() for item in plan.items]},
details=audit_operation_context(
module_id=module_id,
outcome="planned",
items=[item.as_dict() for item in plan.items],
),
)
session.commit()
return _module_install_plan_response(session, request, notes=[f"Package uninstall planned for {module_id}."])
@@ -599,14 +846,28 @@ def update_system_modules(
):
lifecycle = _request_lifecycle(request)
available = lifecycle.available_modules
before_value = {"enabled_modules": list(saved_desired_enabled_modules(session, configured_enabled_modules(core_settings.enabled_modules)))}
apply_value = {"enabled_modules": list(payload.enabled_modules)}
try:
approval = ensure_configuration_change_allowed(
session,
key="module_management.desired_enabled",
value=apply_value,
actor_user_id=principal.user.id,
actor_scopes=tuple(principal.scopes),
change_request_id=payload.change_request_id,
target={"scope": "system"},
)
except ConfigurationControlError as exc:
raise _configuration_control_http_error(exc) from exc
try:
plan = plan_desired_enabled_modules(payload.enabled_modules, available)
except ModuleManagementError as exc:
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=str(exc)) from exc
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_CONTENT, detail=str(exc)) from exc
try:
result = lifecycle.apply_enabled_modules(plan.enabled_modules, protected_modules=PROTECTED_MODULES)
except Exception as exc:
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=str(exc)) from exc
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_CONTENT, detail=str(exc)) from exc
desired = save_desired_enabled_modules(session, result.enabled_modules)
audit_from_principal(
session,
@@ -615,13 +876,24 @@ def update_system_modules(
scope="system",
object_type="module_state",
object_id="global",
details={
"desired_enabled": list(desired),
"added_dependencies": list(plan.added_dependencies),
"activated": list(result.activated_modules),
"deactivated": list(result.deactivated_modules),
"mounted": list(result.mounted_modules),
},
details=audit_operation_context(
outcome="applied",
desired_enabled=list(desired),
added_dependencies=list(plan.added_dependencies),
activated=list(result.activated_modules),
deactivated=list(result.deactivated_modules),
mounted=list(result.mounted_modules),
),
)
record_configuration_change_applied(
session,
key="module_management.desired_enabled",
before_value=before_value,
after_value={"enabled_modules": list(desired)},
actor_user_id=principal.user.id,
approval=approval,
target={"scope": "system"},
audit_event="module_management.updated",
)
session.commit()
notes = ["Module state saved and applied to the running server."]
@@ -641,10 +913,24 @@ def update_module_install_plan(
session: Session = Depends(get_session),
principal: ApiPrincipal = Depends(require_scope("system:settings:write")),
):
before_value = {"items": [item.as_dict() for item in saved_module_install_plan(session).items]}
apply_value = {"items": [item.model_dump() for item in payload.items]}
try:
approval = ensure_configuration_change_allowed(
session,
key="module_management.install_plan",
value=apply_value,
actor_user_id=principal.user.id,
actor_scopes=tuple(principal.scopes),
change_request_id=payload.change_request_id,
target={"scope": "system"},
)
except ConfigurationControlError as exc:
raise _configuration_control_http_error(exc) from exc
try:
plan = save_module_install_plan(session, [item.model_dump() for item in payload.items])
except ModuleManagementError as exc:
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=str(exc)) from exc
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_CONTENT, detail=str(exc)) from exc
audit_from_principal(
session,
principal,
@@ -652,7 +938,21 @@ def update_module_install_plan(
scope="system",
object_type="module_install_plan",
object_id="global",
details={"items": [item.as_dict() for item in plan.items]},
details=audit_operation_context(
outcome="saved",
items=[item.as_dict() for item in plan.items],
item_count=len(plan.items),
),
)
record_configuration_change_applied(
session,
key="module_management.install_plan",
before_value=before_value,
after_value={"items": [item.as_dict() for item in plan.items]},
actor_user_id=principal.user.id,
approval=approval,
target={"scope": "system"},
audit_event="module_install.requested",
)
session.commit()
return _module_install_plan_response(session, request, notes=["Module package install plan saved."])
@@ -672,7 +972,11 @@ def clear_module_install_plan(
scope="system",
object_type="module_install_plan",
object_id="global",
details={"items": [item.as_dict() for item in plan.items]},
details=audit_operation_context(
outcome="cleared",
items=[item.as_dict() for item in plan.items],
item_count=len(plan.items),
),
)
session.commit()
return _module_install_plan_response(session, request, notes=["Module package install plan cleared."])
@@ -683,18 +987,59 @@ def read_system_settings(
session: Session = Depends(get_session),
principal: ApiPrincipal = Depends(require_scope("system:settings:read")),
):
item = get_system_settings(session)
get_system_settings(session)
session.commit()
policy = privacy_policy_from_settings(item)
maintenance_mode = saved_maintenance_mode(session)
return SystemSettingsItem(
default_locale=item.default_locale,
allow_tenant_custom_groups=item.allow_tenant_custom_groups,
allow_tenant_custom_roles=item.allow_tenant_custom_roles,
allow_tenant_api_keys=item.allow_tenant_api_keys,
privacy_retention_policy=PrivacyRetentionPolicyItem.model_validate(policy.model_dump(mode="json")),
maintenance_mode=MaintenanceModeItem.model_validate(maintenance_mode.as_dict()),
settings=item.settings or {},
return _system_settings_item(session)
def _full_system_settings_delta_response(session: Session) -> SystemSettingsDeltaResponse:
item = _system_settings_item(session)
return SystemSettingsDeltaResponse(
item=item,
sections=_system_settings_sections(item),
changed_sections=list(SYSTEM_SETTINGS_SECTIONS),
deleted=[],
watermark=_admin_delta_watermark(session, (SYSTEM_SETTINGS_COLLECTION,)),
has_more=False,
full=True,
)
@router.get("/system/settings/delta", response_model=SystemSettingsDeltaResponse)
def read_system_settings_delta(
since: str | None = None,
limit: int = Query(default=100, ge=1, le=500),
session: Session = Depends(get_session),
principal: ApiPrincipal = Depends(require_scope("system:settings:read")),
):
del principal
get_system_settings(session)
session.commit()
if since is None:
return _full_system_settings_delta_response(session)
entries, has_more = _admin_delta_entries(session, collections=(SYSTEM_SETTINGS_COLLECTION,), since=since, limit=limit)
if entries is None:
return _full_system_settings_delta_response(session)
changed_sections = [
section
for section in SYSTEM_SETTINGS_SECTIONS
if section in {entry.resource_id for entry in entries if entry.resource_type == SYSTEM_SETTINGS_RESOURCE}
]
item = _system_settings_item(session)
sections = _system_settings_sections(item)
return SystemSettingsDeltaResponse(
item=None,
sections={section: sections[section] for section in changed_sections},
changed_sections=changed_sections,
deleted=[],
watermark=_admin_delta_response_watermark(
session,
collections=(SYSTEM_SETTINGS_COLLECTION,),
entries=entries,
has_more=has_more,
),
has_more=has_more,
full=False,
)
@@ -705,10 +1050,45 @@ def write_system_settings(
principal: ApiPrincipal = Depends(require_scope("system:settings:write")),
):
item = get_system_settings(session)
privacy_approval: ConfigurationChangeApproval | None = None
maintenance_approval: ConfigurationChangeApproval | None = None
before_sections = _system_settings_sections(_system_settings_item(session))
before_privacy = privacy_policy_from_settings(item).model_dump(mode="json")
before_maintenance = saved_maintenance_mode(session).as_dict()
if payload.privacy_retention_policy is not None:
privacy_value = payload.privacy_retention_policy.model_dump(mode="json")
try:
privacy_approval = ensure_configuration_change_allowed(
session,
key="privacy_retention_policy",
value=privacy_value,
actor_user_id=principal.user.id,
actor_scopes=tuple(principal.scopes),
change_request_id=payload.change_request_id,
target={"scope": "system"},
)
except ConfigurationControlError as exc:
raise _configuration_control_http_error(exc) from exc
current_i18n = i18n_settings(item.settings)
raw_available = payload.available_languages if "available_languages" in payload.model_fields_set else current_i18n.get("available_languages")
raw_enabled = payload.enabled_language_codes if "enabled_language_codes" in payload.model_fields_set else current_i18n.get("enabled_language_codes", current_i18n.get("enabled_languages"))
available_languages = normalize_language_packages(
[item.model_dump() for item in raw_available] if payload.available_languages is not None else raw_available
)
enabled_language_codes = normalize_enabled_language_codes(
raw_enabled,
available_languages,
default_locale=payload.default_locale,
)
item.default_locale = payload.default_locale.strip() or "en"
item.allow_tenant_custom_groups = payload.allow_tenant_custom_groups
item.allow_tenant_custom_roles = payload.allow_tenant_custom_roles
item.allow_tenant_api_keys = payload.allow_tenant_api_keys
item.settings = update_i18n_settings(
item.settings,
available_languages=available_languages,
enabled_language_codes=enabled_language_codes,
)
if payload.privacy_retention_policy is not None:
set_privacy_policy(item, payload.privacy_retention_policy.model_dump(mode="json"))
if payload.maintenance_mode is not None:
@@ -717,9 +1097,21 @@ def write_system_settings(
enabled=payload.maintenance_mode.enabled,
message=payload.maintenance_mode.message.strip() if payload.maintenance_mode.message else None,
)
try:
maintenance_approval = ensure_configuration_change_allowed(
session,
key="maintenance_mode",
value=next_mode.as_dict(),
actor_user_id=principal.user.id,
actor_scopes=tuple(principal.scopes),
change_request_id=None,
target={"scope": "system"},
)
except ConfigurationControlError as exc:
raise _configuration_control_http_error(exc) from exc
if current.enabled != next_mode.enabled and not has_scope(principal, MAINTENANCE_ACCESS_SCOPE):
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
detail=f"Changing maintenance mode requires {MAINTENANCE_ACCESS_SCOPE}.",
)
save_maintenance_mode(session, next_mode)
@@ -728,8 +1120,32 @@ def write_system_settings(
session, principal, action="system_settings.updated", scope="system", object_type="system_settings", object_id=item.id,
details=payload.model_dump(),
)
if payload.privacy_retention_policy is not None:
record_configuration_change_applied(
session,
key="privacy_retention_policy",
before_value=before_privacy,
after_value=privacy_policy_from_settings(item).model_dump(mode="json"),
actor_user_id=principal.user.id,
approval=privacy_approval,
target={"scope": "system"},
audit_event="privacy_retention.policy_updated",
)
if payload.maintenance_mode is not None:
record_configuration_change_applied(
session,
key="maintenance_mode",
before_value=before_maintenance,
after_value=saved_maintenance_mode(session).as_dict(),
actor_user_id=principal.user.id,
approval=maintenance_approval,
target={"scope": "system"},
audit_event="system_settings.updated",
)
after_sections = _system_settings_sections(_system_settings_item(session))
_record_system_settings_section_changes(session, before=before_sections, after=after_sections, principal=principal)
session.commit()
return read_system_settings(session=session, principal=principal)
return _system_settings_item(session)
@router.get("/system/governance-templates", response_model=GovernanceTemplateListResponse)
@@ -746,12 +1162,63 @@ def list_governance_templates(
return GovernanceTemplateListResponse(templates=[_governance_template_item(session, item) for item in items])
def _full_governance_template_delta_response(session: Session) -> GovernanceTemplateListDeltaResponse:
items = session.query(GovernanceTemplate).order_by(GovernanceTemplate.kind.asc(), GovernanceTemplate.name.asc()).all()
return GovernanceTemplateListDeltaResponse(
templates=[_governance_template_item(session, item) for item in items],
deleted=[],
watermark=_admin_delta_watermark(session, (GOVERNANCE_TEMPLATES_COLLECTION,)),
has_more=False,
full=True,
)
@router.get("/system/governance-templates/delta", response_model=GovernanceTemplateListDeltaResponse)
def list_governance_templates_delta(
since: str | None = None,
limit: int = Query(default=100, ge=1, le=500),
session: Session = Depends(get_session),
principal: ApiPrincipal = Depends(require_scope("system:governance:read")),
):
del principal
if since is None:
return _full_governance_template_delta_response(session)
entries, has_more = _admin_delta_entries(session, collections=(GOVERNANCE_TEMPLATES_COLLECTION,), since=since, limit=limit)
if entries is None:
return _full_governance_template_delta_response(session)
changed_ids = [entry.resource_id for entry in entries if entry.resource_id and entry.operation != "deleted"]
items = []
if changed_ids:
items = session.query(GovernanceTemplate).filter(GovernanceTemplate.id.in_(changed_ids)).order_by(GovernanceTemplate.kind.asc(), GovernanceTemplate.name.asc()).all()
visible_template_ids = {item.id for item in items}
return GovernanceTemplateListDeltaResponse(
templates=[_governance_template_item(session, item) for item in items],
deleted=_governance_template_deleted_entries(entries, visible_template_ids),
watermark=_admin_delta_response_watermark(session, collections=(GOVERNANCE_TEMPLATES_COLLECTION,), entries=entries, has_more=has_more),
has_more=has_more,
full=False,
)
@router.post("/system/governance-templates", response_model=GovernanceTemplateItem, status_code=status.HTTP_201_CREATED)
def create_governance_template(
payload: GovernanceTemplateCreateRequest,
session: Session = Depends(get_session),
principal: ApiPrincipal = Depends(require_scope("system:governance:write")),
):
apply_value = payload.model_dump(exclude={"change_request_id"})
try:
approval = ensure_configuration_change_allowed(
session,
key="governance_templates",
value=apply_value,
actor_user_id=principal.user.id,
actor_scopes=tuple(principal.scopes),
change_request_id=payload.change_request_id,
target={"kind": payload.kind, "slug": payload.slug},
)
except ConfigurationControlError as exc:
raise _configuration_control_http_error(exc) from exc
try:
item = create_template(
session,
@@ -774,6 +1241,17 @@ def create_governance_template(
object_id=item.id,
details={"slug": item.slug, "tenant_ids": [assignment.tenant_id for assignment in payload.assignments]},
)
record_configuration_change_applied(
session,
key="governance_templates",
before_value=None,
after_value={**apply_value, "id": item.id},
actor_user_id=principal.user.id,
approval=approval,
target={"kind": item.kind, "id": item.id},
audit_event="governance_template.updated",
)
_record_governance_template_change(session, item=item, operation="created", principal=principal)
session.commit()
return _governance_template_item(session, item)
@@ -788,6 +1266,20 @@ def update_governance_template(
item = session.get(GovernanceTemplate, template_id)
if item is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Governance template not found")
before_value = _governance_template_item(session, item).model_dump(mode="json")
apply_value = payload.model_dump(exclude={"change_request_id"})
try:
approval = ensure_configuration_change_allowed(
session,
key="governance_templates",
value=apply_value,
actor_user_id=principal.user.id,
actor_scopes=tuple(principal.scopes),
change_request_id=payload.change_request_id,
target={"kind": item.kind, "id": item.id},
)
except ConfigurationControlError as exc:
raise _configuration_control_http_error(exc) from exc
try:
update_template(
session,
@@ -809,6 +1301,17 @@ def update_governance_template(
object_id=item.id,
details={"tenant_ids": [assignment.tenant_id for assignment in payload.assignments]},
)
record_configuration_change_applied(
session,
key="governance_templates",
before_value=before_value,
after_value=_governance_template_item(session, item).model_dump(mode="json"),
actor_user_id=principal.user.id,
approval=approval,
target={"kind": item.kind, "id": item.id},
audit_event="governance_template.updated",
)
_record_governance_template_change(session, item=item, operation="updated", principal=principal)
session.commit()
return _governance_template_item(session, item)
@@ -816,6 +1319,7 @@ def update_governance_template(
@router.delete("/system/governance-templates/{template_id}", status_code=status.HTTP_204_NO_CONTENT)
def remove_governance_template(
template_id: str,
change_request_id: str | None = Query(default=None),
session: Session = Depends(get_session),
principal: ApiPrincipal = Depends(require_scope("system:governance:write")),
):
@@ -823,6 +1327,20 @@ def remove_governance_template(
if item is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Governance template not found")
kind = item.kind
before_value = _governance_template_item(session, item).model_dump(mode="json")
apply_value = {"id": item.id, "kind": item.kind, "delete": True}
try:
approval = ensure_configuration_change_allowed(
session,
key="governance_templates",
value=apply_value,
actor_user_id=principal.user.id,
actor_scopes=tuple(principal.scopes),
change_request_id=change_request_id,
target={"kind": item.kind, "id": item.id},
)
except ConfigurationControlError as exc:
raise _configuration_control_http_error(exc) from exc
try:
delete_template(session, item)
except (AdminConflictError, AdminValidationError) as exc:
@@ -836,5 +1354,16 @@ def remove_governance_template(
object_id=template_id,
details={},
)
record_configuration_change_applied(
session,
key="governance_templates",
before_value=before_value,
after_value=None,
actor_user_id=principal.user.id,
approval=approval,
target={"kind": kind, "id": template_id},
audit_event="governance_template.updated",
)
_record_governance_template_change(session, item=item, operation="deleted", principal=principal)
session.commit()
return None

View File

@@ -5,6 +5,8 @@ from typing import Any, Literal
from pydantic import BaseModel, ConfigDict, Field, field_validator
from govoplan_core.api.v1.schemas import DeltaDeletedItem
RETENTION_DAY_KEYS = (
"raw_campaign_json_retention_days",
@@ -65,6 +67,14 @@ class MaintenanceModeItem(BaseModel):
message: str | None = Field(default=None, max_length=500)
class LanguagePackageItem(BaseModel):
model_config = ConfigDict(extra="forbid")
code: str = Field(min_length=1, max_length=20)
label: str = Field(min_length=1, max_length=100)
native_label: str | None = Field(default=None, max_length=100)
class AdminOverviewResponse(BaseModel):
active_tenant_id: str
active_tenant_name: str
@@ -87,9 +97,21 @@ class SystemSettingsItem(BaseModel):
allow_tenant_api_keys: bool = True
privacy_retention_policy: PrivacyRetentionPolicyItem = Field(default_factory=PrivacyRetentionPolicyItem)
maintenance_mode: MaintenanceModeItem = Field(default_factory=MaintenanceModeItem)
available_languages: list[LanguagePackageItem] = Field(default_factory=list)
enabled_language_codes: list[str] = Field(default_factory=list)
settings: dict[str, Any] = Field(default_factory=dict)
class SystemSettingsDeltaResponse(BaseModel):
item: SystemSettingsItem | None = None
sections: dict[str, Any] = Field(default_factory=dict)
changed_sections: list[str] = Field(default_factory=list)
deleted: list[DeltaDeletedItem] = Field(default_factory=list)
watermark: str | None = None
has_more: bool = False
full: bool = False
class SystemSettingsUpdateRequest(BaseModel):
model_config = ConfigDict(extra="forbid")
@@ -99,6 +121,9 @@ class SystemSettingsUpdateRequest(BaseModel):
allow_tenant_api_keys: bool
privacy_retention_policy: PrivacyRetentionPolicyItem | None = None
maintenance_mode: MaintenanceModeItem | None = None
available_languages: list[LanguagePackageItem] | None = None
enabled_language_codes: list[str] | None = None
change_request_id: str | None = None
class ModuleCatalogItem(BaseModel):
@@ -143,6 +168,7 @@ class ModuleStateUpdateRequest(BaseModel):
model_config = ConfigDict(extra="forbid")
enabled_modules: list[str] = Field(default_factory=list)
change_request_id: str | None = None
class ModuleInstallPlanItem(BaseModel):
@@ -213,6 +239,9 @@ class ModuleInstallerRunSummary(BaseModel):
status: str
started_at: str | None = None
finished_at: str | None = None
request_id: str | None = None
requested_by: str | None = None
trace: dict[str, Any] | None = None
rollback_status: str | None = None
supervisor_status: str | None = None
error: str | None = None
@@ -224,6 +253,9 @@ class ModuleInstallerRunSummary(BaseModel):
class ModuleInstallerRunListResponse(BaseModel):
runs: list[ModuleInstallerRunSummary] = Field(default_factory=list)
lock: ModuleInstallerLockStatus
cursor: str | None = None
next_cursor: str | None = None
full: bool = False
class ModuleInstallerRequestOptions(BaseModel):
@@ -256,6 +288,7 @@ class ModuleInstallerRequestItem(BaseModel):
cancelled_at: str | None = None
cancelled_by: str | None = None
retry_of: str | None = None
trace: dict[str, Any] | None = None
error: str | None = None
record_path: str | None = None
options: dict[str, Any] = Field(default_factory=dict)
@@ -264,6 +297,9 @@ class ModuleInstallerRequestItem(BaseModel):
class ModuleInstallerRequestListResponse(BaseModel):
requests: list[ModuleInstallerRequestItem] = Field(default_factory=list)
daemon: ModuleInstallerDaemonStatus
cursor: str | None = None
next_cursor: str | None = None
full: bool = False
class ModuleInstallerRequestCreateRequest(BaseModel):
@@ -291,6 +327,27 @@ class ModulePackageCatalogItem(BaseModel):
tags: list[str] = Field(default_factory=list)
class ModuleLicenseDiagnostics(BaseModel):
configured: bool = False
valid: bool = True
path: str | None = None
license_id: str | None = None
subject: str | None = None
features: list[str] = Field(default_factory=list)
valid_from: str | None = None
valid_until: str | None = None
signed: bool = False
trusted: bool = False
key_id: str | None = None
enforced: bool = False
allowed: bool = True
required_features: list[str] = Field(default_factory=list)
missing_features: list[str] = Field(default_factory=list)
expires_in_days: int | None = None
reason: str | None = None
guidance: list[str] = Field(default_factory=list)
class ModulePackageCatalogResponse(BaseModel):
modules: list[ModulePackageCatalogItem] = Field(default_factory=list)
configured: bool = False
@@ -298,13 +355,17 @@ class ModulePackageCatalogResponse(BaseModel):
path: str | None = None
source: str | None = None
source_type: str | None = None
cache_used: bool = False
cache_path: str | None = None
channel: str | None = None
sequence: int | None = None
generated_at: str | None = None
not_before: str | None = None
expires_at: str | None = None
signed: bool = False
trusted: bool = False
key_id: str | None = None
license: ModuleLicenseDiagnostics = Field(default_factory=ModuleLicenseDiagnostics)
warnings: list[str] = Field(default_factory=list)
error: str | None = None
@@ -313,6 +374,7 @@ class ModuleInstallPlanUpdateRequest(BaseModel):
model_config = ConfigDict(extra="forbid")
items: list[ModuleInstallPlanItem] = Field(default_factory=list)
change_request_id: str | None = None
class GovernanceAssignment(BaseModel):
@@ -338,6 +400,14 @@ class GovernanceTemplateListResponse(BaseModel):
templates: list[GovernanceTemplateItem]
class GovernanceTemplateListDeltaResponse(BaseModel):
templates: list[GovernanceTemplateItem] = Field(default_factory=list)
deleted: list[DeltaDeletedItem] = Field(default_factory=list)
watermark: str | None = None
has_more: bool = False
full: bool = False
class GovernanceTemplateCreateRequest(BaseModel):
model_config = ConfigDict(extra="forbid")
@@ -348,6 +418,7 @@ class GovernanceTemplateCreateRequest(BaseModel):
permissions: list[str] = Field(default_factory=list)
is_active: bool = True
assignments: list[GovernanceAssignment] = Field(default_factory=list)
change_request_id: str | None = None
class GovernanceTemplateUpdateRequest(BaseModel):
@@ -358,3 +429,4 @@ class GovernanceTemplateUpdateRequest(BaseModel):
permissions: list[str] = Field(default_factory=list)
is_active: bool = True
assignments: list[GovernanceAssignment] = Field(default_factory=list)
change_request_id: str | None = None

View File

@@ -13,7 +13,7 @@ def new_uuid() -> str:
class GovernanceTemplate(Base, TimestampMixin):
__tablename__ = "governance_templates"
__tablename__ = "admin_governance_templates"
__table_args__ = (UniqueConstraint("kind", "slug", name="uq_governance_templates_kind_slug"),)
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_uuid)
@@ -26,12 +26,12 @@ class GovernanceTemplate(Base, TimestampMixin):
class GovernanceTemplateAssignment(Base, TimestampMixin):
__tablename__ = "governance_template_assignments"
__tablename__ = "admin_governance_template_assignments"
__table_args__ = (UniqueConstraint("template_id", "tenant_id", name="uq_governance_template_tenant"),)
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_uuid)
template_id: Mapped[str] = mapped_column(ForeignKey("governance_templates.id", ondelete="CASCADE"), nullable=False, index=True)
tenant_id: Mapped[str] = mapped_column(ForeignKey("tenants.id", ondelete="CASCADE"), nullable=False, index=True)
template_id: Mapped[str] = mapped_column(ForeignKey("admin_governance_templates.id", ondelete="CASCADE"), nullable=False, index=True)
tenant_id: Mapped[str] = mapped_column(ForeignKey("tenancy_tenants.id", ondelete="CASCADE"), nullable=False, index=True)
mode: Mapped[str] = mapped_column(String(20), default="available", nullable=False)

View File

@@ -14,7 +14,7 @@
},
"peerDependencies": {
"@govoplan/core-webui": "^0.1.6",
"lucide-react": "^0.555.0",
"lucide-react": "^1.23.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-router-dom": "^7.1.1"

View File

@@ -1,4 +1,4 @@
import type { ApiSettings } from "@govoplan/core-webui";
import type { ApiSettings, DeltaDeletedItem } from "@govoplan/core-webui";
import { apiFetch } from "@govoplan/core-webui";
export type PermissionItem = {
@@ -75,6 +75,12 @@ export type MaintenanceMode = {
message?: string | null;
};
export type LanguagePackage = {
code: string;
label: string;
native_label?: string | null;
};
export type SystemSettingsItem = {
default_locale: string;
allow_tenant_custom_groups: boolean;
@@ -82,9 +88,20 @@ export type SystemSettingsItem = {
allow_tenant_api_keys: boolean;
privacy_retention_policy: PrivacyRetentionPolicy;
maintenance_mode: MaintenanceMode;
available_languages: LanguagePackage[];
enabled_language_codes: string[];
settings: Record<string, unknown>;
};
export type SystemSettingsDeltaSections = Partial<{
defaults: Pick<SystemSettingsItem, "default_locale">;
tenant_capabilities: Pick<SystemSettingsItem, "allow_tenant_custom_groups" | "allow_tenant_custom_roles" | "allow_tenant_api_keys">;
languages: Pick<SystemSettingsItem, "available_languages" | "enabled_language_codes">;
privacy_retention_policy: SystemSettingsItem["privacy_retention_policy"];
maintenance_mode: SystemSettingsItem["maintenance_mode"];
settings: SystemSettingsItem["settings"];
}>;
export type SystemSettingsUpdatePayload = {
default_locale: string;
allow_tenant_custom_groups: boolean;
@@ -92,8 +109,106 @@ export type SystemSettingsUpdatePayload = {
allow_tenant_api_keys: boolean;
privacy_retention_policy?: PrivacyRetentionPolicy | null;
maintenance_mode?: MaintenanceMode | null;
available_languages?: LanguagePackage[] | null;
enabled_language_codes?: string[] | null;
change_request_id?: string | null;
};
export type ConfigurationChangeRequest = {
id: string;
key: string;
label?: string;
target?: Record<string, unknown>;
dry_run: boolean;
requested_by: string;
requested_at: string;
updated_at: string;
status: string;
approvals: Array<Record<string, unknown>>;
plan: Record<string, unknown>;
value_preview?: unknown;
};
export type ConfigurationChangeRecord = {
id: string;
version: number;
key: string;
target?: Record<string, unknown>;
actor_user_id: string;
approval_request_id?: string | null;
approval_user_ids: string[];
before?: unknown;
after?: unknown;
rollback_value?: unknown;
status: string;
created_at: string;
};
export type ConfigurationPackageDiagnostic = {
severity: "blocker" | "warning" | "info";
code: string;
message: string;
module_id?: string | null;
object_ref?: string | null;
resolution?: string | null;
};
export type ConfigurationPackageRequiredData = {
key: string;
label: string;
data_type: string;
required: boolean;
secret: boolean;
description?: string | null;
};
export type ConfigurationPackagePlanItem = {
action: "create" | "update" | "bind" | "skip" | "blocked" | "noop";
module_id: string;
fragment_type: string;
fragment_id?: string | null;
summary?: string | null;
};
export type ConfigurationPackageFragment = {
module_id: string;
fragment_type: string;
fragment_id?: string | null;
payload: Record<string, unknown>;
};
export type ConfigurationPackageRunPayload = {
package: Record<string, unknown>;
tenant_id?: string | null;
supplied_data?: Record<string, unknown>;
change_request_id?: string | null;
};
type DeltaResponseFields = {
deleted: DeltaDeletedItem[];
watermark?: string | null;
has_more: boolean;
full: boolean;
};
function deltaSuffix(options: { since?: string | null; limit?: number } = {}): string {
const params = new URLSearchParams();
if (options.since) params.set("since", options.since);
if (options.limit) params.set("limit", String(options.limit));
return params.toString() ? `?${params.toString()}` : "";
}
export type SystemSettingsDeltaResponse = {
item?: SystemSettingsItem | null;
sections: SystemSettingsDeltaSections;
changed_sections: string[];
} & DeltaResponseFields;
export type ConfigurationChangesDeltaResponse = {
requests: ConfigurationChangeRequest[];
history: ConfigurationChangeRecord[];
} & DeltaResponseFields;
export type ModuleCatalogItem = {
id: string;
name: string;
@@ -196,6 +311,9 @@ export type ModuleInstallerRunSummary = {
status: string;
started_at?: string | null;
finished_at?: string | null;
request_id?: string | null;
requested_by?: string | null;
trace?: Record<string, unknown> | null;
rollback_status?: string | null;
supervisor_status?: string | null;
error?: string | null;
@@ -207,6 +325,9 @@ export type ModuleInstallerRunSummary = {
export type ModuleInstallerRunListResponse = {
runs: ModuleInstallerRunSummary[];
lock: ModuleInstallerLockStatus;
cursor?: string | null;
next_cursor?: string | null;
full?: boolean;
};
export type ModuleInstallerRequestOptions = {
@@ -235,6 +356,7 @@ export type ModuleInstallerRequestItem = {
cancelled_at?: string | null;
cancelled_by?: string | null;
retry_of?: string | null;
trace?: Record<string, unknown> | null;
error?: string | null;
record_path?: string | null;
options: Record<string, unknown>;
@@ -243,6 +365,9 @@ export type ModuleInstallerRequestItem = {
export type ModuleInstallerRequestListResponse = {
requests: ModuleInstallerRequestItem[];
daemon: ModuleInstallerDaemonStatus;
cursor?: string | null;
next_cursor?: string | null;
full?: boolean;
};
export type ModulePackageCatalogItem = {
@@ -264,6 +389,27 @@ export type ModulePackageCatalogItem = {
tags: string[];
};
export type ModuleLicenseDiagnostics = {
configured: boolean;
valid: boolean;
path?: string | null;
license_id?: string | null;
subject?: string | null;
features: string[];
valid_from?: string | null;
valid_until?: string | null;
signed: boolean;
trusted: boolean;
key_id?: string | null;
enforced: boolean;
allowed: boolean;
required_features: string[];
missing_features: string[];
expires_in_days?: number | null;
reason?: string | null;
guidance: string[];
};
export type ModulePackageCatalogResponse = {
modules: ModulePackageCatalogItem[];
configured: boolean;
@@ -271,13 +417,17 @@ export type ModulePackageCatalogResponse = {
path?: string | null;
source?: string | null;
source_type?: string | null;
cache_used: boolean;
cache_path?: string | null;
channel?: string | null;
sequence?: number | null;
generated_at?: string | null;
not_before?: string | null;
expires_at?: string | null;
signed: boolean;
trusted: boolean;
key_id?: string | null;
license: ModuleLicenseDiagnostics;
warnings: string[];
error?: string | null;
};
@@ -319,6 +469,11 @@ export function fetchSystemSettings(settings: ApiSettings): Promise<SystemSettin
return apiFetch(settings, "/api/v1/admin/system/settings");
}
export function fetchSystemSettingsDelta(settings: ApiSettings, options: { since?: string | null; limit?: number } = {}): Promise<SystemSettingsDeltaResponse> {
const suffix = deltaSuffix(options);
return apiFetch(settings, `/api/v1/admin/system/settings/delta${suffix}`);
}
export function updateSystemSettings(settings: ApiSettings, payload: SystemSettingsUpdatePayload): Promise<SystemSettingsItem> {
return apiFetch(settings, "/api/v1/admin/system/settings", { method: "PATCH", body: JSON.stringify(payload) });
}
@@ -327,10 +482,10 @@ export function fetchModuleCatalog(settings: ApiSettings): Promise<ModuleCatalog
return apiFetch(settings, "/api/v1/admin/system/modules");
}
export function updateModuleState(settings: ApiSettings, enabledModules: string[]): Promise<ModuleCatalogResponse> {
export function updateModuleState(settings: ApiSettings, enabledModules: string[], changeRequestId?: string | null): Promise<ModuleCatalogResponse> {
return apiFetch(settings, "/api/v1/admin/system/modules", {
method: "PUT",
body: JSON.stringify({ enabled_modules: enabledModules })
body: JSON.stringify({ enabled_modules: enabledModules, change_request_id: changeRequestId ?? null })
});
}
@@ -338,12 +493,20 @@ export function fetchModuleInstallPlan(settings: ApiSettings): Promise<ModuleIns
return apiFetch(settings, "/api/v1/admin/system/modules/install-plan");
}
export function fetchModuleInstallerRuns(settings: ApiSettings): Promise<ModuleInstallerRunListResponse> {
return apiFetch(settings, "/api/v1/admin/system/modules/install-runs");
export function fetchModuleInstallerRuns(settings: ApiSettings, options: { page_size?: number; cursor?: string | null } = {}): Promise<ModuleInstallerRunListResponse> {
const params = new URLSearchParams();
if (options.page_size) params.set("page_size", String(options.page_size));
if (options.cursor) params.set("cursor", options.cursor);
const suffix = params.toString() ? `?${params.toString()}` : "";
return apiFetch(settings, `/api/v1/admin/system/modules/install-runs${suffix}`);
}
export function fetchModuleInstallerRequests(settings: ApiSettings): Promise<ModuleInstallerRequestListResponse> {
return apiFetch(settings, "/api/v1/admin/system/modules/install-requests");
export function fetchModuleInstallerRequests(settings: ApiSettings, options: { page_size?: number; cursor?: string | null } = {}): Promise<ModuleInstallerRequestListResponse> {
const params = new URLSearchParams();
if (options.page_size) params.set("page_size", String(options.page_size));
if (options.cursor) params.set("cursor", options.cursor);
const suffix = params.toString() ? `?${params.toString()}` : "";
return apiFetch(settings, `/api/v1/admin/system/modules/install-requests${suffix}`);
}
export function createModuleInstallerRequest(settings: ApiSettings, options: ModuleInstallerRequestOptions): Promise<ModuleInstallerRequestItem> {
@@ -373,10 +536,10 @@ export function planModuleUninstall(settings: ApiSettings, moduleId: string): Pr
return apiFetch(settings, `/api/v1/admin/system/modules/${encodeURIComponent(moduleId)}/uninstall-plan`, { method: "POST" });
}
export function updateModuleInstallPlan(settings: ApiSettings, items: ModuleInstallPlanItem[]): Promise<ModuleInstallPlanResponse> {
export function updateModuleInstallPlan(settings: ApiSettings, items: ModuleInstallPlanItem[], changeRequestId?: string | null): Promise<ModuleInstallPlanResponse> {
return apiFetch(settings, "/api/v1/admin/system/modules/install-plan", {
method: "PUT",
body: JSON.stringify({ items })
body: JSON.stringify({ items, change_request_id: changeRequestId ?? null })
});
}
@@ -390,14 +553,88 @@ export async function fetchGovernanceTemplates(settings: ApiSettings, kind?: "gr
return response.templates;
}
export function createGovernanceTemplate(settings: ApiSettings, payload: Omit<GovernanceTemplateItem, "id" | "created_at" | "updated_at" | "effective_permission_count">): Promise<GovernanceTemplateItem> {
export function createGovernanceTemplate(settings: ApiSettings, payload: Omit<GovernanceTemplateItem, "id" | "created_at" | "updated_at" | "effective_permission_count"> & { change_request_id?: string | null }): Promise<GovernanceTemplateItem> {
return apiFetch(settings, "/api/v1/admin/system/governance-templates", { method: "POST", body: JSON.stringify(payload) });
}
export function updateGovernanceTemplate(settings: ApiSettings, templateId: string, payload: Omit<GovernanceTemplateItem, "id" | "kind" | "slug" | "created_at" | "updated_at" | "effective_permission_count">): Promise<GovernanceTemplateItem> {
export function updateGovernanceTemplate(settings: ApiSettings, templateId: string, payload: Omit<GovernanceTemplateItem, "id" | "kind" | "slug" | "created_at" | "updated_at" | "effective_permission_count"> & { change_request_id?: string | null }): Promise<GovernanceTemplateItem> {
return apiFetch(settings, `/api/v1/admin/system/governance-templates/${templateId}`, { method: "PATCH", body: JSON.stringify(payload) });
}
export function deleteGovernanceTemplate(settings: ApiSettings, templateId: string): Promise<void> {
return apiFetch(settings, `/api/v1/admin/system/governance-templates/${templateId}`, { method: "DELETE" });
export function deleteGovernanceTemplate(settings: ApiSettings, templateId: string, changeRequestId?: string | null): Promise<void> {
const suffix = changeRequestId ? `?change_request_id=${encodeURIComponent(changeRequestId)}` : "";
return apiFetch(settings, `/api/v1/admin/system/governance-templates/${templateId}${suffix}`, { method: "DELETE" });
}
export function fetchConfigurationChanges(settings: ApiSettings): Promise<{ requests: ConfigurationChangeRequest[]; history: ConfigurationChangeRecord[] }> {
return apiFetch(settings, "/api/v1/admin/configuration-changes");
}
export function fetchConfigurationChangesDelta(settings: ApiSettings, options: { since?: string | null; limit?: number } = {}): Promise<ConfigurationChangesDeltaResponse> {
const suffix = deltaSuffix(options);
return apiFetch(settings, `/api/v1/admin/configuration-changes/delta${suffix}`);
}
export async function createConfigurationChangeRequest(settings: ApiSettings, payload: {
key: string;
value?: unknown;
dry_run?: boolean;
target?: Record<string, unknown>;
reason?: string | null;
}): Promise<ConfigurationChangeRequest> {
const response = await apiFetch<{ request: ConfigurationChangeRequest }>(settings, "/api/v1/admin/configuration-change-requests", {
method: "POST",
body: JSON.stringify(payload)
});
return response.request;
}
export async function approveConfigurationChangeRequest(settings: ApiSettings, requestId: string, reason?: string | null): Promise<ConfigurationChangeRequest> {
const response = await apiFetch<{ request: ConfigurationChangeRequest }>(settings, `/api/v1/admin/configuration-change-requests/${encodeURIComponent(requestId)}/approve`, {
method: "POST",
body: JSON.stringify({ reason: reason ?? null })
});
return response.request;
}
export function fetchConfigurationPackageCatalogValidation(settings: ApiSettings): Promise<{ validation: Record<string, unknown> }> {
return apiFetch(settings, "/api/v1/admin/configuration-packages/catalog");
}
export function dryRunConfigurationPackage(settings: ApiSettings, payload: ConfigurationPackageRunPayload): Promise<{
diagnostics: ConfigurationPackageDiagnostic[];
required_data: ConfigurationPackageRequiredData[];
plan: ConfigurationPackagePlanItem[];
}> {
return apiFetch(settings, "/api/v1/admin/configuration-packages/dry-run", {
method: "POST",
body: JSON.stringify(payload)
});
}
export function applyConfigurationPackage(settings: ApiSettings, payload: ConfigurationPackageRunPayload): Promise<{
diagnostics: ConfigurationPackageDiagnostic[];
created_refs: Record<string, string>;
updated_refs: Record<string, string>;
}> {
return apiFetch(settings, "/api/v1/admin/configuration-packages/apply", {
method: "POST",
body: JSON.stringify(payload)
});
}
export function exportConfigurationPackage(settings: ApiSettings, payload: {
tenant_id?: string | null;
scopes?: string[];
module_ids?: string[];
object_refs?: string[];
}): Promise<{
fragments: ConfigurationPackageFragment[];
data_requirements: ConfigurationPackageRequiredData[];
diagnostics: ConfigurationPackageDiagnostic[];
}> {
return apiFetch(settings, "/api/v1/admin/configuration-packages/export", {
method: "POST",
body: JSON.stringify(payload)
});
}

View File

@@ -5,7 +5,7 @@ import { Card } from "@govoplan/core-webui";
import { Button } from "@govoplan/core-webui";
import { AdminPageLayout, adminErrorMessage } from "@govoplan/core-webui";
export default function AdminOverviewPanel({ settings, onSelect, availableSections }: { settings: ApiSettings; onSelect: (section: string) => void; availableSections: ReadonlySet<string> }) {
export default function AdminOverviewPanel({ settings, onSelect, availableSections }: {settings: ApiSettings;onSelect: (section: string) => void;availableSections: ReadonlySet<string>;}) {
const [overview, setOverview] = useState<AdminOverview | null>(null);
const [error, setError] = useState("");
const [loading, setLoading] = useState(true);
@@ -13,81 +13,112 @@ export default function AdminOverviewPanel({ settings, onSelect, availableSectio
async function load() {
setLoading(true);
setError("");
try { setOverview(await fetchAdminOverview(settings)); }
catch (err) { setError(adminErrorMessage(err)); }
finally { setLoading(false); }
try {setOverview(await fetchAdminOverview(settings));}
catch (err) {setError(adminErrorMessage(err));} finally
{setLoading(false);}
}
useEffect(() => { void load(); }, [settings.accessToken, settings.apiBaseUrl]);
useEffect(() => {void load();}, [settings.accessToken, settings.apiBaseUrl]);
const hasSystemMetrics = Boolean(overview && [
overview.tenant_count,
overview.system_account_count,
overview.system_group_template_count,
overview.system_role_template_count
].some((value) => value !== null && value !== undefined));
overview.tenant_count,
overview.system_account_count,
overview.system_group_template_count,
overview.system_role_template_count].
some((value) => value !== null && value !== undefined));
return (
<AdminPageLayout title="Administration" description="System-wide governance and tenant-local access management, separated by scope and enforced by the backend." loading={loading} error={error} actions={<Button onClick={() => void load()} disabled={loading}>Reload</Button>}>
<AdminPageLayout title="i18n:govoplan-admin.administration.b8be3d12" description="i18n:govoplan-admin.system_wide_governance_and_tenant_local_access_m.cda72499" loading={loading} error={error} actions={<Button onClick={() => void load()} disabled={loading}>i18n:govoplan-admin.reload.cce71553</Button>}>
{overview && <>
{hasSystemMetrics && <>
<div className="admin-overview-section-label">System</div>
<div className="admin-overview-section-label">i18n:govoplan-admin.system.bc0792d8</div>
<div className="metric-grid">
<Metric title="Tenants" value={overview.tenant_count ?? "—"} text="Registered tenant spaces." />
<Metric title="Users" value={overview.system_account_count ?? "—"} text="Global login accounts across all tenants." />
<Metric title="Central groups" value={overview.system_group_template_count ?? "—"} text="System-governed group definitions." />
<Metric title="Tenant roles" value={overview.system_role_template_count ?? "—"} text="Centrally governed tenant roles." />
<Metric title="i18n:govoplan-admin.tenants.1f7ae776" value={overview.tenant_count ?? "—"} text="i18n:govoplan-admin.registered_tenant_spaces.61e17d70" />
<Metric title="i18n:govoplan-admin.users.57f2b181" value={overview.system_account_count ?? "—"} text="i18n:govoplan-admin.global_login_accounts_across_all_tenants.2aab129d" />
<Metric title="i18n:govoplan-admin.central_groups.5c9b5b66" value={overview.system_group_template_count ?? "—"} text="i18n:govoplan-admin.system_governed_group_definitions.de8e5dc9" />
<Metric title="i18n:govoplan-admin.tenant_roles.51aca82d" value={overview.system_role_template_count ?? "—"} text="i18n:govoplan-admin.centrally_governed_tenant_roles.797c689b" />
</div>
</>}
{hasSystemArea(availableSections) && <Card title="System administration">
{hasAnySection(availableSections, platformSectionIds) && <Card title="ADMINISTRATION">
<div className="admin-overview-grid">
{availableSections.has("system-settings") && <AreaLink title="General" text="Instance defaults and tenant governance capabilities." onClick={() => onSelect("system-settings")} />}
{availableSections.has("system-tenants") && <AreaLink title="Tenants" text="Create, suspend and govern tenant spaces." onClick={() => onSelect("system-tenants")} />}
{availableSections.has("system-roles") && <AreaLink title="System roles" text="Instance-wide roles assigned directly to global accounts." onClick={() => onSelect("system-roles")} />}
{availableSections.has("system-role-templates") && <AreaLink title="Tenant roles" text="Centrally governed tenant roles and their availability across tenants." onClick={() => onSelect("system-role-templates")} />}
{availableSections.has("system-groups") && <AreaLink title="Groups" text="Group definitions made available or required in selected tenants." onClick={() => onSelect("system-groups")} />}
{availableSections.has("system-users") && <AreaLink title="Users" text="Global accounts, tenant memberships and system-role assignments." onClick={() => onSelect("system-users")} />}
{availableSections.has("system-mail-servers") && <AreaLink title="Mail servers" text="Reusable encrypted SMTP/IMAP profiles and mail policy." onClick={() => onSelect("system-mail-servers")} />}
{availableSections.has("system-retention") && <AreaLink title="Retention" text="Instance privacy retention policy and lower-level override permissions." onClick={() => onSelect("system-retention")} />}
{availableSections.has("system-modules") && <AreaLink title="Modules" text="Installed modules, runtime state and startup state." onClick={() => onSelect("system-modules")} />}
{availableSections.has("system-audit") && <AreaLink title="Audit" text="System-level administrative history." onClick={() => onSelect("system-audit")} />}
{availableSections.has("system-modules") && <AreaLink title="i18n:govoplan-admin.modules.04e9462c" text="i18n:govoplan-admin.installed_modules_runtime_state_and_startup_stat.38dd7028" onClick={() => onSelect("system-modules")} />}
{availableSections.has("system-configuration-packages") && <AreaLink title="i18n:govoplan-admin.packages.0a999012" text="i18n:govoplan-admin.preflight_approve_apply_and_export_configuration.276bd0f8" onClick={() => onSelect("system-configuration-packages")} />}
{availableSections.has("system-settings") && <AreaLink title="i18n:govoplan-admin.maintenance.94de303b" text="i18n:govoplan-admin.instance_defaults_and_tenant_governance_capabili.99d6b2fa" onClick={() => onSelect("system-settings")} />}
{availableSections.has("system-configuration-changes") && <AreaLink title="i18n:govoplan-admin.changes.8aa57de6" text="i18n:govoplan-admin.configuration_requests_approvals_and_version_his.19f37335" onClick={() => onSelect("system-configuration-changes")} />}
{availableSections.has("system-audit") && <AreaLink title="i18n:govoplan-admin.audit.fa1703dd" text="i18n:govoplan-admin.system_level_administrative_history.49f76723" onClick={() => onSelect("system-audit")} />}
</div>
</Card>}
<div className="admin-overview-section-label">Active tenant: {overview.active_tenant_name}</div>
{hasAnySection(availableSections, globalSectionIds) && <Card title="GLOBAL">
<div className="admin-overview-grid">
{availableSections.has("system-tenants") && <AreaLink title="i18n:govoplan-admin.tenants.1f7ae776" text="i18n:govoplan-admin.create_suspend_and_govern_tenant_spaces.77992a39" onClick={() => onSelect("system-tenants")} />}
{availableSections.has("system-roles") && <AreaLink title="i18n:govoplan-admin.system_roles.a9461aa6" text="i18n:govoplan-admin.instance_wide_roles_assigned_directly_to_global_.91050488" onClick={() => onSelect("system-roles")} />}
{availableSections.has("system-role-templates") && <AreaLink title="i18n:govoplan-admin.tenant_roles.51aca82d" text="i18n:govoplan-admin.centrally_governed_tenant_roles_and_their_availa.d879d5d1" onClick={() => onSelect("system-role-templates")} />}
{availableSections.has("system-groups") && <AreaLink title="i18n:govoplan-admin.central_groups.5c9b5b66" text="i18n:govoplan-admin.group_definitions_made_available_or_required_in_.55402e16" onClick={() => onSelect("system-groups")} />}
{availableSections.has("system-users") && <AreaLink title="i18n:govoplan-admin.users.57f2b181" text="i18n:govoplan-admin.global_accounts_tenant_memberships_and_system_ro.939ed01f" onClick={() => onSelect("system-users")} />}
{availableSections.has("system-file-connectors") && <AreaLink title="i18n:govoplan-admin.file_connections.1e362326" text="i18n:govoplan-admin.reusable_file_server_connections_credentials_and.8e5c7d43" onClick={() => onSelect("system-file-connectors")} />}
{availableSections.has("system-mail-servers") && <AreaLink title="i18n:govoplan-admin.mail_servers.d627326a" text="i18n:govoplan-admin.reusable_encrypted_smtp_imap_profiles_and_mail_p.31f150d1" onClick={() => onSelect("system-mail-servers")} />}
{availableSections.has("system-retention") && <AreaLink title="i18n:govoplan-admin.retention.c7199d9e" text="i18n:govoplan-admin.instance_privacy_retention_policy_and_lower_leve.b8d14069" onClick={() => onSelect("system-retention")} />}
</div>
</Card>}
<div className="admin-overview-section-label">i18n:govoplan-admin.active_tenant.dfadf0aa {overview.active_tenant_name}</div>
<div className="metric-grid">
<Metric title="Tenant users" value={`${overview.active_user_count}/${overview.user_count}`} text="Active and total memberships." />
<Metric title="Groups" value={overview.group_count} text="Tenant-local and centrally managed groups." />
<Metric title="Roles" value={overview.role_count} text="Tenant-local and centrally managed roles." />
<Metric title="API keys" value={overview.active_api_key_count} text="Active tenant automation credentials." />
<Metric title="i18n:govoplan-admin.tenant_users.cb800b38" value={`${overview.active_user_count}/${overview.user_count}`} text="i18n:govoplan-admin.active_and_total_memberships.c0c20f10" />
<Metric title="i18n:govoplan-admin.groups.ae9629f4" value={overview.group_count} text="i18n:govoplan-admin.tenant_local_and_centrally_managed_groups.4a6296b5" />
<Metric title="i18n:govoplan-admin.roles.47dcc27d" value={overview.role_count} text="i18n:govoplan-admin.tenant_local_and_centrally_managed_roles.4995a7b2" />
<Metric title="i18n:govoplan-admin.api_keys.94fcf3c2" value={overview.active_api_key_count} text="i18n:govoplan-admin.active_tenant_automation_credentials.240c659e" />
</div>
<Card title="Tenant administration">
{hasAnySection(availableSections, tenantSectionIds) && <Card title="TENANT">
<div className="admin-overview-grid">
{availableSections.has("tenant-settings") && <AreaLink title="General" text="Tenant locale and tenant-specific settings." onClick={() => onSelect("tenant-settings")} />}
{availableSections.has("tenant-roles") && <AreaLink title="Roles" text="Tenant permission bundles and system-managed role copies." onClick={() => onSelect("tenant-roles")} />}
{availableSections.has("tenant-groups") && <AreaLink title="Groups" text="Tenant memberships and inherited roles." onClick={() => onSelect("tenant-groups")} />}
{availableSections.has("tenant-users") && <AreaLink title="Users" text="Membership status, groups and direct roles in the active tenant." onClick={() => onSelect("tenant-users")} />}
{availableSections.has("tenant-mail-servers") && <AreaLink title="Mail servers" text="Reusable encrypted SMTP/IMAP profiles and mail policy." onClick={() => onSelect("tenant-mail-servers")} />}
{availableSections.has("tenant-retention") && <AreaLink title="Retention" text="Tenant-level privacy retention limits inherited by owned objects." onClick={() => onSelect("tenant-retention")} />}
{availableSections.has("tenant-api-keys") && <AreaLink title="API keys" text="Scoped automation credentials capped by owner permissions." onClick={() => onSelect("tenant-api-keys")} />}
{availableSections.has("tenant-audit") && <AreaLink title="Audit" text="Tenant-level administrative history only." onClick={() => onSelect("tenant-audit")} />}
{availableSections.has("tenant-roles") && <AreaLink title="i18n:govoplan-admin.roles.47dcc27d" text="i18n:govoplan-admin.tenant_permission_bundles_and_system_managed_rol.bb563bea" onClick={() => onSelect("tenant-roles")} />}
{availableSections.has("tenant-groups") && <AreaLink title="i18n:govoplan-admin.groups.ae9629f4" text="i18n:govoplan-admin.tenant_memberships_and_inherited_roles.16eda9f6" onClick={() => onSelect("tenant-groups")} />}
{availableSections.has("tenant-users") && <AreaLink title="i18n:govoplan-admin.users.57f2b181" text="i18n:govoplan-admin.membership_status_groups_and_direct_roles_in_the.ab6c10b6" onClick={() => onSelect("tenant-users")} />}
{availableSections.has("tenant-file-connectors") && <AreaLink title="i18n:govoplan-admin.file_connections.1e362326" text="i18n:govoplan-admin.reusable_file_server_connections_credentials_and.8e5c7d43" onClick={() => onSelect("tenant-file-connectors")} />}
{availableSections.has("tenant-mail-servers") && <AreaLink title="i18n:govoplan-admin.mail_servers.d627326a" text="i18n:govoplan-admin.reusable_encrypted_smtp_imap_profiles_and_mail_p.31f150d1" onClick={() => onSelect("tenant-mail-servers")} />}
{availableSections.has("tenant-api-keys") && <AreaLink title="i18n:govoplan-admin.api_keys.94fcf3c2" text="i18n:govoplan-admin.scoped_automation_credentials_capped_by_owner_pe.b3e20e54" onClick={() => onSelect("tenant-api-keys")} />}
{availableSections.has("tenant-retention") && <AreaLink title="i18n:govoplan-admin.retention.c7199d9e" text="i18n:govoplan-admin.tenant_level_privacy_retention_limits_inherited_.48f4989d" onClick={() => onSelect("tenant-retention")} />}
{availableSections.has("tenant-settings") && <AreaLink title="i18n:govoplan-admin.general.9239ee2c" text="i18n:govoplan-admin.tenant_locale_and_tenant_specific_settings.ac49c83b" onClick={() => onSelect("tenant-settings")} />}
{availableSections.has("tenant-audit") && <AreaLink title="i18n:govoplan-admin.audit.fa1703dd" text="i18n:govoplan-admin.tenant_level_administrative_history_only.55495c3c" onClick={() => onSelect("tenant-audit")} />}
</div>
</Card>
</Card>}
{hasAnySection(availableSections, groupSectionIds) && <Card title="GROUP">
<div className="admin-overview-grid">
{availableSections.has("tenant-group-file-connectors") && <AreaLink title="i18n:govoplan-admin.file_connections.1e362326" text="i18n:govoplan-admin.group_file_connector_policy_limits" onClick={() => onSelect("tenant-group-file-connectors")} />}
{availableSections.has("tenant-group-mail-servers") && <AreaLink title="i18n:govoplan-admin.mail_servers.d627326a" text="i18n:govoplan-admin.group_mail_server_policy_limits" onClick={() => onSelect("tenant-group-mail-servers")} />}
{availableSections.has("tenant-group-retention") && <AreaLink title="i18n:govoplan-admin.retention.c7199d9e" text="i18n:govoplan-admin.group_retention_policy_limits" onClick={() => onSelect("tenant-group-retention")} />}
</div>
</Card>}
{hasAnySection(availableSections, userSectionIds) && <Card title="USER">
<div className="admin-overview-grid">
{availableSections.has("tenant-user-file-connectors") && <AreaLink title="i18n:govoplan-admin.file_connections.1e362326" text="i18n:govoplan-admin.user_file_connector_policy_limits" onClick={() => onSelect("tenant-user-file-connectors")} />}
{availableSections.has("tenant-user-mail-servers") && <AreaLink title="i18n:govoplan-admin.mail_servers.d627326a" text="i18n:govoplan-admin.user_mail_server_policy_limits" onClick={() => onSelect("tenant-user-mail-servers")} />}
{availableSections.has("tenant-user-retention") && <AreaLink title="i18n:govoplan-admin.retention.c7199d9e" text="i18n:govoplan-admin.user_retention_policy_limits" onClick={() => onSelect("tenant-user-retention")} />}
</div>
</Card>}
</>}
</AdminPageLayout>
);
</AdminPageLayout>);
}
function hasSystemArea(sections: ReadonlySet<string>): boolean {
return ["system-settings", "system-tenants", "system-roles", "system-role-templates", "system-groups", "system-users", "system-mail-servers", "system-retention", "system-modules", "system-audit"].some((section) => sections.has(section));
const platformSectionIds = ["system-modules", "system-configuration-packages", "system-settings", "system-configuration-changes", "system-audit"];
const globalSectionIds = ["system-tenants", "system-roles", "system-role-templates", "system-groups", "system-users", "system-file-connectors", "system-mail-servers", "system-retention"];
const tenantSectionIds = ["tenant-roles", "tenant-groups", "tenant-users", "tenant-file-connectors", "tenant-mail-servers", "tenant-api-keys", "tenant-retention", "tenant-settings", "tenant-audit"];
const groupSectionIds = ["tenant-group-file-connectors", "tenant-group-mail-servers", "tenant-group-retention"];
const userSectionIds = ["tenant-user-file-connectors", "tenant-user-mail-servers", "tenant-user-retention"];
function hasAnySection(sections: ReadonlySet<string>, candidates: readonly string[]): boolean {
return candidates.some((section) => sections.has(section));
}
function Metric({ title, value, text }: { title: string; value: string | number; text: string }) {
function Metric({ title, value, text }: {title: string;value: string | number;text: string;}) {
return <Card title={title}><strong className="module-big-number">{value}</strong><p className="muted">{text}</p></Card>;
}
function AreaLink({ title, text, onClick }: { title: string; text: string; onClick: () => void }) {
function AreaLink({ title, text, onClick }: {title: string;text: string;onClick: () => void;}) {
return <button className="admin-overview-link" onClick={onClick}><strong>{title}</strong><span>{text}</span></button>;
}

View File

@@ -0,0 +1,170 @@
import { useEffect, useMemo, useRef, useState } from "react";
import { Check, RefreshCw } from "lucide-react";
import type { ApiSettings } from "@govoplan/core-webui";
import { AdminPageLayout, Button, Card, StatusBadge, adminErrorMessage, formatDateTime, i18nMessage, mergeDeltaRows, useDeltaWatermarks } from "@govoplan/core-webui";
import {
approveConfigurationChangeRequest,
fetchConfigurationChangesDelta,
type ConfigurationChangeRecord,
type ConfigurationChangeRequest } from
"../../api/admin";
const DELTA_KEY = "admin:configuration-changes";
export default function ConfigurationChangesPanel({ settings, canApprove }: {settings: ApiSettings;canApprove: boolean;}) {
const [requests, setRequests] = useState<ConfigurationChangeRequest[]>([]);
const [history, setHistory] = useState<ConfigurationChangeRecord[]>([]);
const requestsRef = useRef<ConfigurationChangeRequest[]>([]);
const historyRef = useRef<ConfigurationChangeRecord[]>([]);
const { getDeltaWatermark, setDeltaWatermark, resetDeltaWatermark } = useDeltaWatermarks();
const [loading, setLoading] = useState(true);
const [busyId, setBusyId] = useState("");
const [error, setError] = useState("");
const [message, setMessage] = useState("");
async function load() {
setLoading(true);
setError("");
try {
let nextWatermark = getDeltaWatermark(DELTA_KEY);
let nextRequests = requestsRef.current;
let nextHistory = historyRef.current;
let hasMore = false;
do {
const payload = await fetchConfigurationChangesDelta(settings, { since: nextWatermark });
nextRequests = payload.full ? payload.requests : mergeDeltaRows(nextRequests, payload.requests, payload.deleted, (request) => request.id, { sort: sortConfigurationRequests });
nextHistory = payload.full ? payload.history : mergeDeltaRows(nextHistory, payload.history, payload.deleted, (record) => record.id, { sort: sortConfigurationHistory });
nextWatermark = payload.watermark ?? null;
hasMore = payload.has_more;
} while (hasMore);
requestsRef.current = nextRequests;
historyRef.current = nextHistory;
setRequests(nextRequests);
setHistory(nextHistory);
setDeltaWatermark(DELTA_KEY, nextWatermark);
} catch (err) {
setError(adminErrorMessage(err));
} finally {
setLoading(false);
}
}
useEffect(() => {
requestsRef.current = [];
historyRef.current = [];
resetDeltaWatermark(DELTA_KEY);
void load();
}, [settings.accessToken, settings.apiBaseUrl, resetDeltaWatermark]);
async function approve(request: ConfigurationChangeRequest) {
setBusyId(request.id);
setError("");
setMessage("");
try {
await approveConfigurationChangeRequest(settings, request.id);
setMessage(i18nMessage("i18n:govoplan-admin.approved_value.52da808b", { value0: request.key }));
await load();
} catch (err) {
setError(adminErrorMessage(err));
} finally {
setBusyId("");
}
}
const pending = useMemo(() => requests.filter((item) => item.status !== "applied" && item.status !== "rejected"), [requests]);
return (
<AdminPageLayout
title="i18n:govoplan-admin.configuration_changes.82933bbb"
description="i18n:govoplan-admin.safety_controlled_configuration_requests_approva.e8259509"
loading={loading}
error={error}
success={message}
actions={<Button onClick={() => void load()} disabled={loading}><RefreshCw size={16} /> i18n:govoplan-admin.reload.cce71553</Button>}>
<Card title="i18n:govoplan-admin.requests.f7194e6a">
<div className="admin-table-wrap">
<table className="admin-table">
<thead>
<tr>
<th>i18n:govoplan-admin.setting.fb449f71</th>
<th>i18n:govoplan-admin.status.bae7d5be</th>
<th>i18n:govoplan-admin.requested.c26bf60f</th>
<th>i18n:govoplan-admin.approvals.deb9d03c</th>
<th>i18n:govoplan-admin.target.61ad50a9</th>
<th className="actions">i18n:govoplan-admin.action.97c89a4d</th>
</tr>
</thead>
<tbody>
{pending.map((request) =>
<tr key={request.id}>
<td><strong>{request.label || request.key}</strong><span className="muted block">{request.key}</span></td>
<td><StatusBadge status={statusTone(request.status)} label={request.status} /></td>
<td>{formatDateTime(request.requested_at)}</td>
<td>{request.approvals.length}</td>
<td>{targetLabel(request.target)}</td>
<td className="actions">
{canApprove && request.status === "pending_approval" &&
<Button onClick={() => void approve(request)} disabled={busyId === request.id}><Check size={16} /> i18n:govoplan-admin.approve.7b2c7f14</Button>
}
</td>
</tr>
)}
{!pending.length && <tr><td colSpan={6} className="muted">i18n:govoplan-admin.no_open_requests.580c95f9</td></tr>}
</tbody>
</table>
</div>
</Card>
<Card title="i18n:govoplan-admin.history.90ccd649">
<div className="admin-table-wrap">
<table className="admin-table">
<thead>
<tr>
<th>i18n:govoplan-admin.version.2da600bf</th>
<th>i18n:govoplan-admin.setting.fb449f71</th>
<th>i18n:govoplan-admin.status.bae7d5be</th>
<th>i18n:govoplan-admin.applied.a3e4a569</th>
<th>i18n:govoplan-admin.approvers.0e2de1fb</th>
<th>i18n:govoplan-admin.target.61ad50a9</th>
</tr>
</thead>
<tbody>
{history.map((record) =>
<tr key={record.id}>
<td>#{record.version}</td>
<td><strong>{record.key}</strong><span className="muted block">{record.id}</span></td>
<td><StatusBadge status={statusTone(record.status)} label={record.status} /></td>
<td>{formatDateTime(record.created_at)}</td>
<td>{record.approval_user_ids.length || "-"}</td>
<td>{targetLabel(record.target)}</td>
</tr>
)}
{!history.length && <tr><td colSpan={6} className="muted">i18n:govoplan-admin.no_applied_configuration_changes.6a3ae4a7</td></tr>}
</tbody>
</table>
</div>
</Card>
</AdminPageLayout>);
}
function statusTone(status: string): string {
if (status === "approved" || status === "applied") return "success";
if (status === "pending_approval") return "warning";
if (status === "rejected") return "error";
return "inactive";
}
function targetLabel(target?: Record<string, unknown>): string {
if (!target || !Object.keys(target).length) return "-";
return Object.entries(target).map(([key, value]) => `${key}: ${String(value)}`).join(", ");
}
function sortConfigurationRequests(left: ConfigurationChangeRequest, right: ConfigurationChangeRequest): number {
return new Date(right.updated_at || right.requested_at).getTime() - new Date(left.updated_at || left.requested_at).getTime();
}
function sortConfigurationHistory(left: ConfigurationChangeRecord, right: ConfigurationChangeRecord): number {
return right.version - left.version;
}

View File

@@ -0,0 +1,374 @@
import { useEffect, useMemo, useState } from "react";
import { Check, Download, Play, RefreshCw } from "lucide-react";
import type { ApiSettings } from "@govoplan/core-webui";
import { AdminPageLayout, Button, Card, StatusBadge, adminErrorMessage, i18nMessage } from "@govoplan/core-webui";
import {
applyConfigurationPackage,
createConfigurationChangeRequest,
dryRunConfigurationPackage,
exportConfigurationPackage,
fetchConfigurationPackageCatalogValidation,
type ConfigurationChangeRequest,
type ConfigurationPackageDiagnostic,
type ConfigurationPackagePlanItem,
type ConfigurationPackageRequiredData } from
"../../api/admin";
const SAMPLE_ACCESS_PACKAGE = {
package_id: "govoplan.access.minimal-office",
name: "i18n:govoplan-admin.minimal_office_access.af48f49a",
version: "0.1.0",
required_modules: [{ module_id: "access" }],
required_capabilities: ["configuration.provider", "access.configuration"],
fragments: [
{
module_id: "access",
fragment_type: "roles",
payload: {
items: [
{
slug: "case-clerk",
name: "i18n:govoplan-admin.case_clerk.b78a314a",
description: "i18n:govoplan-admin.handles_incoming_administrative_work.dc80c349",
permissions: ["admin:users:read"]
}]
}
},
{
module_id: "access",
fragment_type: "groups",
payload: { items: [{ slug: "front-office", name: "i18n:govoplan-admin.front_office.d9dcfee1" }] }
},
{
module_id: "access",
fragment_type: "group_role_assignments",
payload: { items: [{ group: "front-office", role: "case-clerk" }] }
}]
};
type DryRunResult = {
diagnostics: ConfigurationPackageDiagnostic[];
required_data: ConfigurationPackageRequiredData[];
plan: ConfigurationPackagePlanItem[];
};
type ApplyResult = {
diagnostics: ConfigurationPackageDiagnostic[];
created_refs: Record<string, string>;
updated_refs: Record<string, string>;
};
export default function ConfigurationPackagesPanel({ settings, canWrite }: {settings: ApiSettings;canWrite: boolean;}) {
const [catalogValidation, setCatalogValidation] = useState<Record<string, unknown> | null>(null);
const [packageText, setPackageText] = useState(() => JSON.stringify(SAMPLE_ACCESS_PACKAGE, null, 2));
const [tenantId, setTenantId] = useState("");
const [suppliedDataText, setSuppliedDataText] = useState("{}");
const [changeRequestId, setChangeRequestId] = useState("");
const [dryRun, setDryRun] = useState<DryRunResult | null>(null);
const [applyResult, setApplyResult] = useState<ApplyResult | null>(null);
const [exportText, setExportText] = useState("");
const [lastRequest, setLastRequest] = useState<ConfigurationChangeRequest | null>(null);
const [loading, setLoading] = useState(true);
const [busy, setBusy] = useState("");
const [error, setError] = useState("");
const [message, setMessage] = useState("");
const parsedPackage = useMemo(() => parseObject(packageText), [packageText]);
const parsedSuppliedData = useMemo(() => parseObject(suppliedDataText), [suppliedDataText]);
const canRun = Boolean(parsedPackage.value && parsedSuppliedData.value);
async function loadCatalog() {
setLoading(true);
setError("");
try {
const response = await fetchConfigurationPackageCatalogValidation(settings);
setCatalogValidation(response.validation);
} catch (err) {
setError(adminErrorMessage(err));
} finally {
setLoading(false);
}
}
useEffect(() => {void loadCatalog();}, [settings.accessToken, settings.apiBaseUrl]);
async function runDryRun() {
const manifest = requireParsedPackage();
const suppliedData = requireParsedSuppliedData();
if (!manifest || !suppliedData) return;
setBusy("dry-run");
setError("");
setMessage("");
setApplyResult(null);
try {
const result = await dryRunConfigurationPackage(settings, runPayload(manifest, suppliedData));
setDryRun(result);
setMessage(result.diagnostics.some((item) => item.severity === "blocker") ? "i18n:govoplan-admin.preflight_finished_with_blockers.7e2ca12b" : "i18n:govoplan-admin.preflight_passed.c0c99055");
} catch (err) {
setError(adminErrorMessage(err));
} finally {
setBusy("");
}
}
async function requestApproval() {
const manifest = requireParsedPackage();
if (!manifest) return;
setBusy("approval");
setError("");
setMessage("");
try {
const request = await createConfigurationChangeRequest(settings, {
key: "configuration_packages.apply",
value: manifest,
dry_run: true,
target: tenantId.trim() ? { tenant_id: tenantId.trim() } : {},
reason: "i18n:govoplan-admin.configuration_package_apply.c270e5f3"
});
setLastRequest(request);
setChangeRequestId(request.id);
setMessage(i18nMessage("i18n:govoplan-admin.change_request_created_value.4af6d3d2", { value0: request.id }));
} catch (err) {
setError(adminErrorMessage(err));
} finally {
setBusy("");
}
}
async function applyPackage() {
const manifest = requireParsedPackage();
const suppliedData = requireParsedSuppliedData();
if (!manifest || !suppliedData) return;
setBusy("apply");
setError("");
setMessage("");
try {
const result = await applyConfigurationPackage(settings, runPayload(manifest, suppliedData, changeRequestId.trim() || null));
setApplyResult(result);
setMessage(result.diagnostics.some((item) => item.severity === "blocker") ? "i18n:govoplan-admin.apply_finished_with_blockers.78487a12" : "i18n:govoplan-admin.package_applied.63782ce7");
} catch (err) {
setError(adminErrorMessage(err));
} finally {
setBusy("");
}
}
async function exportAccessPackage() {
setBusy("export");
setError("");
setMessage("");
try {
const result = await exportConfigurationPackage(settings, {
tenant_id: tenantId.trim() || null,
module_ids: ["access"],
scopes: tenantId.trim() ? ["tenant"] : ["system"]
});
const exported = {
package_id: tenantId.trim() ? "govoplan.export.access-tenant" : "govoplan.export.access-system",
name: tenantId.trim() ? "i18n:govoplan-admin.exported_tenant_access_configuration.9122c85c" : "i18n:govoplan-admin.exported_system_access_configuration.2e1c6f4c",
version: "0.1.0",
required_modules: [{ module_id: "access" }],
required_capabilities: ["configuration.provider", "access.configuration"],
fragments: result.fragments,
data_requirements: result.data_requirements
};
setExportText(JSON.stringify(exported, null, 2));
setMessage(result.diagnostics.some((item) => item.severity === "blocker") ? "i18n:govoplan-admin.export_finished_with_blockers.e2f70611" : "i18n:govoplan-admin.access_configuration_exported.098f200d");
} catch (err) {
setError(adminErrorMessage(err));
} finally {
setBusy("");
}
}
function runPayload(manifest: Record<string, unknown>, suppliedData: Record<string, unknown>, requestId?: string | null) {
return {
package: manifest,
tenant_id: tenantId.trim() || null,
supplied_data: suppliedData,
change_request_id: requestId ?? null
};
}
function requireParsedPackage(): Record<string, unknown> | null {
if (!parsedPackage.value) {
setError(parsedPackage.error || "i18n:govoplan-admin.package_json_must_be_an_object.db825bb3");
return null;
}
return parsedPackage.value;
}
function requireParsedSuppliedData(): Record<string, unknown> | null {
if (!parsedSuppliedData.value) {
setError(parsedSuppliedData.error || "i18n:govoplan-admin.supplied_data_json_must_be_an_object.b265eb92");
return null;
}
return parsedSuppliedData.value;
}
return (
<AdminPageLayout
title="i18n:govoplan-admin.configuration_packages.eb2f05f1"
description="i18n:govoplan-admin.import_preflight_approve_apply_and_export_module.29aec929"
loading={loading}
error={error || parsedPackage.error || parsedSuppliedData.error || ""}
success={message}
actions={<Button onClick={() => void loadCatalog()} disabled={loading || Boolean(busy)}><RefreshCw size={16} /> i18n:govoplan-admin.reload.cce71553</Button>}>
<Card title="i18n:govoplan-admin.catalog.4a88d27b">
<div className="button-row compact-actions">
<StatusBadge status={catalogValidation?.valid ? "success" : catalogValidation?.configured ? "warning" : "inactive"} label={catalogValidation?.valid ? "i18n:govoplan-admin.valid.a4aefa35" : catalogValidation?.configured ? "i18n:govoplan-admin.needs_attention.a126722e" : "i18n:govoplan-admin.not_configured.811931bb"} />
{catalogValidation?.signed !== undefined && <StatusBadge status={catalogValidation.signed ? "success" : "inactive"} label={catalogValidation.signed ? "i18n:govoplan-admin.signed.6e3665d8" : "i18n:govoplan-admin.unsigned.e91344ea"} />}
{catalogValidation?.trusted !== undefined && <StatusBadge status={catalogValidation.trusted ? "success" : "warning"} label={catalogValidation.trusted ? "i18n:govoplan-admin.trusted.99f7ed54" : "i18n:govoplan-admin.untrusted.cdc7838a"} />}
</div>
<pre className="code-panel module-install-plan-commands">{JSON.stringify(catalogValidation ?? {}, null, 2)}</pre>
</Card>
<Card title="i18n:govoplan-admin.package.7431e3df">
<div className="module-installer-request-grid">
<label className="wide"><span>i18n:govoplan-admin.tenant_id.59eba244</span><input value={tenantId} onChange={(event) => setTenantId(event.target.value)} placeholder="active tenant" /></label>
<label className="wide"><span>i18n:govoplan-admin.change_request_id.96ee3239</span><input value={changeRequestId} onChange={(event) => setChangeRequestId(event.target.value)} placeholder="cfgreq-..." /></label>
<label className="wide"><span>i18n:govoplan-admin.package_json.a2b10f38</span><textarea rows={18} value={packageText} onChange={(event) => setPackageText(event.target.value)} /></label>
<label className="wide"><span>i18n:govoplan-admin.supplied_data_json.6932bfb7</span><textarea rows={6} value={suppliedDataText} onChange={(event) => setSuppliedDataText(event.target.value)} /></label>
</div>
<div className="button-row compact-actions">
<Button onClick={() => void runDryRun()} disabled={!canRun || Boolean(busy)}><Play size={16} /> i18n:govoplan-admin.dry_run.3d14659c</Button>
<Button onClick={() => void requestApproval()} disabled={!canWrite || !parsedPackage.value || Boolean(busy)}><Check size={16} /> i18n:govoplan-admin.request_approval.6245aea1</Button>
<Button variant="primary" onClick={() => void applyPackage()} disabled={!canWrite || !canRun || Boolean(busy)}><Check size={16} /> i18n:govoplan-admin.apply.cfea419c</Button>
<Button onClick={() => void exportAccessPackage()} disabled={Boolean(busy)}><Download size={16} /> i18n:govoplan-admin.export_access.9a2eb91e</Button>
</div>
{lastRequest && <p className="muted small-note">i18n:govoplan-admin.last_request.4508ef35 {lastRequest.id} ({lastRequest.status})</p>}
</Card>
{dryRun && <Card title="i18n:govoplan-admin.preflight.8016a487">
<PackageDiagnostics diagnostics={dryRun.diagnostics} />
<RequiredData items={dryRun.required_data} />
<PackagePlan items={dryRun.plan} />
</Card>}
{applyResult && <Card title="i18n:govoplan-admin.apply_result.0fde1c3c">
<PackageDiagnostics diagnostics={applyResult.diagnostics} />
<ReferenceMap title="i18n:govoplan-admin.created.accf40c8" refs={applyResult.created_refs} />
<ReferenceMap title="i18n:govoplan-admin.updated.f2f8570d" refs={applyResult.updated_refs} />
</Card>}
{exportText && <Card title="i18n:govoplan-admin.export.f3e4fadb">
<textarea className="code-panel module-install-plan-commands" rows={16} value={exportText} onChange={(event) => setExportText(event.target.value)} />
</Card>}
</AdminPageLayout>);
}
function parseObject(text: string): {value: Record<string, unknown> | null;error: string;} {
try {
const value = JSON.parse(text) as unknown;
if (!value || typeof value !== "object" || Array.isArray(value)) return { value: null, error: "i18n:govoplan-admin.json_must_be_an_object.848569c9" };
return { value: value as Record<string, unknown>, error: "" };
} catch (err) {
return { value: null, error: err instanceof Error ? err.message : "i18n:govoplan-admin.invalid_json.01ccb74f" };
}
}
function PackageDiagnostics({ diagnostics }: {diagnostics: ConfigurationPackageDiagnostic[];}) {
if (diagnostics.length === 0) return <p className="muted">i18n:govoplan-admin.no_diagnostics.2b6e2630</p>;
return (
<div className="admin-table-wrap">
<table className="admin-table">
<thead><tr><th>i18n:govoplan-admin.severity.de314fa0</th><th>i18n:govoplan-admin.code.adac6937</th><th>i18n:govoplan-admin.owner.89ff3122</th><th>i18n:govoplan-admin.object.2883f191</th><th>i18n:govoplan-admin.message.68f4145f</th></tr></thead>
<tbody>
{diagnostics.map((item, index) =>
<tr key={`${item.code}-${index}`}>
<td><StatusBadge status={diagnosticTone(item.severity)} label={item.severity} /></td>
<td><code>{item.code}</code></td>
<td>{item.module_id || "-"}</td>
<td>{item.object_ref || "-"}</td>
<td>{item.message}{item.resolution ? <span className="muted block">{item.resolution}</span> : null}</td>
</tr>
)}
</tbody>
</table>
</div>);
}
function RequiredData({ items }: {items: ConfigurationPackageRequiredData[];}) {
if (items.length === 0) return null;
return (
<>
<h3>i18n:govoplan-admin.required_data.1b1c1b34</h3>
<div className="admin-table-wrap">
<table className="admin-table">
<thead><tr><th>i18n:govoplan-admin.key.c67dd20e</th><th>i18n:govoplan-admin.label.74341e3c</th><th>i18n:govoplan-admin.type.3deb7456</th><th>i18n:govoplan-admin.required.eed6bfb4</th><th>i18n:govoplan-admin.secret.f4e7a874</th></tr></thead>
<tbody>
{items.map((item) =>
<tr key={item.key}>
<td><code>{item.key}</code></td>
<td>{item.label}</td>
<td>{item.data_type}</td>
<td>{item.required ? "yes" : "no"}</td>
<td>{item.secret ? "yes" : "no"}</td>
</tr>
)}
</tbody>
</table>
</div>
</>);
}
function PackagePlan({ items }: {items: ConfigurationPackagePlanItem[];}) {
if (items.length === 0) return <p className="muted">i18n:govoplan-admin.no_plan_items.7108c582</p>;
return (
<>
<h3>i18n:govoplan-admin.plan.ae2f98a0</h3>
<div className="admin-table-wrap">
<table className="admin-table">
<thead><tr><th>i18n:govoplan-admin.action.97c89a4d</th><th>i18n:govoplan-admin.module.b8ff0289</th><th>i18n:govoplan-admin.fragment.3f19d616</th><th>i18n:govoplan-admin.id.474ae526</th><th>i18n:govoplan-admin.summary.12b71c3e</th></tr></thead>
<tbody>
{items.map((item, index) =>
<tr key={`${item.module_id}-${item.fragment_type}-${item.fragment_id ?? index}`}>
<td><StatusBadge status={planTone(item.action)} label={item.action} /></td>
<td>{item.module_id}</td>
<td>{item.fragment_type}</td>
<td>{item.fragment_id || "-"}</td>
<td>{item.summary || "-"}</td>
</tr>
)}
</tbody>
</table>
</div>
</>);
}
function ReferenceMap({ title, refs }: {title: string;refs: Record<string, string>;}) {
const entries = Object.entries(refs);
if (entries.length === 0) return null;
return (
<>
<h3>{title}</h3>
<div className="admin-table-wrap">
<table className="admin-table">
<tbody>
{entries.map(([key, value]) => <tr key={key}><td><code>{key}</code></td><td>{value}</td></tr>)}
</tbody>
</table>
</div>
</>);
}
function diagnosticTone(severity: ConfigurationPackageDiagnostic["severity"]): string {
if (severity === "blocker") return "error";
if (severity === "warning") return "warning";
return "inactive";
}
function planTone(action: ConfigurationPackagePlanItem["action"]): string {
if (action === "blocked") return "error";
if (action === "skip" || action === "noop") return "inactive";
if (action === "update" || action === "bind") return "warning";
return "success";
}

View File

@@ -17,9 +17,9 @@ import {
type GovernanceAssignment,
type GovernanceTemplateItem,
type PermissionItem,
type TenantAdminItem
} from "../../api/admin";
import { AdminIconButton, AdminPageLayout, AdminSelectionList, adminErrorMessage, joinLabels } from "@govoplan/core-webui";
type TenantAdminItem } from
"../../api/admin";
import { AdminIconButton, AdminPageLayout, AdminSelectionList, adminErrorMessage, joinLabels, i18nMessage, useUnsavedDraftGuard } from "@govoplan/core-webui";
const emptyDraft = {
slug: "",
@@ -35,12 +35,12 @@ export default function GovernanceTemplatesPanel({
kind,
canWrite,
onAuthRefresh
}: {
settings: ApiSettings;
kind: "group" | "role";
canWrite: boolean;
onAuthRefresh: () => Promise<void>;
}) {
}: {settings: ApiSettings;kind: "group" | "role";canWrite: boolean;onAuthRefresh: () => Promise<void>;}) {
const [items, setItems] = useState<GovernanceTemplateItem[]>([]);
const [tenants, setTenants] = useState<TenantAdminItem[]>([]);
const [permissions, setPermissions] = useState<PermissionItem[]>([]);
@@ -48,46 +48,63 @@ export default function GovernanceTemplatesPanel({
const [viewing, setViewing] = useState<GovernanceTemplateItem | null>(null);
const [deleting, setDeleting] = useState<GovernanceTemplateItem | null>(null);
const [draft, setDraft] = useState(emptyDraft);
const [savedDraftKey, setSavedDraftKey] = useState(draftKey(emptyDraft));
const [loading, setLoading] = useState(true);
const [busy, setBusy] = useState(false);
const [error, setError] = useState("");
const [success, setSuccess] = useState("");
const dirty = editing !== null && draftKey(draft) !== savedDraftKey;
useUnsavedDraftGuard({
dirty,
onSave: save,
onDiscard: closeEditor
});
async function load() {
setLoading(true);
setError("");
try {
const [nextItems, nextTenants, nextPermissions] = await Promise.all([
fetchGovernanceTemplates(settings, kind),
fetchTenants(settings),
kind === "role" ? fetchPermissionCatalog(settings) : Promise.resolve([])
]);
fetchGovernanceTemplates(settings, kind),
fetchTenants(settings),
kind === "role" ? fetchPermissionCatalog(settings) : Promise.resolve([])]
);
setItems(nextItems);
setTenants(nextTenants);
setPermissions(nextPermissions.filter((item) => item.level === "tenant"));
} catch (err) { setError(adminErrorMessage(err)); }
finally { setLoading(false); }
} catch (err) {setError(adminErrorMessage(err));} finally
{setLoading(false);}
}
useEffect(() => { void load(); }, [settings.accessToken, settings.apiBaseUrl, kind]);
useEffect(() => {void load();}, [settings.accessToken, settings.apiBaseUrl, kind]);
function openCreate() {
setDraft(emptyDraft);
setSavedDraftKey(draftKey(emptyDraft));
setEditing("new");
}
function openEdit(item: GovernanceTemplateItem) {
setDraft({
const nextDraft = {
slug: item.slug,
name: item.name,
description: item.description || "",
isActive: item.is_active,
permissions: item.permissions,
assignments: item.assignments
});
};
setDraft(nextDraft);
setSavedDraftKey(draftKey(nextDraft));
setEditing(item);
}
function closeEditor() {
setEditing(null);
setDraft(emptyDraft);
setSavedDraftKey(draftKey(emptyDraft));
}
function assignmentMode(tenantId: string): "none" | "available" | "required" {
return draft.assignments.find((item) => item.tenant_id === tenantId)?.mode ?? "none";
}
@@ -95,13 +112,13 @@ export default function GovernanceTemplatesPanel({
function setAssignment(tenantId: string, mode: "none" | "available" | "required") {
setDraft((current) => ({
...current,
assignments: mode === "none"
? current.assignments.filter((item) => item.tenant_id !== tenantId)
: [...current.assignments.filter((item) => item.tenant_id !== tenantId), { tenant_id: tenantId, mode }]
assignments: mode === "none" ?
current.assignments.filter((item) => item.tenant_id !== tenantId) :
[...current.assignments.filter((item) => item.tenant_id !== tenantId), { tenant_id: tenantId, mode }]
}));
}
async function save() {
async function save(): Promise<boolean> {
setBusy(true);
setError("");
try {
@@ -115,7 +132,7 @@ export default function GovernanceTemplatesPanel({
is_active: draft.isActive,
assignments: draft.assignments
});
setSuccess(`${kind === "group" ? "Group" : "Role"} template created.`);
setSuccess(i18nMessage("i18n:govoplan-admin.value_template_created.d68a5166", { value0: kind === "group" ? "i18n:govoplan-admin.group.171a0606" : "i18n:govoplan-admin.role.c3f104d1" }));
} else if (editing) {
await updateGovernanceTemplate(settings, editing.id, {
name: draft.name,
@@ -124,13 +141,14 @@ export default function GovernanceTemplatesPanel({
is_active: draft.isActive,
assignments: draft.assignments
});
setSuccess(`${draft.name} updated and synchronized to assigned tenants.`);
setSuccess(i18nMessage("i18n:govoplan-admin.value_updated_and_synchronized_to_assigned_tenan.d136eef2", { value0: draft.name }));
}
setEditing(null);
await load();
await onAuthRefresh();
} catch (err) { setError(adminErrorMessage(err)); }
finally { setBusy(false); }
return true;
} catch (err) {setError(adminErrorMessage(err));return false;} finally
{setBusy(false);}
}
async function remove() {
@@ -139,47 +157,47 @@ export default function GovernanceTemplatesPanel({
setError("");
try {
await deleteGovernanceTemplate(settings, deleting.id);
setSuccess(`${deleting.name} deleted.`);
setSuccess(i18nMessage("i18n:govoplan-admin.value_deleted.3c4bf574", { value0: deleting.name }));
setDeleting(null);
await load();
await onAuthRefresh();
} catch (err) { setError(adminErrorMessage(err)); }
finally { setBusy(false); }
} catch (err) {setError(adminErrorMessage(err));} finally
{setBusy(false);}
}
const columns = useMemo<DataGridColumn<GovernanceTemplateItem>[]>(() => [
{
id: "template", header: kind === "group" ? "Group template" : "Tenant role", width: "minmax(240px, 1.2fr)", minWidth: 210, resizable: true, sticky: "start", sortable: true, filterable: true,
value: (row) => `${row.name} ${row.slug}`,
render: (row) => <div><strong>{row.name}</strong><div className="muted small-note">{row.slug}</div></div>
},
{
id: "tenants", header: "Tenant availability", width: 320, minWidth: 210, maxWidth: 640, resizable: true, fill: true, sortable: true, filterable: true,
value: (row) => row.assignments.map((assignment) => tenants.find((tenant) => tenant.id === assignment.tenant_id)?.name || assignment.tenant_id).join(", ") || "—",
render: (row) => row.assignments.length ? row.assignments.map((assignment) => {
const tenant = tenants.find((item) => item.id === assignment.tenant_id);
return `${tenant?.name || assignment.tenant_id} (${assignment.mode})`;
}).join(", ") : "—"
},
...(kind === "role" ? [{
id: "permissions", header: "Permissions", width: 120, resizable: false, sortable: true, filterable: true, filterType: "integer" as const,
value: (row: GovernanceTemplateItem) => row.effective_permission_count
}] : []),
{ id: "status", header: "Status", width: 120, resizable: false, sortable: true, filterable: true, value: (row) => row.is_active ? "active" : "inactive", render: (row) => <StatusBadge status={row.is_active ? "active" : "inactive"} /> },
{
id: "actions", header: "Actions", width: 150, sticky: "end", resizable: false, align: "right",
render: (row) => <div className="admin-icon-actions">
<AdminIconButton label={`Inspect ${row.name}`} icon={<Search />} onClick={() => setViewing(row)} />
<AdminIconButton label={`Edit ${row.name}`} icon={<Pencil />} onClick={() => openEdit(row)} disabled={!canWrite} />
<AdminIconButton label={`Delete ${row.name}`} icon={<Trash2 />} variant="danger" onClick={() => setDeleting(row)} disabled={!canWrite} />
{
id: "template", header: kind === "group" ? "i18n:govoplan-admin.group_template.973e0fa6" : "i18n:govoplan-admin.tenant_role.6b53115d", width: "minmax(240px, 1.2fr)", minWidth: 210, resizable: true, sticky: "start", sortable: true, filterable: true,
value: (row) => `${row.name} ${row.slug}`,
render: (row) => <div><strong>{row.name}</strong><div className="muted small-note">{row.slug}</div></div>
},
{
id: "tenants", header: "i18n:govoplan-admin.tenant_availability.067a4f31", width: 320, minWidth: 210, maxWidth: 640, resizable: true, fill: true, sortable: true, filterable: true,
value: (row) => row.assignments.map((assignment) => tenants.find((tenant) => tenant.id === assignment.tenant_id)?.name || assignment.tenant_id).join(", ") || "—",
render: (row) => row.assignments.length ? row.assignments.map((assignment) => {
const tenant = tenants.find((item) => item.id === assignment.tenant_id);
return i18nMessage("i18n:govoplan-admin.value_value.c189e8bc", { value0: tenant?.name || assignment.tenant_id, value1: assignment.mode });
}).join(", ") : "—"
},
...(kind === "role" ? [{
id: "permissions", header: "i18n:govoplan-admin.permissions.d06d5557", width: 120, resizable: false, sortable: true, filterable: true, filterType: "integer" as const,
value: (row: GovernanceTemplateItem) => row.effective_permission_count
}] : []),
{ id: "status", header: "i18n:govoplan-admin.status.bae7d5be", width: 120, resizable: false, sortable: true, filterable: true, value: (row) => row.is_active ? "active" : "inactive", render: (row) => <StatusBadge status={row.is_active ? "active" : "inactive"} /> },
{
id: "actions", header: "i18n:govoplan-admin.actions.c3cd636a", width: 150, sticky: "end", resizable: false, align: "right",
render: (row) => <div className="admin-icon-actions">
<AdminIconButton label={i18nMessage("i18n:govoplan-admin.inspect_value.9d5d1071", { value0: row.name })} icon={<Search />} onClick={() => setViewing(row)} />
<AdminIconButton label={i18nMessage("i18n:govoplan-admin.edit_value.fad75899", { value0: row.name })} icon={<Pencil />} onClick={() => openEdit(row)} disabled={!canWrite} />
<AdminIconButton label={i18nMessage("i18n:govoplan-admin.delete_value.4d18989e", { value0: row.name })} icon={<Trash2 />} variant="danger" onClick={() => setDeleting(row)} disabled={!canWrite} />
</div>
}
], [canWrite, kind, tenants]);
}],
[canWrite, kind, tenants]);
const title = kind === "group" ? "Central groups" : "Tenant roles";
const description = kind === "group"
? "Centrally defined group identities that are provisioned into selected tenants as available or required definitions. Membership remains tenant-local."
: "Centrally governed tenant roles that are provisioned into selected tenants as available or required definitions.";
const title = kind === "group" ? "i18n:govoplan-admin.central_groups.5c9b5b66" : "i18n:govoplan-admin.tenant_roles.51aca82d";
const description = kind === "group" ?
"i18n:govoplan-admin.centrally_defined_group_identities_that_are_prov.7761fce9" :
"i18n:govoplan-admin.centrally_governed_tenant_roles_that_are_provisi.8739fca3";
return (
<>
@@ -189,41 +207,45 @@ export default function GovernanceTemplatesPanel({
loading={loading}
error={error}
success={success}
actions={<><Button onClick={() => void load()} disabled={loading}>Reload</Button><AdminIconButton label={kind === "group" ? "Add group template" : "Add tenant role"} icon={<Plus />} variant="primary" onClick={openCreate} disabled={!canWrite} /></>}
>
actions={<><Button onClick={() => void load()} disabled={loading}>i18n:govoplan-admin.reload.cce71553</Button><AdminIconButton label={kind === "group" ? "i18n:govoplan-admin.add_group_template.b74d8f0f" : "i18n:govoplan-admin.add_tenant_role.fcd904ea"} icon={<Plus />} variant="primary" onClick={openCreate} disabled={!canWrite} /></>}>
<div className="admin-table-surface">
<DataGrid id={`admin-system-${kind}-templates-v3`} rows={items} columns={columns} initialFit="container" getRowKey={(row) => row.id} emptyText={`No central ${kind} templates found.`} />
<DataGrid id={`admin-system-${kind}-templates-v3`} rows={items} columns={columns} initialFit="container" getRowKey={(row) => row.id} emptyText={i18nMessage("i18n:govoplan-admin.no_central_value_templates_found.081149fa", { value0: kind })} />
</div>
</AdminPageLayout>
<Dialog
open={editing !== null}
title={editing === "new" ? (kind === "group" ? "Create group template" : "Create tenant role") : (kind === "group" ? "Edit group template" : "Edit tenant role")}
title={editing === "new" ? kind === "group" ? "i18n:govoplan-admin.create_group_template.72407248" : "i18n:govoplan-admin.create_tenant_role.f58db104" : kind === "group" ? "i18n:govoplan-admin.edit_group_template.9bc72d21" : "i18n:govoplan-admin.edit_tenant_role.c15a260d"}
onClose={() => !busy && setEditing(null)}
className="admin-dialog admin-dialog-wide"
footer={<><Button onClick={() => setEditing(null)} disabled={busy}>Cancel</Button><Button variant="primary" onClick={() => void save()} disabled={busy || !draft.name.trim() || !draft.slug.trim()}>{busy ? "Saving…" : kind === "group" ? "Save template" : "Save tenant role"}</Button></>}
>
footer={<><Button onClick={() => setEditing(null)} disabled={busy}>i18n:govoplan-admin.cancel.77dfd213</Button><Button variant="primary" onClick={() => void save()} disabled={busy || !draft.name.trim() || !draft.slug.trim()}>{busy ? "i18n:govoplan-admin.saving.56a2285c" : kind === "group" ? "i18n:govoplan-admin.save_template.0885fab2" : "i18n:govoplan-admin.save_tenant_role.8fc0d37d"}</Button></>}>
<div className="admin-form-grid two-columns">
<FormField label="Name"><input value={draft.name} onChange={(event) => setDraft({ ...draft, name: event.target.value })} /></FormField>
<FormField label="Slug"><input value={draft.slug} disabled={editing !== "new"} onChange={(event) => setDraft({ ...draft, slug: event.target.value })} /></FormField>
<FormField label="Status"><select value={draft.isActive ? "active" : "inactive"} onChange={(event) => setDraft({ ...draft, isActive: event.target.value === "active" })}><option value="active">Active</option><option value="inactive">Inactive</option></select></FormField>
<FormField label="Description"><textarea rows={3} value={draft.description} onChange={(event) => setDraft({ ...draft, description: event.target.value })} /></FormField>
<FormField label="i18n:govoplan-admin.name.709a2322"><input value={draft.name} onChange={(event) => setDraft({ ...draft, name: event.target.value })} /></FormField>
<FormField label="i18n:govoplan-admin.slug.094da9b9"><input value={draft.slug} disabled={editing !== "new"} onChange={(event) => setDraft({ ...draft, slug: event.target.value })} /></FormField>
<FormField label="i18n:govoplan-admin.status.bae7d5be"><select value={draft.isActive ? "active" : "inactive"} onChange={(event) => setDraft({ ...draft, isActive: event.target.value === "active" })}><option value="active">i18n:govoplan-admin.active.a733b809</option><option value="inactive">i18n:govoplan-admin.inactive.09af574c</option></select></FormField>
<FormField label="i18n:govoplan-admin.description.55f8ebc8"><textarea rows={3} value={draft.description} onChange={(event) => setDraft({ ...draft, description: event.target.value })} /></FormField>
</div>
{kind === "role" && <div className="form-field"><span className="form-label">Tenant permissions</span><AdminSelectionList options={permissions.map((permission) => ({ id: permission.scope, label: permission.label, description: permission.description }))} selected={draft.permissions} onChange={(next) => setDraft({ ...draft, permissions: next })} /></div>}
{kind === "role" && <div className="form-field"><span className="form-label">i18n:govoplan-admin.tenant_permissions.246294bc</span><AdminSelectionList options={permissions.map((permission) => ({ id: permission.scope, label: permission.label, description: permission.description }))} selected={draft.permissions} onChange={(next) => setDraft({ ...draft, permissions: next })} /></div>}
<div className="form-field">
<span className="form-label">Tenant availability</span>
<span className="form-label">i18n:govoplan-admin.tenant_availability.067a4f31</span>
<div className="admin-selection-list admin-governance-mode">
{tenants.map((tenant) => <div className="admin-tenant-assignment-row" key={tenant.id}><span><strong>{tenant.name}</strong><small>{tenant.slug}</small></span><select value={assignmentMode(tenant.id)} onChange={(event) => setAssignment(tenant.id, event.target.value as "none" | "available" | "required")}><option value="none">Not available</option><option value="available">Available</option><option value="required">Required</option></select></div>)}
{tenants.map((tenant) => <div className="admin-tenant-assignment-row" key={tenant.id}><span><strong>{tenant.name}</strong><small>{tenant.slug}</small></span><select value={assignmentMode(tenant.id)} onChange={(event) => setAssignment(tenant.id, event.target.value as "none" | "available" | "required")}><option value="none">i18n:govoplan-admin.not_available.d1a17af1</option><option value="available">i18n:govoplan-admin.available.7c62a142</option><option value="required">i18n:govoplan-admin.required.eed6bfb4</option></select></div>)}
</div>
<p className="muted small-note">Required means the definition must remain present and system-controlled. It does not automatically assign users or grant permissions.</p>
<p className="muted small-note">i18n:govoplan-admin.required_means_the_definition_must_remain_presen.8462063c</p>
</div>
</Dialog>
<Dialog open={Boolean(viewing)} title={viewing?.name || "Template details"} onClose={() => setViewing(null)} className="admin-dialog admin-dialog-wide" footer={<Button onClick={() => setViewing(null)}>Close</Button>}>
{viewing && <dl className="admin-details-grid"><div><dt>Kind</dt><dd>{viewing.kind}</dd></div><div><dt>Slug</dt><dd>{viewing.slug}</dd></div><div><dt>Status</dt><dd>{viewing.is_active ? "Active" : "Inactive"}</dd></div><div><dt>Tenants</dt><dd>{viewing.assignments.length || "None"}</dd></div><div><dt>Description</dt><dd>{viewing.description || "—"}</dd></div><div><dt>Permissions</dt><dd>{viewing.permissions.length ? joinLabels(viewing.permissions.map((name) => ({ name }))) : "—"}</dd></div></dl>}
<Dialog open={Boolean(viewing)} title={viewing?.name || "i18n:govoplan-admin.template_details.d5d75e4d"} onClose={() => setViewing(null)} className="admin-dialog admin-dialog-wide" footer={<Button onClick={() => setViewing(null)}>i18n:govoplan-admin.close.bbfa773e</Button>}>
{viewing && <dl className="admin-details-grid"><div><dt>i18n:govoplan-admin.kind.e00ac23f</dt><dd>{viewing.kind}</dd></div><div><dt>i18n:govoplan-admin.slug.094da9b9</dt><dd>{viewing.slug}</dd></div><div><dt>i18n:govoplan-admin.status.bae7d5be</dt><dd>{viewing.is_active ? "i18n:govoplan-admin.active.a733b809" : "i18n:govoplan-admin.inactive.09af574c"}</dd></div><div><dt>i18n:govoplan-admin.tenants.1f7ae776</dt><dd>{viewing.assignments.length || "i18n:govoplan-admin.none.6eef6648"}</dd></div><div><dt>i18n:govoplan-admin.description.55f8ebc8</dt><dd>{viewing.description || "—"}</dd></div><div><dt>i18n:govoplan-admin.permissions.d06d5557</dt><dd>{viewing.permissions.length ? joinLabels(viewing.permissions.map((name) => ({ name }))) : "—"}</dd></div></dl>}
</Dialog>
<ConfirmDialog open={Boolean(deleting)} title={kind === "group" ? "Delete group template" : "Delete tenant role"} message={`Delete ${deleting?.name}? Removal is blocked while a materialized tenant definition still has members or assignments.`} confirmLabel={kind === "group" ? "Delete template" : "Delete tenant role"} tone="danger" busy={busy} onCancel={() => setDeleting(null)} onConfirm={() => void remove()} />
</>
);
<ConfirmDialog open={Boolean(deleting)} title={kind === "group" ? "i18n:govoplan-admin.delete_group_template.8745d842" : "i18n:govoplan-admin.delete_tenant_role.4efa0813"} message={i18nMessage("i18n:govoplan-admin.delete_value_removal_is_blocked_while_a_material.d4eba73d", { value0: deleting?.name })} confirmLabel={kind === "group" ? "i18n:govoplan-admin.delete_template.399bf72a" : "i18n:govoplan-admin.delete_tenant_role.4efa0813"} tone="danger" busy={busy} onCancel={() => setDeleting(null)} onConfirm={() => void remove()} />
</>);
}
function draftKey(draft: typeof emptyDraft): string {
return JSON.stringify(draft);
}

View File

@@ -1,20 +1,24 @@
import { useEffect, useState } from "react";
import type { ApiSettings } from "@govoplan/core-webui";
import { AdminPageLayout, adminErrorMessage, Button, dispatchPlatformModulesChanged, StatusBadge, ToggleSwitch } from "@govoplan/core-webui";
import { AdminPageLayout, adminErrorMessage, Button, dispatchPlatformModulesChanged, StatusBadge, ToggleSwitch, i18nMessage, useUnsavedDraftGuard } from "@govoplan/core-webui";
import {
cancelModuleInstallerRequest,
clearModuleInstallPlan,
createConfigurationChangeRequest,
createModuleInstallerRequest,
fetchModuleCatalog,
fetchModuleInstallPlan,
fetchModuleInstallerRequests,
fetchModuleInstallerRuns,
fetchModulePackageCatalog,
fetchSystemSettings,
planModuleInstallFromCatalog,
planModuleUninstall,
retryModuleInstallerRequest,
updateModuleInstallPlan,
updateModuleState,
updateSystemSettings,
type ConfigurationChangeRequest,
type ModuleCatalogItem,
type ModuleCatalogResponse,
type ModuleInstallerRequestListResponse,
@@ -22,21 +26,24 @@ import {
type ModuleInstallerRunListResponse,
type ModuleInstallPlanItem,
type ModuleInstallPlanResponse,
type ModuleLicenseDiagnostics,
type ModulePackageCatalogResponse,
type ModulePackageCatalogItem
} from "../../api/admin";
type ModulePackageCatalogItem } from
"../../api/admin";
export default function ModuleManagementPanel({ settings, canWrite, canAccessMaintenance }: { settings: ApiSettings; canWrite: boolean; canAccessMaintenance: boolean }) {
export default function ModuleManagementPanel({ settings, canWrite, canAccessMaintenance }: {settings: ApiSettings;canWrite: boolean;canAccessMaintenance: boolean;}) {
const [catalog, setCatalog] = useState<ModuleCatalogResponse | null>(null);
const [installPlan, setInstallPlan] = useState<ModuleInstallPlanResponse | null>(null);
const [installerRuns, setInstallerRuns] = useState<ModuleInstallerRunListResponse | null>(null);
const [installerRequests, setInstallerRequests] = useState<ModuleInstallerRequestListResponse | null>(null);
const [packageCatalog, setPackageCatalog] = useState<ModulePackageCatalogResponse | null>(null);
const [draftEnabled, setDraftEnabled] = useState<Set<string> | null>(null);
const [moduleStateRequest, setModuleStateRequest] = useState<ModuleStateRequestState | null>(null);
const [draftPlanItems, setDraftPlanItems] = useState<ModuleInstallPlanItem[]>([]);
const [requestOptions, setRequestOptions] = useState<InstallerRequestFormOptions>(() => defaultInstallerRequestOptions());
const [loading, setLoading] = useState(true);
const [busy, setBusy] = useState(false);
const [maintenanceBusy, setMaintenanceBusy] = useState(false);
const [planBusy, setPlanBusy] = useState(false);
const [error, setError] = useState("");
const [success, setSuccess] = useState("");
@@ -46,12 +53,12 @@ export default function ModuleManagementPanel({ settings, canWrite, canAccessMai
setError("");
try {
const [loaded, loadedPlan, loadedRuns, loadedRequests, loadedPackageCatalog] = await Promise.all([
fetchModuleCatalog(settings),
fetchModuleInstallPlan(settings),
fetchModuleInstallerRuns(settings),
fetchModuleInstallerRequests(settings),
fetchModulePackageCatalog(settings)
]);
fetchModuleCatalog(settings),
fetchModuleInstallPlan(settings),
fetchModuleInstallerRuns(settings),
fetchModuleInstallerRequests(settings),
fetchModulePackageCatalog(settings)]
);
setCatalog(loaded);
setInstallPlan(loadedPlan);
setInstallerRuns(loadedRuns);
@@ -66,7 +73,7 @@ export default function ModuleManagementPanel({ settings, canWrite, canAccessMai
}
}
useEffect(() => { void load(); }, [settings.accessToken, settings.apiBaseUrl]);
useEffect(() => {void load();}, [settings.accessToken, settings.apiBaseUrl]);
const desired = draftEnabled ?? new Set(catalog?.desired_enabled ?? []);
const dirty = Boolean(catalog && !sameSet(desired, new Set(catalog.desired_enabled)));
@@ -76,37 +83,100 @@ export default function ModuleManagementPanel({ settings, canWrite, canAccessMai
const maintenanceEnabled = Boolean(catalog?.maintenance_mode.enabled || installPlan?.maintenance_mode.enabled);
const planDirty = Boolean(installPlan && JSON.stringify(normalizePlanItems(draftPlanItems)) !== JSON.stringify(normalizePlanItems(installPlan.items)));
const planValid = planValidationError(draftPlanItems) === "";
useUnsavedDraftGuard({
dirty: dirty || planDirty,
onSave: saveDirtyChanges,
onDiscard: () => {
if (catalog) setDraftEnabled(new Set(catalog.desired_enabled));
if (installPlan) setDraftPlanItems(installPlan.items);
}
});
const saveLabel = maintenanceEnabled ?
moduleStateRequest ? "i18n:govoplan-admin.apply_approved_request.ab218d9a" : "i18n:govoplan-admin.create_request_and_apply.da79e153" :
moduleStateRequest ? "i18n:govoplan-admin.waiting_for_maintenance.60d00019" : "i18n:govoplan-admin.create_dry_run_request.05b9cbdb";
const saveDisabled = !canWrite || !dirty || busy || !maintenanceEnabled && Boolean(moduleStateRequest);
function setModuleDesired(module: ModuleCatalogItem, enabled: boolean) {
if (module.protected) return;
setSuccess("");
setModuleStateRequest(null);
setDraftEnabled((current) => {
const next = new Set(current ?? catalog?.desired_enabled ?? []);
if (enabled) next.add(module.id);
else next.delete(module.id);
if (enabled) next.add(module.id);else
next.delete(module.id);
return next;
});
}
async function save() {
if (!draftEnabled) return;
async function save(): Promise<boolean> {
if (!draftEnabled) return false;
const enabledModules = Array.from(draftEnabled).sort();
const payloadKey = moduleStatePayloadKey(enabledModules);
setBusy(true);
setError("");
setSuccess("");
try {
const updated = await updateModuleState(settings, Array.from(draftEnabled).sort());
let request = moduleStateRequest?.payloadKey === payloadKey ? moduleStateRequest.request : null;
if (!request) {
request = await createConfigurationChangeRequest(settings, {
key: "module_management.desired_enabled",
value: { enabled_modules: enabledModules },
dry_run: true,
target: { scope: "system" },
reason: "i18n:govoplan-admin.update_enabled_modules.9b7ae790"
});
setModuleStateRequest({ request, payloadKey });
}
if (!maintenanceEnabled) {
setSuccess(i18nMessage("i18n:govoplan-admin.dry_run_change_request_created_value_enable_main.49826d07", { value0: request.id }));
return true;
}
const updated = await updateModuleState(settings, enabledModules, request.id);
setCatalog(updated);
setDraftEnabled(new Set(updated.desired_enabled));
setSuccess(updated.notes[0] ?? "Module state saved.");
setModuleStateRequest(null);
setSuccess(updated.notes[0] ?? "i18n:govoplan-admin.module_state_saved.e3e22105");
dispatchPlatformModulesChanged();
return true;
} catch (err) {
setError(adminErrorMessage(err));
return false;
} finally {
setBusy(false);
}
}
async function savePlan() {
async function enableMaintenanceMode() {
setMaintenanceBusy(true);
setError("");
setSuccess("");
try {
const current = await fetchSystemSettings(settings);
const updated = await updateSystemSettings(settings, {
default_locale: current.default_locale,
allow_tenant_custom_groups: current.allow_tenant_custom_groups,
allow_tenant_custom_roles: current.allow_tenant_custom_roles,
allow_tenant_api_keys: current.allow_tenant_api_keys,
maintenance_mode: {
enabled: true,
message: current.maintenance_mode.message || "i18n:govoplan-admin.module_maintenance_in_progress.f6395152"
}
});
setCatalog((value) => value ? { ...value, maintenance_mode: updated.maintenance_mode } : value);
setInstallPlan((value) => value ? {
...value,
maintenance_mode: updated.maintenance_mode,
preflight: value.preflight ? { ...value.preflight, maintenance_mode: updated.maintenance_mode.enabled } : value.preflight
} : value);
setSuccess("i18n:govoplan-admin.maintenance_mode_enabled_apply_the_module_state_.dda13e5c");
} catch (err) {
setError(adminErrorMessage(err));
} finally {
setMaintenanceBusy(false);
}
}
async function savePlan(): Promise<boolean> {
setPlanBusy(true);
setError("");
setSuccess("");
@@ -114,14 +184,23 @@ export default function ModuleManagementPanel({ settings, canWrite, canAccessMai
const updated = await updateModuleInstallPlan(settings, normalizePlanItems(draftPlanItems));
setInstallPlan(updated);
setDraftPlanItems(updated.items);
setSuccess(updated.notes[0] ?? "Module install plan saved.");
setSuccess(updated.notes[0] ?? "i18n:govoplan-admin.module_install_plan_saved.5d43d944");
return true;
} catch (err) {
setError(adminErrorMessage(err));
return false;
} finally {
setPlanBusy(false);
}
}
async function saveDirtyChanges(): Promise<boolean> {
let ok = true;
if (dirty) ok = await save() && ok;
if (planDirty) ok = await savePlan() && ok;
return ok;
}
async function clearPlan() {
setPlanBusy(true);
setError("");
@@ -130,7 +209,7 @@ export default function ModuleManagementPanel({ settings, canWrite, canAccessMai
const updated = await clearModuleInstallPlan(settings);
setInstallPlan(updated);
setDraftPlanItems([]);
setSuccess(updated.notes[0] ?? "Module install plan cleared.");
setSuccess(updated.notes[0] ?? "i18n:govoplan-admin.module_install_plan_cleared.afa2698d");
} catch (err) {
setError(adminErrorMessage(err));
} finally {
@@ -155,7 +234,7 @@ export default function ModuleManagementPanel({ settings, canWrite, canAccessMai
const updated = await planModuleInstallFromCatalog(settings, item.module_id);
setInstallPlan(updated);
setDraftPlanItems(updated.items);
setSuccess(updated.notes[0] ?? "Catalog install entry planned.");
setSuccess(updated.notes[0] ?? "i18n:govoplan-admin.catalog_install_entry_planned.13b23f4f");
} catch (err) {
setError(adminErrorMessage(err));
} finally {
@@ -171,7 +250,7 @@ export default function ModuleManagementPanel({ settings, canWrite, canAccessMai
const updated = await planModuleUninstall(settings, module.id);
setInstallPlan(updated);
setDraftPlanItems(updated.items);
setSuccess(updated.notes[0] ?? "Package uninstall planned.");
setSuccess(updated.notes[0] ?? "i18n:govoplan-admin.package_uninstall_planned.56e8566d");
} catch (err) {
setError(adminErrorMessage(err));
} finally {
@@ -186,11 +265,11 @@ export default function ModuleManagementPanel({ settings, canWrite, canAccessMai
setSuccess("");
try {
const request = await createModuleInstallerRequest(settings, normalizeRequestOptions(requestOptions, installPlan));
setSuccess(`Installer request queued: ${request.request_id}`);
setSuccess(i18nMessage("i18n:govoplan-admin.installer_request_queued_value.00726abb", { value0: request.request_id }));
const [loadedRuns, loadedRequests] = await Promise.all([
fetchModuleInstallerRuns(settings),
fetchModuleInstallerRequests(settings)
]);
fetchModuleInstallerRuns(settings),
fetchModuleInstallerRequests(settings)]
);
setInstallerRuns(loadedRuns);
setInstallerRequests(loadedRequests);
} catch (err) {
@@ -206,7 +285,7 @@ export default function ModuleManagementPanel({ settings, canWrite, canAccessMai
setSuccess("");
try {
const request = await cancelModuleInstallerRequest(settings, requestId);
setSuccess(`Installer request cancelled: ${request.request_id}`);
setSuccess(i18nMessage("i18n:govoplan-admin.installer_request_cancelled_value.1d6d099d", { value0: request.request_id }));
setInstallerRequests(await fetchModuleInstallerRequests(settings));
} catch (err) {
setError(adminErrorMessage(err));
@@ -221,7 +300,7 @@ export default function ModuleManagementPanel({ settings, canWrite, canAccessMai
setSuccess("");
try {
const request = await retryModuleInstallerRequest(settings, requestId);
setSuccess(`Installer request queued: ${request.request_id}`);
setSuccess(i18nMessage("i18n:govoplan-admin.installer_request_queued_value.00726abb", { value0: request.request_id }));
setInstallerRequests(await fetchModuleInstallerRequests(settings));
} catch (err) {
setError(adminErrorMessage(err));
@@ -232,22 +311,33 @@ export default function ModuleManagementPanel({ settings, canWrite, canAccessMai
return (
<AdminPageLayout
title="System modules"
description="Installed modules, active runtime state, and saved startup state."
title="i18n:govoplan-admin.system_modules.629539f2"
description="i18n:govoplan-admin.installed_modules_active_runtime_state_and_saved.f33907dd"
loading={loading}
error={error}
success={success}
actions={<><Button onClick={() => void load()} disabled={loading || busy}>Reload</Button><Button variant="primary" onClick={() => void save()} disabled={!canWrite || !dirty || busy}>{busy ? "Applying..." : "Save and apply"}</Button></>}
>
actions={<><Button onClick={() => void load()} disabled={loading || busy}>i18n:govoplan-admin.reload.cce71553</Button><Button variant="primary" onClick={() => void save()} disabled={saveDisabled}>{busy ? "i18n:govoplan-admin.working.049ac820" : saveLabel}</Button></>}>
{catalog && <>
<div className="metric-grid module-management-metrics">
<ModuleMetric label="Installed" value={catalog.modules.length} detail="Discovered packages" />
<ModuleMetric label="Active" value={activeCount} detail="Running registry" />
<ModuleMetric label="Saved" value={desiredCount} detail="Startup state" />
<ModuleMetric label="Drift" value={pendingCount} detail={pendingCount ? "Runtime differs" : "Runtime matches"} tone={pendingCount ? "warning" : "good"} />
<ModuleMetric label="Maintenance" value={maintenanceEnabled ? "On" : "Off"} detail={canAccessMaintenance ? "Bypass allowed" : "Bypass denied"} tone={maintenanceEnabled ? "warning" : "info"} />
<ModuleMetric label="i18n:govoplan-admin.installed.7bb4405c" value={catalog.modules.length} detail="i18n:govoplan-admin.discovered_packages.660902de" />
<ModuleMetric label="i18n:govoplan-admin.active.a733b809" value={activeCount} detail="i18n:govoplan-admin.running_registry.5274b24b" />
<ModuleMetric label="i18n:govoplan-admin.saved.c0ae8f6e" value={desiredCount} detail="i18n:govoplan-admin.startup_state.d1300be6" />
<ModuleMetric label="i18n:govoplan-admin.drift.4876f7b9" value={pendingCount} detail={pendingCount ? "i18n:govoplan-admin.runtime_differs.43d1fd78" : "i18n:govoplan-admin.runtime_matches.a84afa48"} tone={pendingCount ? "warning" : "good"} />
<ModuleMetric label="i18n:govoplan-admin.maintenance.94de303b" value={maintenanceEnabled ? "i18n:govoplan-admin.on.e0049a66" : "i18n:govoplan-admin.off.e3de5ab0"} detail={canAccessMaintenance ? "i18n:govoplan-admin.bypass_allowed.4e347c27" : "i18n:govoplan-admin.bypass_denied.ab987400"} tone={maintenanceEnabled ? "warning" : "info"} />
</div>
{dirty && <div className="module-management-notes">
<p className="alert warning">i18n:govoplan-admin.changing_enabled_modules_requires_a_dry_run_chan.10b6f5ed</p>
{moduleStateRequest && <p>i18n:govoplan-admin.change_request_ready.b17e8c57 <code>{moduleStateRequest.request.id}</code></p>}
{!maintenanceEnabled && <div className="button-row compact-actions">
<Button onClick={() => void enableMaintenanceMode()} disabled={!canWrite || !canAccessMaintenance || maintenanceBusy}>
{maintenanceBusy ? "i18n:govoplan-admin.enabling.2b8e03e6" : "i18n:govoplan-admin.enable_maintenance_mode.75f98d57"}
</Button>
</div>}
{!maintenanceEnabled && !canAccessMaintenance && <p className="alert warning">i18n:govoplan-admin.enabling_maintenance_mode_requires_system_mainte.062d3968</p>}
</div>}
<div className="module-management-list">
{catalog.modules.map((module) => {
const desiredEnabled = desired.has(module.id);
@@ -261,27 +351,27 @@ export default function ModuleManagementPanel({ settings, canWrite, canAccessMai
</div>
<div className="module-management-meta">
<ModuleStatus module={module} desiredEnabled={desiredEnabled} />
{module.protected && <StatusBadge status="locked" label="Locked" />}
{module.protected && <StatusBadge status="locked" label="i18n:govoplan-admin.locked.a798882f" />}
{module.frontend_package && <span>{module.frontend_package}</span>}
{module.migration_module_id && <span>DB: {module.migration_module_id}</span>}
{module.migration_module_id && <span>i18n:govoplan-admin.db.e355c23a {module.migration_module_id}</span>}
</div>
<div className="module-management-details">
<span>Requires: {module.dependencies.length ? module.dependencies.join(", ") : "none"}</span>
<span>Optional: {module.optional_dependencies.length ? module.optional_dependencies.join(", ") : "none"}</span>
<span>Dependents: {module.dependents.length ? module.dependents.join(", ") : "none"}</span>
<span>i18n:govoplan-admin.requires.a4fc9357 {module.dependencies.length ? module.dependencies.join(", ") : "none"}</span>
<span>i18n:govoplan-admin.optional.166b8369 {module.optional_dependencies.length ? module.optional_dependencies.join(", ") : "none"}</span>
<span>i18n:govoplan-admin.dependents.072665c4 {module.dependents.length ? module.dependents.join(", ") : "none"}</span>
</div>
</div>
<div className="module-management-toggle">
<ToggleSwitch
label={desiredEnabled ? "Enabled" : "Disabled"}
label={desiredEnabled ? "i18n:govoplan-admin.enabled.df174a3f" : "i18n:govoplan-admin.disabled.f4f4473d"}
checked={desiredEnabled}
disabled={!canWrite || module.protected || busy}
onChange={(checked) => setModuleDesired(module, checked)}
/>
{module.install_uninstall_supported && <Button onClick={() => void addUninstallPlan(module)} disabled={!canWrite || planBusy || module.current_enabled || module.desired_enabled}>Plan uninstall</Button>}
onChange={(checked) => setModuleDesired(module, checked)} />
{module.install_uninstall_supported && <Button onClick={() => void addUninstallPlan(module)} disabled={!canWrite || planBusy || module.current_enabled || module.desired_enabled}>i18n:govoplan-admin.plan_uninstall.e62804ab</Button>}
</div>
</div>
);
</div>);
})}
</div>
@@ -292,27 +382,28 @@ export default function ModuleManagementPanel({ settings, canWrite, canAccessMai
{packageCatalog && <div className="module-package-catalog">
<div className="module-install-plan-heading">
<div>
<h2>Package catalog</h2>
<p>Approved package references that can be added to the operator install plan.</p>
<h2>i18n:govoplan-admin.package_catalog.b36e6d31</h2>
<p>i18n:govoplan-admin.approved_package_references_that_can_be_added_to.cc911c16</p>
</div>
<div className="button-row compact-actions">
{packageCatalog.channel && <StatusBadge status="inactive" label={`Channel: ${packageCatalog.channel}`} />}
{packageCatalog.sequence !== null && packageCatalog.sequence !== undefined && <StatusBadge status="inactive" label={`Seq ${packageCatalog.sequence}`} />}
{packageCatalog.channel && <StatusBadge status="inactive" label={i18nMessage("i18n:govoplan-admin.channel_value.d3894efe", { value0: packageCatalog.channel })} />}
{packageCatalog.sequence !== null && packageCatalog.sequence !== undefined && <StatusBadge status="inactive" label={i18nMessage("i18n:govoplan-admin.seq_value.0ae5fa47", { value0: packageCatalog.sequence })} />}
<StatusBadge
status={packageCatalog.trusted ? "success" : packageCatalog.signed ? "warning" : "inactive"}
label={packageCatalog.trusted ? "Signed and trusted" : packageCatalog.signed ? "Signed, untrusted" : "Unsigned"}
/>
label={packageCatalog.trusted ? "i18n:govoplan-admin.signed_and_trusted.bee31302" : packageCatalog.signed ? "i18n:govoplan-admin.signed_untrusted.864ed50c" : "i18n:govoplan-admin.unsigned.e91344ea"} />
</div>
</div>
{(packageCatalog.source || packageCatalog.path) && <p className="module-package-catalog-description">Catalog {packageCatalog.source_type ?? "source"}: <code>{packageCatalog.source ?? packageCatalog.path}</code></p>}
{(packageCatalog.generated_at || packageCatalog.expires_at) && <p className="module-package-catalog-description">Generated: {packageCatalog.generated_at ?? "unknown"} · Expires: {packageCatalog.expires_at ?? "unknown"}</p>}
{(packageCatalog.source || packageCatalog.path) && <p className="module-package-catalog-description">i18n:govoplan-admin.catalog.4a88d27b {packageCatalog.source_type ?? "source"}: <code>{packageCatalog.source ?? packageCatalog.path}</code>{packageCatalog.cache_used && packageCatalog.cache_path ? <> i18n:govoplan-admin.cached_from.d73c5b2a <code>{packageCatalog.cache_path}</code></> : null}</p>}
{(packageCatalog.generated_at || packageCatalog.not_before || packageCatalog.expires_at) && <p className="module-package-catalog-description">i18n:govoplan-admin.generated.a2edf57c {packageCatalog.generated_at ?? "unknown"} i18n:govoplan-admin.valid_after.c615c873 {packageCatalog.not_before ?? "now"} i18n:govoplan-admin.expires.2d21b7de {packageCatalog.expires_at ?? "unknown"}</p>}
<LicenseStatus license={packageCatalog.license} />
{packageCatalog.error && <p className="alert warning">{packageCatalog.error}</p>}
{packageCatalog.warnings.map((warning) => <p key={warning} className="alert warning">{warning}</p>)}
{!packageCatalog.configured && <div className="module-install-plan-empty">No package catalog configured. Set GOVOPLAN_MODULE_PACKAGE_CATALOG or GOVOPLAN_MODULE_PACKAGE_CATALOG_URL to enable curated install entries.</div>}
{packageCatalog.configured && packageCatalog.modules.length === 0 && !packageCatalog.error && <div className="module-install-plan-empty">Package catalog is configured but contains no entries.</div>}
{!packageCatalog.configured && <div className="module-install-plan-empty">i18n:govoplan-admin.no_package_catalog_configured_set_govoplan_modul.2c5fceb3</div>}
{packageCatalog.configured && packageCatalog.modules.length === 0 && !packageCatalog.error && <div className="module-install-plan-empty">i18n:govoplan-admin.package_catalog_is_configured_but_contains_no_en.b6b053cb</div>}
{packageCatalog.modules.length > 0 && <div className="module-package-catalog-list">
{packageCatalog.modules.map((item) => (
<div key={`${item.module_id}-${item.version ?? "latest"}`} className="module-package-catalog-row">
{packageCatalog.modules.map((item) =>
<div key={`${item.module_id}-${item.version ?? "latest"}`} className="module-package-catalog-row">
<div className="module-management-main">
<div className="module-management-title">
<strong>{item.name}</strong>
@@ -323,75 +414,75 @@ export default function ModuleManagementPanel({ settings, canWrite, canAccessMai
{item.python_package && <span>{item.python_package}</span>}
{item.webui_package && <span>{item.webui_package}</span>}
{item.tags.length > 0 && <span>{item.tags.join(", ")}</span>}
{item.license_features.length > 0 && <span>License: {item.license_features.join(", ")}</span>}
{item.license_missing_features.length > 0 && <span>Missing: {item.license_missing_features.join(", ")}</span>}
{item.license_features.length > 0 && <span>i18n:govoplan-admin.license.de13bf1a {item.license_features.join(", ")}</span>}
{item.license_missing_features.length > 0 && <span>i18n:govoplan-admin.missing.feb2bbaa {item.license_missing_features.join(", ")}</span>}
</div>
{item.description && <p className="module-package-catalog-description">{item.description}</p>}
{item.license_reason && <p className={`module-package-catalog-description${item.license_allowed ? "" : " alert warning"}`}>{item.license_reason}</p>}
</div>
<Button onClick={() => void addCatalogItem(item)} disabled={!canWrite || planBusy || !packageCatalog.valid || !item.license_allowed || item.action !== "install"}>Plan install</Button>
<Button onClick={() => void addCatalogItem(item)} disabled={!canWrite || planBusy || !packageCatalog.valid || !item.license_allowed || item.action !== "install"}>i18n:govoplan-admin.plan_install.e82bffe6</Button>
</div>
))}
)}
</div>}
</div>}
{installPlan && <div className="module-install-plan-panel">
<div className="module-install-plan-heading">
<div>
<h2>Operator install plan</h2>
<p>Plan package installs and removals here, then apply the rendered commands from an operator shell during maintenance mode.</p>
<h2>i18n:govoplan-admin.operator_install_plan.b203aabc</h2>
<p>i18n:govoplan-admin.plan_package_installs_and_removals_here_then_app.4aeb03bf</p>
</div>
<div className="button-row compact-actions">
<Button onClick={addPlanItem} disabled={!canWrite || planBusy}>Add plan item</Button>
<Button onClick={() => void clearPlan()} disabled={!canWrite || planBusy || draftPlanItems.length === 0}>Clear</Button>
<Button variant="primary" onClick={() => void savePlan()} disabled={!canWrite || planBusy || !planDirty || !planValid}>{planBusy ? "Saving..." : "Save plan"}</Button>
<Button onClick={addPlanItem} disabled={!canWrite || planBusy}>i18n:govoplan-admin.add_plan_item.fa55f028</Button>
<Button onClick={() => void clearPlan()} disabled={!canWrite || planBusy || draftPlanItems.length === 0}>i18n:govoplan-admin.clear.719ea396</Button>
<Button variant="primary" onClick={() => void savePlan()} disabled={!canWrite || planBusy || !planDirty || !planValid}>{planBusy ? "i18n:govoplan-admin.saving.ae7e8875" : "i18n:govoplan-admin.save_plan.842dc280"}</Button>
</div>
</div>
{planValidationError(draftPlanItems) && <p className="alert warning">{planValidationError(draftPlanItems)}</p>}
{!maintenanceEnabled && <p className="alert warning">Maintenance mode is off. Package changes should be applied only after enabling maintenance mode.</p>}
{!maintenanceEnabled && <p className="alert warning">i18n:govoplan-admin.maintenance_mode_is_off_package_changes_should_b.1d262629</p>}
{installPlan.preflight && <div className={`module-install-preflight ${installPlan.preflight.allowed ? "is-allowed" : "is-blocked"}`}>
<div className="module-install-preflight-title">
<strong>{installPlan.preflight.allowed ? "Installer preflight passed" : "Installer preflight blocked"}</strong>
<span>{installPlan.preflight.restart_required ? "Restart/reload required after package changes" : "No package restart pending"}</span>
{installPlan.preflight.frontend_rebuild_required && <span>WebUI rebuild required</span>}
<strong>{installPlan.preflight.allowed ? "i18n:govoplan-admin.installer_preflight_passed.6d5f8060" : "i18n:govoplan-admin.installer_preflight_blocked.cf83bf79"}</strong>
<span>{installPlan.preflight.restart_required ? "i18n:govoplan-admin.restart_reload_required_after_package_changes.3a99cd63" : "i18n:govoplan-admin.no_package_restart_pending.efe6b8ce"}</span>
{installPlan.preflight.frontend_rebuild_required && <span>i18n:govoplan-admin.webui_rebuild_required.010ce49f</span>}
</div>
{installPlan.preflight.issues.length > 0 && <div className="module-install-preflight-issues">
{installPlan.preflight.issues.map((issue, index) => (
<div key={`${issue.code}-${issue.module_id ?? "global"}-${index}`} className={`module-install-preflight-issue severity-${issue.severity}`}>
{installPlan.preflight.issues.map((issue, index) =>
<div key={`${issue.code}-${issue.module_id ?? "global"}-${index}`} className={`module-install-preflight-issue severity-${issue.severity}`}>
<strong>{issue.severity}</strong>
<span>{issue.module_id ? `${issue.module_id}: ` : ""}{issue.message}</span>
</div>
))}
)}
</div>}
{installPlan.preflight.checklist.length > 0 && <div className="module-install-checklist">
{installPlan.preflight.checklist.map((item) => (
<div key={item.id} className={`module-install-checklist-item status-${item.status}`}>
{installPlan.preflight.checklist.map((item) =>
<div key={item.id} className={`module-install-checklist-item status-${item.status}`}>
<strong>{item.label}</strong>
<span>{item.status}</span>
{item.detail && <p>{item.detail}</p>}
</div>
))}
)}
</div>}
</div>}
<div className="module-install-plan-list">
{draftPlanItems.length === 0 && <div className="module-install-plan-empty">No package changes planned.</div>}
{draftPlanItems.map((item, index) => (
<div className="module-install-plan-row" key={`${item.module_id}-${index}`}>
<label><span>Action</span><select value={item.action} disabled={!canWrite || planBusy} onChange={(event) => updatePlanItem(index, { action: event.target.value as ModuleInstallPlanItem["action"] })}><option value="install">Install</option><option value="uninstall">Uninstall</option></select></label>
<label><span>Module</span><input value={item.module_id} disabled={!canWrite || planBusy} onChange={(event) => updatePlanItem(index, { module_id: event.target.value })} placeholder="files" /></label>
<label><span>Python package</span><input value={item.python_package ?? ""} disabled={!canWrite || planBusy} onChange={(event) => updatePlanItem(index, { python_package: event.target.value })} placeholder="govoplan-files" /></label>
<ToggleSwitch label="Destroy data" checked={Boolean(item.destroy_data)} onChange={(checked) => updatePlanItem(index, { destroy_data: checked })} disabled={!canWrite || planBusy || item.action !== "uninstall"} />
<label className="wide"><span>Python ref</span><input value={item.python_ref ?? ""} disabled={!canWrite || planBusy || item.action === "uninstall"} onChange={(event) => updatePlanItem(index, { python_ref: event.target.value })} placeholder="govoplan-files @ git+ssh://...@v0.1.4" /></label>
<label><span>WebUI package</span><input value={item.webui_package ?? ""} disabled={!canWrite || planBusy} onChange={(event) => updatePlanItem(index, { webui_package: event.target.value })} placeholder="@govoplan/files-webui" /></label>
<label className="wide"><span>WebUI ref</span><input value={item.webui_ref ?? ""} disabled={!canWrite || planBusy || item.action === "uninstall"} onChange={(event) => updatePlanItem(index, { webui_ref: event.target.value })} placeholder="git+ssh://...#v0.1.4" /></label>
<label><span>Status</span><select value={item.status} disabled={!canWrite || planBusy} onChange={(event) => updatePlanItem(index, { status: event.target.value as ModuleInstallPlanItem["status"] })}><option value="planned">Planned</option><option value="applied">Applied</option><option value="blocked">Blocked</option></select></label>
<label className="wide"><span>Notes</span><input value={item.notes ?? ""} disabled={!canWrite || planBusy} onChange={(event) => updatePlanItem(index, { notes: event.target.value })} /></label>
<div className="module-install-plan-actions"><Button onClick={() => setDraftPlanItems((items) => items.filter((_, itemIndex) => itemIndex !== index))} disabled={!canWrite || planBusy}>Remove</Button></div>
{draftPlanItems.length === 0 && <div className="module-install-plan-empty">i18n:govoplan-admin.no_package_changes_planned.f12d8b33</div>}
{draftPlanItems.map((item, index) =>
<div className="module-install-plan-row" key={`${item.module_id}-${index}`}>
<label><span>i18n:govoplan-admin.action.97c89a4d</span><select value={item.action} disabled={!canWrite || planBusy} onChange={(event) => updatePlanItem(index, { action: event.target.value as ModuleInstallPlanItem["action"] })}><option value="install">i18n:govoplan-admin.install.fd6c3ebf</option><option value="uninstall">i18n:govoplan-admin.uninstall.a735da1d</option></select></label>
<label><span>i18n:govoplan-admin.module.b8ff0289</span><input value={item.module_id} disabled={!canWrite || planBusy} onChange={(event) => updatePlanItem(index, { module_id: event.target.value })} placeholder="files" /></label>
<label><span>i18n:govoplan-admin.python_package.34591c71</span><input value={item.python_package ?? ""} disabled={!canWrite || planBusy} onChange={(event) => updatePlanItem(index, { python_package: event.target.value })} placeholder="govoplan-files" /></label>
<ToggleSwitch label="i18n:govoplan-admin.destroy_data.34557c3c" checked={Boolean(item.destroy_data)} onChange={(checked) => updatePlanItem(index, { destroy_data: checked })} disabled={!canWrite || planBusy || item.action !== "uninstall"} />
<label className="wide"><span>i18n:govoplan-admin.python_ref.1af35d0d</span><input value={item.python_ref ?? ""} disabled={!canWrite || planBusy || item.action === "uninstall"} onChange={(event) => updatePlanItem(index, { python_ref: event.target.value })} placeholder="govoplan-files @ git+ssh://...@v0.1.4" /></label>
<label><span>i18n:govoplan-admin.webui_package.19645536</span><input value={item.webui_package ?? ""} disabled={!canWrite || planBusy} onChange={(event) => updatePlanItem(index, { webui_package: event.target.value })} placeholder="@govoplan/files-webui" /></label>
<label className="wide"><span>i18n:govoplan-admin.webui_ref.cbae3559</span><input value={item.webui_ref ?? ""} disabled={!canWrite || planBusy || item.action === "uninstall"} onChange={(event) => updatePlanItem(index, { webui_ref: event.target.value })} placeholder="git+ssh://...#v0.1.4" /></label>
<label><span>i18n:govoplan-admin.status.bae7d5be</span><select value={item.status} disabled={!canWrite || planBusy} onChange={(event) => updatePlanItem(index, { status: event.target.value as ModuleInstallPlanItem["status"] })}><option value="planned">i18n:govoplan-admin.planned.9cbe42aa</option><option value="applied">i18n:govoplan-admin.applied.a3e4a569</option><option value="blocked">i18n:govoplan-admin.blocked.99613c74</option></select></label>
<label className="wide"><span>i18n:govoplan-admin.notes.70440046</span><input value={item.notes ?? ""} disabled={!canWrite || planBusy} onChange={(event) => updatePlanItem(index, { notes: event.target.value })} /></label>
<div className="module-install-plan-actions"><Button onClick={() => setDraftPlanItems((items) => items.filter((_, itemIndex) => itemIndex !== index))} disabled={!canWrite || planBusy}>i18n:govoplan-admin.remove.e963907d</Button></div>
</div>
))}
)}
</div>
<pre className="code-panel module-install-plan-commands">{installerCommand(installPlan.preflight?.frontend_rebuild_required ?? false)}</pre>
@@ -401,26 +492,26 @@ export default function ModuleManagementPanel({ settings, canWrite, canAccessMai
<div className="module-installer-request-panel">
<div className="module-install-plan-heading">
<div>
<h2>Daemon execution</h2>
<p>Queue the saved plan for a separate installer daemon process.</p>
<h2>i18n:govoplan-admin.daemon_execution.cc0fad8d</h2>
<p>i18n:govoplan-admin.queue_the_saved_plan_for_a_separate_installer_da.92d4c96e</p>
</div>
<Button variant="primary" onClick={() => void queueInstallerRequest()} disabled={!canWrite || !canAccessMaintenance || planBusy || planDirty || !maintenanceEnabled || !installPlan.preflight?.allowed}>
Queue supervised run
i18n:govoplan-admin.queue_supervised_run.a53f248c
</Button>
</div>
{planDirty && <p className="alert warning">Save the install plan before queueing a daemon request.</p>}
{!maintenanceEnabled && <p className="alert warning">Maintenance mode must be enabled before queueing a daemon request.</p>}
{!canAccessMaintenance && <p className="alert warning">Queueing installer requests requires maintenance access.</p>}
{planDirty && <p className="alert warning">i18n:govoplan-admin.save_the_install_plan_before_queueing_a_daemon_r.3ba00691</p>}
{!maintenanceEnabled && <p className="alert warning">i18n:govoplan-admin.maintenance_mode_must_be_enabled_before_queueing.d233a32f</p>}
{!canAccessMaintenance && <p className="alert warning">i18n:govoplan-admin.queueing_installer_requests_requires_maintenance.ae81f9ac</p>}
<div className="module-installer-request-grid">
<ToggleSwitch label="Run migrations" checked={requestOptions.migrateDatabase} onChange={(checked) => setRequestOptions((current) => ({ ...current, migrateDatabase: checked }))} disabled={!canWrite || planBusy} />
<ToggleSwitch label="Build WebUI" checked={requestOptions.buildWebui} onChange={(checked) => setRequestOptions((current) => ({ ...current, buildWebui: checked }))} disabled={!canWrite || planBusy} />
<ToggleSwitch label="Activate installs" checked={requestOptions.activateInstalledModules} onChange={(checked) => setRequestOptions((current) => ({ ...current, activateInstalledModules: checked }))} disabled={!canWrite || planBusy} />
<ToggleSwitch label="Drop uninstalls from startup" checked={requestOptions.removeUninstalledModulesFromDesired} onChange={(checked) => setRequestOptions((current) => ({ ...current, removeUninstalledModulesFromDesired: checked }))} disabled={!canWrite || planBusy} />
<label><span>Health URLs</span><textarea value={requestOptions.healthUrls} disabled={!canWrite || planBusy} onChange={(event) => setRequestOptions((current) => ({ ...current, healthUrls: event.target.value }))} placeholder="http://127.0.0.1:8000/health" /></label>
<label><span>Restart commands</span><textarea value={requestOptions.restartCommands} disabled={!canWrite || planBusy} onChange={(event) => setRequestOptions((current) => ({ ...current, restartCommands: event.target.value }))} placeholder="systemctl restart govoplan.service" /></label>
<label><span>DB backup command</span><textarea value={requestOptions.databaseBackupCommand} disabled={!canWrite || planBusy} onChange={(event) => setRequestOptions((current) => ({ ...current, databaseBackupCommand: event.target.value }))} placeholder={"pg_dump --format=custom \"$GOVOPLAN_DATABASE_URL\" > \"$GOVOPLAN_DATABASE_BACKUP_PATH\""} /></label>
<label><span>DB restore command</span><textarea value={requestOptions.databaseRestoreCommand} disabled={!canWrite || planBusy} onChange={(event) => setRequestOptions((current) => ({ ...current, databaseRestoreCommand: event.target.value }))} placeholder={"pg_restore --clean --if-exists --dbname \"$GOVOPLAN_DATABASE_URL\" \"$GOVOPLAN_DATABASE_BACKUP_PATH\""} /></label>
<label><span>DB restore-check command</span><textarea value={requestOptions.databaseRestoreCheckCommand} disabled={!canWrite || planBusy} onChange={(event) => setRequestOptions((current) => ({ ...current, databaseRestoreCheckCommand: event.target.value }))} placeholder={"pg_restore --list \"$GOVOPLAN_DATABASE_BACKUP_PATH\" >/dev/null"} /></label>
<ToggleSwitch label="i18n:govoplan-admin.run_migrations.db6e0ce2" checked={requestOptions.migrateDatabase} onChange={(checked) => setRequestOptions((current) => ({ ...current, migrateDatabase: checked }))} disabled={!canWrite || planBusy} />
<ToggleSwitch label="i18n:govoplan-admin.build_webui.fe8ccad7" checked={requestOptions.buildWebui} onChange={(checked) => setRequestOptions((current) => ({ ...current, buildWebui: checked }))} disabled={!canWrite || planBusy} />
<ToggleSwitch label="i18n:govoplan-admin.activate_installs.731d22a1" checked={requestOptions.activateInstalledModules} onChange={(checked) => setRequestOptions((current) => ({ ...current, activateInstalledModules: checked }))} disabled={!canWrite || planBusy} />
<ToggleSwitch label="i18n:govoplan-admin.drop_uninstalls_from_startup.0e2a5c5b" checked={requestOptions.removeUninstalledModulesFromDesired} onChange={(checked) => setRequestOptions((current) => ({ ...current, removeUninstalledModulesFromDesired: checked }))} disabled={!canWrite || planBusy} />
<label><span>i18n:govoplan-admin.health_urls.3b86647c</span><textarea value={requestOptions.healthUrls} disabled={!canWrite || planBusy} onChange={(event) => setRequestOptions((current) => ({ ...current, healthUrls: event.target.value }))} placeholder="http://127.0.0.1:8000/health" /></label>
<label><span>i18n:govoplan-admin.restart_commands.035997f9</span><textarea value={requestOptions.restartCommands} disabled={!canWrite || planBusy} onChange={(event) => setRequestOptions((current) => ({ ...current, restartCommands: event.target.value }))} placeholder="systemctl restart govoplan.service" /></label>
<label><span>i18n:govoplan-admin.db_backup_command.5bded3ac</span><textarea value={requestOptions.databaseBackupCommand} disabled={!canWrite || planBusy} onChange={(event) => setRequestOptions((current) => ({ ...current, databaseBackupCommand: event.target.value }))} placeholder={"pg_dump --format=custom \"$GOVOPLAN_DATABASE_URL\" > \"$GOVOPLAN_DATABASE_BACKUP_PATH\""} /></label>
<label><span>i18n:govoplan-admin.db_restore_command.de11eb01</span><textarea value={requestOptions.databaseRestoreCommand} disabled={!canWrite || planBusy} onChange={(event) => setRequestOptions((current) => ({ ...current, databaseRestoreCommand: event.target.value }))} placeholder={"pg_restore --clean --if-exists --dbname \"$GOVOPLAN_DATABASE_URL\" \"$GOVOPLAN_DATABASE_BACKUP_PATH\""} /></label>
<label><span>i18n:govoplan-admin.db_restore_check_command.750ae936</span><textarea value={requestOptions.databaseRestoreCheckCommand} disabled={!canWrite || planBusy} onChange={(event) => setRequestOptions((current) => ({ ...current, databaseRestoreCheckCommand: event.target.value }))} placeholder={"pg_restore --list \"$GOVOPLAN_DATABASE_BACKUP_PATH\" >/dev/null"} /></label>
</div>
</div>
</div>}
@@ -428,88 +519,113 @@ export default function ModuleManagementPanel({ settings, canWrite, canAccessMai
{installerRequests && <div className="module-installer-runs">
<div className="module-installer-runs-heading">
<div>
<h2>Installer requests</h2>
<p>Queued daemon handoffs created from the admin UI or CLI.</p>
<h2>i18n:govoplan-admin.installer_requests.b88db439</h2>
<p>i18n:govoplan-admin.queued_daemon_handoffs_created_from_the_admin_ui.d7ae7914</p>
</div>
<StatusBadge
status={installerRequests.daemon.running ? statusTone(installerRequests.daemon.status) : "inactive"}
label={installerRequests.daemon.running ? `Daemon: ${installerRequests.daemon.status}` : "Daemon offline"}
/>
label={installerRequests.daemon.running ? i18nMessage("i18n:govoplan-admin.daemon_value.e54f5a31", { value0: installerRequests.daemon.status }) : "i18n:govoplan-admin.daemon_offline.314f19a4"} />
</div>
{installerRequests.requests.length === 0 && <div className="module-install-plan-empty">No installer requests recorded yet.</div>}
{installerRequests.requests.length === 0 && <div className="module-install-plan-empty">i18n:govoplan-admin.no_installer_requests_recorded_yet.17e630fa</div>}
{installerRequests.requests.length > 0 && <div className="module-installer-run-list">
{installerRequests.requests.map((request) => (
<div key={request.request_id} className="module-installer-run-row">
{installerRequests.requests.map((request) =>
<div key={request.request_id} className="module-installer-run-row">
<div className="module-installer-run-main">
<div className="module-installer-run-title">
<strong>{request.request_id}</strong>
<StatusBadge status={statusTone(request.status)} label={request.status} />
</div>
<div className="module-management-details">
<span>Created: {formatDateTime(request.created_at)}</span>
<span>Started: {formatDateTime(request.started_at)}</span>
<span>Finished: {formatDateTime(request.finished_at)}</span>
{request.cancelled_at && <span>Cancelled: {formatDateTime(request.cancelled_at)}</span>}
{request.retry_of && <span>Retry of: {request.retry_of}</span>}
<span>i18n:govoplan-admin.created.0c78dab1 {formatDateTime(request.created_at)}</span>
<span>i18n:govoplan-admin.started.fe3824e9 {formatDateTime(request.started_at)}</span>
<span>i18n:govoplan-admin.finished.4b52fe3f {formatDateTime(request.finished_at)}</span>
{request.cancelled_at && <span>i18n:govoplan-admin.cancelled.8c9dcfa8 {formatDateTime(request.cancelled_at)}</span>}
{request.retry_of && <span>i18n:govoplan-admin.retry_of.3a6bb304 {request.retry_of}</span>}
{traceId(request.trace) && <span>i18n:govoplan-admin.trace.04a75036 <code>{traceId(request.trace)}</code></span>}
</div>
{request.error && <p className="module-installer-run-error">{request.error}</p>}
</div>
<div className="module-installer-run-actions">
{request.status === "queued" && <Button onClick={() => void cancelInstallerRequest(request.request_id)} disabled={!canWrite || !canAccessMaintenance || !maintenanceEnabled || planBusy}>Cancel</Button>}
{(request.status === "failed" || request.status === "cancelled") && <Button onClick={() => void retryInstallerRequest(request.request_id)} disabled={!canWrite || !canAccessMaintenance || !maintenanceEnabled || planBusy}>Retry</Button>}
{request.status === "queued" && <Button onClick={() => void cancelInstallerRequest(request.request_id)} disabled={!canWrite || !canAccessMaintenance || !maintenanceEnabled || planBusy}>i18n:govoplan-admin.cancel.77dfd213</Button>}
{(request.status === "failed" || request.status === "cancelled") && <Button onClick={() => void retryInstallerRequest(request.request_id)} disabled={!canWrite || !canAccessMaintenance || !maintenanceEnabled || planBusy}>i18n:govoplan-admin.retry.9f5cd8a2</Button>}
{request.record_path && <code>{request.record_path}</code>}
</div>
</div>
))}
)}
</div>}
</div>}
{installerRuns && <div className="module-installer-runs">
<div className="module-installer-runs-heading">
<div>
<h2>Installer runs</h2>
<p>Recent supervised installer records and the current package-install lock.</p>
<h2>i18n:govoplan-admin.installer_runs.e9e344e1</h2>
<p>i18n:govoplan-admin.recent_supervised_installer_records_and_the_curr.29860245</p>
</div>
<StatusBadge status={installerRuns.lock.locked ? "warning" : "success"} label={installerRuns.lock.locked ? "Locked" : "Unlocked"} />
<StatusBadge status={installerRuns.lock.locked ? "warning" : "success"} label={installerRuns.lock.locked ? "i18n:govoplan-admin.locked.a798882f" : "i18n:govoplan-admin.unlocked.b3da7025"} />
</div>
{installerRuns.runs.length === 0 && <div className="module-install-plan-empty">No installer runs recorded yet.</div>}
{installerRuns.runs.length === 0 && <div className="module-install-plan-empty">i18n:govoplan-admin.no_installer_runs_recorded_yet.f8b61964</div>}
{installerRuns.runs.length > 0 && <div className="module-installer-run-list">
{installerRuns.runs.map((run) => (
<div key={run.run_id} className="module-installer-run-row">
{installerRuns.runs.map((run) =>
<div key={run.run_id} className="module-installer-run-row">
<div className="module-installer-run-main">
<div className="module-installer-run-title">
<strong>{run.run_id}</strong>
<StatusBadge status={statusTone(run.status)} label={run.status} />
{run.supervisor_status && <StatusBadge status={statusTone(run.supervisor_status)} label={`Supervisor: ${run.supervisor_status}`} />}
{run.rollback_status && <StatusBadge status={statusTone(run.rollback_status)} label={`Rollback: ${run.rollback_status}`} />}
{run.supervisor_status && <StatusBadge status={statusTone(run.supervisor_status)} label={i18nMessage("i18n:govoplan-admin.supervisor_value.d9f0c8eb", { value0: run.supervisor_status })} />}
{run.rollback_status && <StatusBadge status={statusTone(run.rollback_status)} label={i18nMessage("i18n:govoplan-admin.rollback_value.47e9c00b", { value0: run.rollback_status })} />}
</div>
<div className="module-management-details">
<span>Modules: {run.planned_modules.length ? run.planned_modules.join(", ") : "none"}</span>
<span>Commands: {run.commands_count}</span>
<span>Started: {formatDateTime(run.started_at)}</span>
<span>Finished: {formatDateTime(run.finished_at)}</span>
<span>i18n:govoplan-admin.modules.2dc3b299 {run.planned_modules.length ? run.planned_modules.join(", ") : "none"}</span>
{run.request_id && <span>i18n:govoplan-admin.request.43c2e7a7 {run.request_id}</span>}
{traceId(run.trace) && <span>i18n:govoplan-admin.trace.04a75036 <code>{traceId(run.trace)}</code></span>}
<span>i18n:govoplan-admin.commands.f6a34aed {run.commands_count}</span>
<span>i18n:govoplan-admin.started.fe3824e9 {formatDateTime(run.started_at)}</span>
<span>i18n:govoplan-admin.finished.4b52fe3f {formatDateTime(run.finished_at)}</span>
</div>
{run.error && <p className="module-installer-run-error">{run.error}</p>}
</div>
<code>{run.record_path}</code>
</div>
))}
)}
</div>}
</div>}
</>}
</AdminPageLayout>
);
</AdminPageLayout>);
}
function ModuleMetric({ label, value, detail, tone = "info" }: { label: string; value: string | number; detail: string; tone?: "info" | "good" | "warning" }) {
function ModuleMetric({ label, value, detail, tone = "info" }: {label: string;value: string | number;detail: string;tone?: "info" | "good" | "warning";}) {
return <div className={`metric-card metric-${tone}`}><div className="metric-label">{label}</div><div className="metric-value">{value}</div><div className="metric-detail">{detail}</div></div>;
}
function ModuleStatus({ module, desiredEnabled }: { module: ModuleCatalogItem; desiredEnabled: boolean }) {
if (module.current_enabled && !desiredEnabled) return <StatusBadge status="warning" label="Will disable" />;
if (!module.current_enabled && desiredEnabled) return <StatusBadge status="warning" label="Will enable" />;
if (module.current_enabled) return <StatusBadge status="success" label="Active" />;
return <StatusBadge status="inactive" label="Inactive" />;
function LicenseStatus({ license }: {license: ModuleLicenseDiagnostics;}) {
const label = license.license_id ? `${license.license_id}${license.subject ? ` · ${license.subject}` : ""}` : license.configured ? "i18n:govoplan-admin.configured_license.3e73f8f5" : "i18n:govoplan-admin.no_license_configured.693cea1a";
return (
<div className="module-license-status">
<div className="button-row compact-actions">
<StatusBadge status={licenseTone(license)} label={licenseLabel(license)} />
<StatusBadge status={license.enforced ? "warning" : "inactive"} label={license.enforced ? "i18n:govoplan-admin.enforced.93ff909c" : "i18n:govoplan-admin.observe_only.c6a1f8bc"} />
{license.key_id && <StatusBadge status="inactive" label={i18nMessage("i18n:govoplan-admin.key_value.847b914f", { value0: license.key_id })} />}
</div>
<p className="module-package-catalog-description">{label}</p>
<div className="module-management-details">
<span>i18n:govoplan-admin.valid.b374b8f9 {formatDateTime(license.valid_from)} to {formatDateTime(license.valid_until)}</span>
<span>i18n:govoplan-admin.features.5df81ffa {license.features.length ? license.features.join(", ") : "none"}</span>
{license.required_features.length > 0 && <span>i18n:govoplan-admin.required.d5793988 {license.required_features.join(", ")}</span>}
{license.missing_features.length > 0 && <span>i18n:govoplan-admin.missing.feb2bbaa {license.missing_features.join(", ")}</span>}
</div>
{license.reason && <p className={`module-package-catalog-description${license.allowed ? "" : " alert warning"}`}>{license.reason}</p>}
{license.guidance.map((item) => <p key={item} className="module-package-catalog-description">{item}</p>)}
</div>);
}
function ModuleStatus({ module, desiredEnabled }: {module: ModuleCatalogItem;desiredEnabled: boolean;}) {
if (module.current_enabled && !desiredEnabled) return <StatusBadge status="warning" label="i18n:govoplan-admin.will_disable.14bb193a" />;
if (!module.current_enabled && desiredEnabled) return <StatusBadge status="warning" label="i18n:govoplan-admin.will_enable.5d52d70b" />;
if (module.current_enabled) return <StatusBadge status="success" label="i18n:govoplan-admin.active.a733b809" />;
return <StatusBadge status="inactive" label="i18n:govoplan-admin.inactive.09af574c" />;
}
function sameSet(left: ReadonlySet<string>, right: ReadonlySet<string>) {
@@ -535,19 +651,19 @@ function emptyPlanItem(): ModuleInstallPlanItem {
}
function normalizePlanItems(items: ModuleInstallPlanItem[]): ModuleInstallPlanItem[] {
return items
.map((item) => ({
module_id: item.module_id.trim(),
action: item.action,
python_package: cleanOptional(item.python_package),
python_ref: item.action === "install" ? cleanOptional(item.python_ref) : null,
webui_package: cleanOptional(item.webui_package),
webui_ref: item.action === "install" ? cleanOptional(item.webui_ref) : null,
destroy_data: item.action === "uninstall" && Boolean(item.destroy_data),
status: item.status,
notes: cleanOptional(item.notes)
}))
.filter((item) => item.module_id || item.python_package || item.python_ref || item.webui_package || item.webui_ref || item.notes);
return items.
map((item) => ({
module_id: item.module_id.trim(),
action: item.action,
python_package: cleanOptional(item.python_package),
python_ref: item.action === "install" ? cleanOptional(item.python_ref) : null,
webui_package: cleanOptional(item.webui_package),
webui_ref: item.action === "install" ? cleanOptional(item.webui_ref) : null,
destroy_data: item.action === "uninstall" && Boolean(item.destroy_data),
status: item.status,
notes: cleanOptional(item.notes)
})).
filter((item) => item.module_id || item.python_package || item.python_ref || item.webui_package || item.webui_ref || item.notes);
}
function cleanOptional(value?: string | null): string | null {
@@ -557,12 +673,12 @@ function cleanOptional(value?: string | null): string | null {
function planValidationError(items: ModuleInstallPlanItem[]): string {
for (const item of normalizePlanItems(items)) {
if (!item.module_id) return "Every plan item needs a module id.";
if (item.action === "install" && !item.python_ref) return `${item.module_id} needs a Python package reference.`;
if (item.action === "install" && item.python_ref && !item.python_package) return `${item.module_id} needs a Python package name for rollback.`;
if (item.action === "uninstall" && !item.python_package) return `${item.module_id} needs a Python package name for uninstall.`;
if (item.action === "install" && Boolean(item.webui_package) !== Boolean(item.webui_ref)) return `${item.module_id} needs both WebUI package and WebUI reference, or neither.`;
if (item.action === "uninstall" && item.webui_ref && !item.webui_package) return `${item.module_id} has a WebUI reference but no WebUI package.`;
if (!item.module_id) return "i18n:govoplan-admin.every_plan_item_needs_a_module_id.23a7f206";
if (item.action === "install" && !item.python_ref) return i18nMessage("i18n:govoplan-admin.value_needs_a_python_package_reference.950b898e", { value0: item.module_id });
if (item.action === "install" && item.python_ref && !item.python_package) return i18nMessage("i18n:govoplan-admin.value_needs_a_python_package_name_for_rollback.6c89ed75", { value0: item.module_id });
if (item.action === "uninstall" && !item.python_package) return i18nMessage("i18n:govoplan-admin.value_needs_a_python_package_name_for_uninstall.004ba6fa", { value0: item.module_id });
if (item.action === "install" && Boolean(item.webui_package) !== Boolean(item.webui_ref)) return i18nMessage("i18n:govoplan-admin.value_needs_both_webui_package_and_webui_referen.d2bab073", { value0: item.module_id });
if (item.action === "uninstall" && item.webui_ref && !item.webui_package) return i18nMessage("i18n:govoplan-admin.value_has_a_webui_reference_but_no_webui_package.18f44149", { value0: item.module_id });
}
return "";
}
@@ -575,20 +691,49 @@ function statusTone(status: string): string {
return "inactive";
}
function licenseTone(license: ModuleLicenseDiagnostics): string {
if (!license.valid || !license.allowed || license.missing_features.length > 0) return "error";
if (!license.configured || !license.trusted || license.expires_in_days !== null && license.expires_in_days !== undefined && license.expires_in_days <= 30) return "warning";
return "success";
}
function licenseLabel(license: ModuleLicenseDiagnostics): string {
if (!license.configured) return "i18n:govoplan-admin.no_license.eb4bfe19";
if (!license.valid) return "i18n:govoplan-admin.invalid_license.f16bf749";
if (license.missing_features.length > 0) return "i18n:govoplan-admin.missing_entitlement.0cafa611";
if (license.trusted) return "i18n:govoplan-admin.valid_and_trusted.73cf89bc";
if (license.signed) return "i18n:govoplan-admin.signed_untrusted.864ed50c";
return "i18n:govoplan-admin.unsigned.e91344ea";
}
function formatDateTime(value?: string | null): string {
if (!value) return "not recorded";
if (!value) return "i18n:govoplan-admin.not_recorded.9925ee3c";
const date = new Date(value);
if (Number.isNaN(date.getTime())) return value;
return date.toLocaleString();
}
function traceId(value?: Record<string, unknown> | null): string {
const correlationId = value?.correlation_id;
return typeof correlationId === "string" && correlationId.trim() ? correlationId : "";
}
function installerCommand(frontendRebuildRequired: boolean): string {
const buildFlag = frontendRebuildRequired ? " --build-webui" : "";
return [
"govoplan-module-installer --format shell",
`govoplan-module-installer --daemon --health-url http://127.0.0.1:8000/health --restart-command '<restart govoplan server>'`,
`govoplan-module-installer --enqueue-supervised --migrate${buildFlag} --health-url http://127.0.0.1:8000/health --restart-command '<restart govoplan server>'`
].join("\n");
"govoplan-module-installer --format shell",
`govoplan-module-installer --daemon --health-url http://127.0.0.1:8000/health --restart-command '<restart govoplan server>'`,
`govoplan-module-installer --enqueue-supervised --migrate${buildFlag} --health-url http://127.0.0.1:8000/health --restart-command '<restart govoplan server>'`].
join("\n");
}
type ModuleStateRequestState = {
request: ConfigurationChangeRequest;
payloadKey: string;
};
function moduleStatePayloadKey(enabledModules: string[]): string {
return JSON.stringify(enabledModules);
}
type InstallerRequestFormOptions = {

View File

@@ -4,8 +4,10 @@ import { Button } from "@govoplan/core-webui";
import { Card } from "@govoplan/core-webui";
import { FormField } from "@govoplan/core-webui";
import { ToggleSwitch } from "@govoplan/core-webui";
import { fetchSystemSettings, updateSystemSettings, type PrivacyRetentionLimitPermissions, type PrivacyRetentionPolicy, type SystemSettingsItem } from "../../api/admin";
import { AdminPageLayout, adminErrorMessage } from "@govoplan/core-webui";
import { fetchSystemSettingsDelta, updateSystemSettings, type LanguagePackage, type PrivacyRetentionLimitPermissions, type PrivacyRetentionPolicy, type SystemSettingsDeltaSections, type SystemSettingsItem } from "../../api/admin";
import { AdminPageLayout, adminErrorMessage, useDeltaWatermarks, useUnsavedDraftGuard } from "@govoplan/core-webui";
const DELTA_KEY = "admin:system-settings";
const defaultAllowLowerLevelLimits: PrivacyRetentionLimitPermissions = {
store_raw_campaign_json: true,
@@ -35,86 +37,220 @@ const fallback: SystemSettingsItem = {
allow_tenant_api_keys: true,
privacy_retention_policy: defaultPrivacyPolicy,
maintenance_mode: { enabled: false, message: "" },
available_languages: [
{ code: "en", label: "English", native_label: "English" },
{ code: "de", label: "German", native_label: "Deutsch" }
],
enabled_language_codes: ["en", "de"],
settings: {}
};
export default function SystemSettingsPanel({ settings, canWrite, canAccessMaintenance }: { settings: ApiSettings; canWrite: boolean; canAccessMaintenance: boolean }) {
export default function SystemSettingsPanel({ settings, canWrite, canAccessMaintenance }: {settings: ApiSettings;canWrite: boolean;canAccessMaintenance: boolean;}) {
const [draft, setDraft] = useState<SystemSettingsItem>(fallback);
const [savedDraft, setSavedDraft] = useState<SystemSettingsItem>(fallback);
const [loading, setLoading] = useState(true);
const [busy, setBusy] = useState(false);
const [error, setError] = useState("");
const [success, setSuccess] = useState("");
const [packageDraft, setPackageDraft] = useState({ code: "", label: "", nativeLabel: "" });
const { getDeltaWatermark, setDeltaWatermark, resetDeltaWatermark } = useDeltaWatermarks();
const defaultLocaleOptions = localeOptions(draft.default_locale, draft.enabled_language_codes);
const dirty = systemSettingsDraftKey(draft) !== systemSettingsDraftKey(savedDraft);
useUnsavedDraftGuard({
dirty,
onSave: save,
onDiscard: () => {
setDraft(savedDraft);
setPackageDraft({ code: "", label: "", nativeLabel: "" });
}
});
async function load() {
setLoading(true);
setError("");
try {
const loaded = await fetchSystemSettings(settings);
setDraft(loaded);
const wasDirty = systemSettingsDraftKey(draft) !== systemSettingsDraftKey(savedDraft);
const response = await fetchSystemSettingsDelta(settings, { since: getDeltaWatermark(DELTA_KEY) });
setDeltaWatermark(DELTA_KEY, response.watermark);
if (response.full && response.item) {
setSavedDraft(response.item);
if (!wasDirty) setDraft(response.item);
} else if (response.changed_sections.length) {
setSavedDraft((current) => applySystemSettingsSections(current, response.sections));
if (!wasDirty) setDraft((current) => applySystemSettingsSections(current, response.sections));
}
}
catch (err) { setError(adminErrorMessage(err)); }
finally { setLoading(false); }
catch (err) {setError(adminErrorMessage(err));} finally
{setLoading(false);}
}
useEffect(() => { void load(); }, [settings.accessToken, settings.apiBaseUrl]);
useEffect(() => {
resetDeltaWatermark(DELTA_KEY);
void load();
}, [settings.accessToken, settings.apiBaseUrl, resetDeltaWatermark]);
async function save() {
async function save(): Promise<boolean> {
setBusy(true);
setError("");
try {
setDraft(await updateSystemSettings(settings, {
const saved = await updateSystemSettings(settings, {
default_locale: draft.default_locale,
allow_tenant_custom_groups: draft.allow_tenant_custom_groups,
allow_tenant_custom_roles: draft.allow_tenant_custom_roles,
allow_tenant_api_keys: draft.allow_tenant_api_keys,
maintenance_mode: draft.maintenance_mode
}));
setSuccess("System settings saved.");
} catch (err) { setError(adminErrorMessage(err)); }
finally { setBusy(false); }
maintenance_mode: draft.maintenance_mode,
available_languages: draft.available_languages,
enabled_language_codes: draft.enabled_language_codes
});
setDraft(saved);
setSavedDraft(saved);
resetDeltaWatermark(DELTA_KEY);
setSuccess("i18n:govoplan-admin.system_settings_saved.dac4f17b");
return true;
} catch (err) {setError(adminErrorMessage(err));return false;} finally
{setBusy(false);}
}
function toggleLanguage(code: string, checked: boolean) {
const enabled = new Set(draft.enabled_language_codes);
if (checked) enabled.add(code);
else enabled.delete(code);
const nextEnabled = draft.available_languages.map((item) => item.code).filter((item) => enabled.has(item));
const defaultLocale = nextEnabled.includes(draft.default_locale) ? draft.default_locale : (nextEnabled[0] ?? draft.default_locale);
setDraft({ ...draft, enabled_language_codes: nextEnabled, default_locale: defaultLocale });
}
function installLanguagePackage() {
const code = normalizeLanguageCode(packageDraft.code);
if (!code || !packageDraft.label.trim()) return;
const nextPackage: LanguagePackage = {
code,
label: packageDraft.label.trim(),
native_label: packageDraft.nativeLabel.trim() || packageDraft.label.trim()
};
const existing = new Set(draft.available_languages.map((item) => item.code));
const available = existing.has(code)
? draft.available_languages.map((item) => item.code === code ? nextPackage : item)
: [...draft.available_languages, nextPackage];
const enabled = draft.enabled_language_codes.includes(code) ? draft.enabled_language_codes : [...draft.enabled_language_codes, code];
setDraft({ ...draft, available_languages: available, enabled_language_codes: enabled, default_locale: draft.default_locale || code });
setPackageDraft({ code: "", label: "", nativeLabel: "" });
}
return (
<AdminPageLayout
title="System general settings"
description="Instance-wide defaults and tenant governance capabilities. Retention policy management has its own system section."
title="i18n:govoplan-admin.system_general_settings.7cd662ed"
description="i18n:govoplan-admin.instance_wide_defaults_and_tenant_governance_cap.096a4f97"
loading={loading}
error={error}
success={success}
actions={<><Button onClick={() => void load()} disabled={loading}>Reload</Button><Button variant="primary" onClick={() => void save()} disabled={!canWrite || busy}>{busy ? "Saving…" : "Save settings"}</Button></>}
>
actions={<><Button onClick={() => void load()} disabled={loading}>i18n:govoplan-admin.reload.cce71553</Button><Button variant="primary" onClick={() => void save()} disabled={!canWrite || busy}>{busy ? "i18n:govoplan-admin.saving.56a2285c" : "i18n:govoplan-admin.save_settings.913aba9f"}</Button></>}>
<div className="admin-settings-form">
<Card title="Defaults for newly created tenants">
<FormField label="Default locale"><input value={draft.default_locale} onChange={(event) => setDraft({ ...draft, default_locale: event.target.value })} /></FormField>
<Card title="i18n:govoplan-admin.defaults_for_newly_created_tenants.9d0f4ccd">
<FormField label="i18n:govoplan-admin.default_locale.b99d021f">
<select value={draft.default_locale} onChange={(event) => setDraft({ ...draft, default_locale: event.target.value })} disabled={!canWrite || busy || defaultLocaleOptions.length === 0}>
{defaultLocaleOptions.map((code) => {
const language = draft.available_languages.find((item) => item.code === code);
return <option key={code} value={code}>{languageOptionLabel(language ?? { code, label: code.toUpperCase() })}</option>;
})}
</select>
</FormField>
</Card>
<Card title="Tenant administration capabilities">
<Card title="i18n:govoplan-admin.language_packages">
<div className="settings-list">
<ToggleSwitch checked={draft.allow_tenant_custom_groups} onChange={(checked) => setDraft({ ...draft, allow_tenant_custom_groups: checked })} label="Allow tenant-defined groups by default" />
<ToggleSwitch checked={draft.allow_tenant_custom_roles} onChange={(checked) => setDraft({ ...draft, allow_tenant_custom_roles: checked })} label="Allow tenant-defined roles by default" />
<ToggleSwitch checked={draft.allow_tenant_api_keys} onChange={(checked) => setDraft({ ...draft, allow_tenant_api_keys: checked })} label="Allow tenant API keys by default" />
{draft.available_languages.map((item) => (
<label className="admin-inline-check" key={item.code}>
<input
type="checkbox"
checked={draft.enabled_language_codes.includes(item.code)}
disabled={!canWrite || busy || item.code === draft.default_locale}
onChange={(event) => toggleLanguage(item.code, event.target.checked)} />
<span><strong>{item.code.toUpperCase()}</strong> {languageOptionLabel(item)}</span>
</label>
))}
</div>
<p className="muted small-note">These settings are enforced by the backend. Central groups and tenant roles remain available even when local creation is disabled.</p>
<div className="form-grid">
<FormField label="i18n:govoplan-admin.language_code">
<input value={packageDraft.code} disabled={!canWrite || busy} placeholder="fr" onChange={(event) => setPackageDraft({ ...packageDraft, code: event.target.value })} />
</FormField>
<FormField label="i18n:govoplan-admin.language_name">
<input value={packageDraft.label} disabled={!canWrite || busy} placeholder="i18n:govoplan-admin.language_name_placeholder" onChange={(event) => setPackageDraft({ ...packageDraft, label: event.target.value })} />
</FormField>
<FormField label="i18n:govoplan-admin.native_language_name">
<input value={packageDraft.nativeLabel} disabled={!canWrite || busy} placeholder="i18n:govoplan-admin.native_language_name_placeholder" onChange={(event) => setPackageDraft({ ...packageDraft, nativeLabel: event.target.value })} />
</FormField>
<div className="button-row compact-actions">
<Button onClick={installLanguagePackage} disabled={!canWrite || busy || !normalizeLanguageCode(packageDraft.code) || !packageDraft.label.trim()}>i18n:govoplan-admin.install_language_package</Button>
</div>
</div>
<p className="muted small-note">i18n:govoplan-admin.language_packages_help</p>
</Card>
<Card title="Maintenance mode">
<Card title="i18n:govoplan-admin.tenant_administration_capabilities.5d265972">
<div className="settings-list">
<ToggleSwitch checked={draft.allow_tenant_custom_groups} onChange={(checked) => setDraft({ ...draft, allow_tenant_custom_groups: checked })} label="i18n:govoplan-admin.allow_tenant_defined_groups_by_default.32099d5a" />
<ToggleSwitch checked={draft.allow_tenant_custom_roles} onChange={(checked) => setDraft({ ...draft, allow_tenant_custom_roles: checked })} label="i18n:govoplan-admin.allow_tenant_defined_roles_by_default.b1f79ee9" />
<ToggleSwitch checked={draft.allow_tenant_api_keys} onChange={(checked) => setDraft({ ...draft, allow_tenant_api_keys: checked })} label="i18n:govoplan-admin.allow_tenant_api_keys_by_default.58a6cf17" />
</div>
<p className="muted small-note">i18n:govoplan-admin.these_settings_are_enforced_by_the_backend_centr.8112ed6f</p>
</Card>
<Card title="i18n:govoplan-admin.maintenance_mode.98cca5c6">
<div className="settings-list">
<ToggleSwitch
checked={draft.maintenance_mode.enabled}
disabled={!canWrite || !canAccessMaintenance}
onChange={(checked) => setDraft({ ...draft, maintenance_mode: { ...draft.maintenance_mode, enabled: checked } })}
label="Restrict authenticated API access to maintenance operators"
/>
label="i18n:govoplan-admin.restrict_authenticated_api_access_to_maintenance.2bc47195" />
</div>
<FormField label="Maintenance message">
<FormField label="i18n:govoplan-admin.maintenance_message.ca62571f">
<textarea
rows={3}
value={draft.maintenance_mode.message ?? ""}
disabled={!canWrite}
onChange={(event) => setDraft({ ...draft, maintenance_mode: { ...draft.maintenance_mode, message: event.target.value } })}
/>
onChange={(event) => setDraft({ ...draft, maintenance_mode: { ...draft.maintenance_mode, message: event.target.value } })} />
</FormField>
<p className="muted small-note">Changing the maintenance-mode flag requires system:maintenance:access. Login remains available so an operator can sign in during maintenance.</p>
<p className="muted small-note">i18n:govoplan-admin.changing_the_maintenance_mode_flag_requires_syst.4492050f</p>
</Card>
</div>
</AdminPageLayout>
);
</AdminPageLayout>);
}
function normalizeLanguageCode(value: string): string {
return value.trim().toLowerCase().replace(/_/g, "-").split(/[^a-z0-9]+/).filter(Boolean).join("-");
}
function localeOptions(current: string, enabled: string[]): string[] {
return [...new Set([current, ...enabled].filter((item) => item && item.trim()))];
}
function languageOptionLabel(language: LanguagePackage): string {
return `${language.code.toUpperCase()} - ${language.native_label || language.label}`;
}
function systemSettingsDraftKey(item: SystemSettingsItem): string {
return JSON.stringify({
default_locale: item.default_locale,
allow_tenant_custom_groups: item.allow_tenant_custom_groups,
allow_tenant_custom_roles: item.allow_tenant_custom_roles,
allow_tenant_api_keys: item.allow_tenant_api_keys,
maintenance_mode: item.maintenance_mode,
available_languages: item.available_languages,
enabled_language_codes: item.enabled_language_codes
});
}
function applySystemSettingsSections(item: SystemSettingsItem, sections: SystemSettingsDeltaSections): SystemSettingsItem {
return {
...item,
...(sections.defaults ?? {}),
...(sections.tenant_capabilities ?? {}),
...(sections.languages ?? {}),
...(sections.privacy_retention_policy ? { privacy_retention_policy: sections.privacy_retention_policy } : {}),
...(sections.maintenance_mode ? { maintenance_mode: sections.maintenance_mode } : {}),
...(sections.settings ? { settings: sections.settings } : {})
};
}

View File

@@ -0,0 +1,710 @@
import type { PlatformTranslations } from "@govoplan/core-webui";
export const generatedTranslations: PlatformTranslations = {
"en": {
"i18n:govoplan-admin.access_configuration_exported.098f200d": "Access configuration exported.",
"i18n:govoplan-admin.action.97c89a4d": "Action",
"i18n:govoplan-admin.actions.c3cd636a": "Actions",
"i18n:govoplan-admin.activate_installs.731d22a1": "Activate installs",
"i18n:govoplan-admin.active_and_total_memberships.c0c20f10": "Active and total memberships.",
"i18n:govoplan-admin.active_tenant_automation_credentials.240c659e": "Active tenant automation credentials.",
"i18n:govoplan-admin.active_tenant.dfadf0aa": "Active tenant:",
"i18n:govoplan-admin.active.a733b809": "Active",
"i18n:govoplan-admin.add_group_template.b74d8f0f": "Add group template",
"i18n:govoplan-admin.add_plan_item.fa55f028": "Add plan item",
"i18n:govoplan-admin.add_tenant_role.fcd904ea": "Add tenant role",
"i18n:govoplan-admin.admin.4e7afebc": "Admin",
"i18n:govoplan-admin.administration.b8be3d12": "Administration",
"i18n:govoplan-admin.platform_administration": "Platform administration",
"i18n:govoplan-admin.alert_warning.804f617a": " alert warning",
"i18n:govoplan-admin.allow_tenant_api_keys_by_default.58a6cf17": "Allow tenant API keys by default",
"i18n:govoplan-admin.allow_tenant_defined_groups_by_default.32099d5a": "Allow tenant-defined groups by default",
"i18n:govoplan-admin.allow_tenant_defined_roles_by_default.b1f79ee9": "Allow tenant-defined roles by default",
"i18n:govoplan-admin.api_keys.94fcf3c2": "API keys",
"i18n:govoplan-admin.applied.a3e4a569": "Applied",
"i18n:govoplan-admin.apply_approved_request.ab218d9a": "Apply approved request",
"i18n:govoplan-admin.apply_finished_with_blockers.78487a12": "Apply finished with blockers.",
"i18n:govoplan-admin.apply_result.0fde1c3c": "Apply result",
"i18n:govoplan-admin.apply.cfea419c": "Apply",
"i18n:govoplan-admin.approvals.deb9d03c": "Approvals",
"i18n:govoplan-admin.approve.7b2c7f14": "Approve",
"i18n:govoplan-admin.approved_package_references_that_can_be_added_to.cc911c16": "Approved package references that can be added to the operator install plan.",
"i18n:govoplan-admin.approved_value.52da808b": "Approved {value0}.",
"i18n:govoplan-admin.approvers.0e2de1fb": "Approvers",
"i18n:govoplan-admin.audit.fa1703dd": "Audit",
"i18n:govoplan-admin.available.7c62a142": "Available",
"i18n:govoplan-admin.blocked.99613c74": "Blocked",
"i18n:govoplan-admin.build_webui.ed5ef1fd": " --build-webui",
"i18n:govoplan-admin.build_webui.fe8ccad7": "Build WebUI",
"i18n:govoplan-admin.bypass_allowed.4e347c27": "Bypass allowed",
"i18n:govoplan-admin.bypass_denied.ab987400": "Bypass denied",
"i18n:govoplan-admin.cached_from.d73c5b2a": "· cached from",
"i18n:govoplan-admin.cancel.77dfd213": "Cancel",
"i18n:govoplan-admin.cancelled.8c9dcfa8": "Cancelled:",
"i18n:govoplan-admin.case_clerk.b78a314a": "Case clerk",
"i18n:govoplan-admin.catalog_install_entry_planned.13b23f4f": "Catalog install entry planned.",
"i18n:govoplan-admin.catalog.4a88d27b": "Catalog",
"i18n:govoplan-admin.central_groups.5c9b5b66": "Central groups",
"i18n:govoplan-admin.centrally_defined_group_identities_that_are_prov.7761fce9": "Centrally defined group identities that are provisioned into selected tenants as available or required definitions. Membership remains tenant-local.",
"i18n:govoplan-admin.centrally_governed_tenant_roles_and_their_availa.d879d5d1": "Centrally governed tenant roles and their availability across tenants.",
"i18n:govoplan-admin.centrally_governed_tenant_roles_that_are_provisi.8739fca3": "Centrally governed tenant roles that are provisioned into selected tenants as available or required definitions.",
"i18n:govoplan-admin.centrally_governed_tenant_roles.797c689b": "Centrally governed tenant roles.",
"i18n:govoplan-admin.change_request_created_value.4af6d3d2": "Change request created: {value0}",
"i18n:govoplan-admin.change_request_id.96ee3239": "Change request id",
"i18n:govoplan-admin.change_request_ready.b17e8c57": "Change request ready:",
"i18n:govoplan-admin.changes.8aa57de6": "Changes",
"i18n:govoplan-admin.changing_enabled_modules_requires_a_dry_run_chan.10b6f5ed": "Changing enabled modules requires a dry-run change request and active maintenance mode.",
"i18n:govoplan-admin.changing_the_maintenance_mode_flag_requires_syst.4492050f": "Changing the maintenance-mode flag requires system:maintenance:access. Login remains available so an operator can sign in during maintenance.",
"i18n:govoplan-admin.channel_value.d3894efe": "Channel: {value0}",
"i18n:govoplan-admin.clear.719ea396": "Clear",
"i18n:govoplan-admin.close.bbfa773e": "Close",
"i18n:govoplan-admin.code.adac6937": "Code",
"i18n:govoplan-admin.commands.f6a34aed": "Commands:",
"i18n:govoplan-admin.configuration_changes.82933bbb": "Configuration changes",
"i18n:govoplan-admin.configuration_package_apply.c270e5f3": "Configuration package apply",
"i18n:govoplan-admin.configuration_packages.eb2f05f1": "Configuration packages",
"i18n:govoplan-admin.configuration_requests_approvals_and_version_his.19f37335": "Configuration requests, approvals and version history.",
"i18n:govoplan-admin.configured_license.3e73f8f5": "Configured license",
"i18n:govoplan-admin.create_dry_run_request.05b9cbdb": "Create dry-run request",
"i18n:govoplan-admin.create_group_template.72407248": "Create group template",
"i18n:govoplan-admin.create_request_and_apply.da79e153": "Create request and apply",
"i18n:govoplan-admin.create_suspend_and_govern_tenant_spaces.77992a39": "Create, suspend and govern tenant spaces.",
"i18n:govoplan-admin.create_tenant_role.f58db104": "Create tenant role",
"i18n:govoplan-admin.created.0c78dab1": "Created:",
"i18n:govoplan-admin.created.accf40c8": "Created",
"i18n:govoplan-admin.daemon_execution.cc0fad8d": "Daemon execution",
"i18n:govoplan-admin.daemon_offline.314f19a4": "Daemon offline",
"i18n:govoplan-admin.daemon_value.e54f5a31": "Daemon: {value0}",
"i18n:govoplan-admin.db_backup_command.5bded3ac": "DB backup command",
"i18n:govoplan-admin.db_restore_check_command.750ae936": "DB restore-check command",
"i18n:govoplan-admin.db_restore_command.de11eb01": "DB restore command",
"i18n:govoplan-admin.db.e355c23a": "DB:",
"i18n:govoplan-admin.default_locale.b99d021f": "Default locale",
"i18n:govoplan-admin.install_language_package": "Install language package",
"i18n:govoplan-admin.language_code": "Language code",
"i18n:govoplan-admin.language_name": "Language name",
"i18n:govoplan-admin.language_name_placeholder": "French",
"i18n:govoplan-admin.language_packages": "Language packages",
"i18n:govoplan-admin.language_packages_help": "Installed packages define which language codes can be enabled. A package only becomes fully translated when matching module translation catalogs are present.",
"i18n:govoplan-admin.native_language_name": "Native language name",
"i18n:govoplan-admin.native_language_name_placeholder": "Francais",
"i18n:govoplan-admin.defaults_for_newly_created_tenants.9d0f4ccd": "Defaults for newly created tenants",
"i18n:govoplan-admin.delete_group_template.8745d842": "Delete group template",
"i18n:govoplan-admin.delete_template.399bf72a": "Delete template",
"i18n:govoplan-admin.delete_tenant_role.4efa0813": "Delete tenant role",
"i18n:govoplan-admin.delete_value_removal_is_blocked_while_a_material.d4eba73d": "Delete {value0}? Removal is blocked while a materialized tenant definition still has members or assignments.",
"i18n:govoplan-admin.delete_value.4d18989e": "Delete {value0}",
"i18n:govoplan-admin.dependents.072665c4": "Dependents:",
"i18n:govoplan-admin.description.55f8ebc8": "Description",
"i18n:govoplan-admin.destroy_data.34557c3c": "Destroy data",
"i18n:govoplan-admin.disabled.f4f4473d": "Disabled",
"i18n:govoplan-admin.discovered_packages.660902de": "Discovered packages",
"i18n:govoplan-admin.drift.4876f7b9": "Drift",
"i18n:govoplan-admin.drop_uninstalls_from_startup.0e2a5c5b": "Drop uninstalls from startup",
"i18n:govoplan-admin.dry_run_change_request_created_value_enable_main.49826d07": "Dry-run change request created: {value0}. Enable maintenance mode, then apply the request.",
"i18n:govoplan-admin.dry_run.3d14659c": "Dry-run",
"i18n:govoplan-admin.edit_group_template.9bc72d21": "Edit group template",
"i18n:govoplan-admin.edit_tenant_role.c15a260d": "Edit tenant role",
"i18n:govoplan-admin.edit_value.fad75899": "Edit {value0}",
"i18n:govoplan-admin.enable_maintenance_mode.75f98d57": "Enable maintenance mode",
"i18n:govoplan-admin.enabled.df174a3f": "Enabled",
"i18n:govoplan-admin.enabling_maintenance_mode_requires_system_mainte.062d3968": "Enabling maintenance mode requires system:maintenance:access.",
"i18n:govoplan-admin.enabling.2b8e03e6": "Enabling...",
"i18n:govoplan-admin.enforced.93ff909c": "Enforced",
"i18n:govoplan-admin.every_plan_item_needs_a_module_id.23a7f206": "Every plan item needs a module id.",
"i18n:govoplan-admin.expires.2d21b7de": "· Expires:",
"i18n:govoplan-admin.export_access.9a2eb91e": "Export access",
"i18n:govoplan-admin.export_finished_with_blockers.e2f70611": "Export finished with blockers.",
"i18n:govoplan-admin.export.f3e4fadb": "Export",
"i18n:govoplan-admin.exported_system_access_configuration.2e1c6f4c": "Exported system access configuration",
"i18n:govoplan-admin.exported_tenant_access_configuration.9122c85c": "Exported tenant access configuration",
"i18n:govoplan-admin.features.5df81ffa": "Features:",
"i18n:govoplan-admin.file_connections.1e362326": "File connections",
"i18n:govoplan-admin.finished.4b52fe3f": "Finished:",
"i18n:govoplan-admin.fragment.3f19d616": "Fragment",
"i18n:govoplan-admin.front_office.d9dcfee1": "Front office",
"i18n:govoplan-admin.general.9239ee2c": "General",
"i18n:govoplan-admin.generated.a2edf57c": "Generated:",
"i18n:govoplan-admin.global": "Global",
"i18n:govoplan-admin.global_accounts_tenant_memberships_and_system_ro.939ed01f": "Global accounts, tenant memberships and system-role assignments.",
"i18n:govoplan-admin.global_login_accounts_across_all_tenants.2aab129d": "Global login accounts across all tenants.",
"i18n:govoplan-admin.govoplan_module_installer_format_shell.226d1450": "govoplan-module-installer --format shell",
"i18n:govoplan-admin.group_definitions_made_available_or_required_in_.55402e16": "Group definitions made available or required in selected tenants.",
"i18n:govoplan-admin.group_file_connector_policy_limits": "File connector policy limits for group-owned spaces.",
"i18n:govoplan-admin.group_mail_server_policy_limits": "Mail server policy limits for group-owned work.",
"i18n:govoplan-admin.group_retention_policy_limits": "Retention limits for group-owned records.",
"i18n:govoplan-admin.group_template.973e0fa6": "Group template",
"i18n:govoplan-admin.group.171a0606": "Group",
"i18n:govoplan-admin.groups.ae9629f4": "Groups",
"i18n:govoplan-admin.handles_incoming_administrative_work.dc80c349": "Handles incoming administrative work.",
"i18n:govoplan-admin.health_urls.3b86647c": "Health URLs",
"i18n:govoplan-admin.history.90ccd649": "History",
"i18n:govoplan-admin.id.474ae526": "Id",
"i18n:govoplan-admin.import_preflight_approve_apply_and_export_module.29aec929": "Import, preflight, approve, apply and export module-owned configuration packages.",
"i18n:govoplan-admin.inactive.09af574c": "Inactive",
"i18n:govoplan-admin.inspect_value.9d5d1071": "Inspect {value0}",
"i18n:govoplan-admin.install.fd6c3ebf": "Install",
"i18n:govoplan-admin.installed_modules_active_runtime_state_and_saved.f33907dd": "Installed modules, active runtime state, and saved startup state.",
"i18n:govoplan-admin.installed_modules_runtime_state_and_startup_stat.38dd7028": "Installed modules, runtime state and startup state.",
"i18n:govoplan-admin.installed.7bb4405c": "Installed",
"i18n:govoplan-admin.installer_preflight_blocked.cf83bf79": "Installer preflight blocked",
"i18n:govoplan-admin.installer_preflight_passed.6d5f8060": "Installer preflight passed",
"i18n:govoplan-admin.installer_request_cancelled_value.1d6d099d": "Installer request cancelled: {value0}",
"i18n:govoplan-admin.installer_request_queued_value.00726abb": "Installer request queued: {value0}",
"i18n:govoplan-admin.installer_requests.b88db439": "Installer requests",
"i18n:govoplan-admin.installer_runs.e9e344e1": "Installer runs",
"i18n:govoplan-admin.instance_defaults_and_tenant_governance_capabili.99d6b2fa": "Instance defaults and tenant governance capabilities.",
"i18n:govoplan-admin.instance_privacy_retention_policy_and_lower_leve.b8d14069": "Instance privacy retention policy and lower-level override permissions.",
"i18n:govoplan-admin.instance_wide_defaults_and_tenant_governance_cap.096a4f97": "Instance-wide defaults and tenant governance capabilities. Retention policy management has its own system section.",
"i18n:govoplan-admin.instance_wide_roles_assigned_directly_to_global_.91050488": "Instance-wide roles assigned directly to global accounts.",
"i18n:govoplan-admin.invalid_json.01ccb74f": "Invalid JSON.",
"i18n:govoplan-admin.invalid_license.f16bf749": "Invalid license",
"i18n:govoplan-admin.json_must_be_an_object.848569c9": "JSON must be an object.",
"i18n:govoplan-admin.key_value.847b914f": "Key: {value0}",
"i18n:govoplan-admin.key.c67dd20e": "Key",
"i18n:govoplan-admin.kind.e00ac23f": "Kind",
"i18n:govoplan-admin.label.74341e3c": "Label",
"i18n:govoplan-admin.last_request.4508ef35": "Last request:",
"i18n:govoplan-admin.license.de13bf1a": "License:",
"i18n:govoplan-admin.locked.a798882f": "Locked",
"i18n:govoplan-admin.mail_servers.d627326a": "Mail servers",
"i18n:govoplan-admin.maintenance_message.ca62571f": "Maintenance message",
"i18n:govoplan-admin.maintenance_mode_enabled_apply_the_module_state_.dda13e5c": "Maintenance mode enabled. Apply the module-state request when ready.",
"i18n:govoplan-admin.maintenance_mode_is_off_package_changes_should_b.1d262629": "Maintenance mode is off. Package changes should be applied only after enabling maintenance mode.",
"i18n:govoplan-admin.maintenance_mode_must_be_enabled_before_queueing.d233a32f": "Maintenance mode must be enabled before queueing a daemon request.",
"i18n:govoplan-admin.maintenance_mode.98cca5c6": "Maintenance mode",
"i18n:govoplan-admin.maintenance.94de303b": "Maintenance",
"i18n:govoplan-admin.membership_status_groups_and_direct_roles_in_the.ab6c10b6": "Membership status, groups and direct roles in the active tenant.",
"i18n:govoplan-admin.message.68f4145f": "Message",
"i18n:govoplan-admin.minimal_office_access.af48f49a": "Minimal office access",
"i18n:govoplan-admin.missing_entitlement.0cafa611": "Missing entitlement",
"i18n:govoplan-admin.missing.feb2bbaa": "Missing:",
"i18n:govoplan-admin.module_install_plan_cleared.afa2698d": "Module install plan cleared.",
"i18n:govoplan-admin.module_install_plan_saved.5d43d944": "Module install plan saved.",
"i18n:govoplan-admin.module_maintenance_in_progress.f6395152": "Module maintenance in progress.",
"i18n:govoplan-admin.module_state_saved.e3e22105": "Module state saved.",
"i18n:govoplan-admin.module.b8ff0289": "Module",
"i18n:govoplan-admin.modules.04e9462c": "Modules",
"i18n:govoplan-admin.modules.2dc3b299": "Modules:",
"i18n:govoplan-admin.name.709a2322": "Name",
"i18n:govoplan-admin.needs_attention.a126722e": "Needs attention",
"i18n:govoplan-admin.no_applied_configuration_changes.6a3ae4a7": "No applied configuration changes.",
"i18n:govoplan-admin.no_central_value_templates_found.081149fa": "No central {value0} templates found.",
"i18n:govoplan-admin.no_diagnostics.2b6e2630": "No diagnostics.",
"i18n:govoplan-admin.no_installer_requests_recorded_yet.17e630fa": "No installer requests recorded yet.",
"i18n:govoplan-admin.no_installer_runs_recorded_yet.f8b61964": "No installer runs recorded yet.",
"i18n:govoplan-admin.no_license_configured.693cea1a": "No license configured",
"i18n:govoplan-admin.no_license.eb4bfe19": "No license",
"i18n:govoplan-admin.no_open_requests.580c95f9": "No open requests.",
"i18n:govoplan-admin.no_package_catalog_configured_set_govoplan_modul.2c5fceb3": "No package catalog configured. Set GOVOPLAN_MODULE_PACKAGE_CATALOG or GOVOPLAN_MODULE_PACKAGE_CATALOG_URL to enable curated install entries.",
"i18n:govoplan-admin.no_package_changes_planned.f12d8b33": "No package changes planned.",
"i18n:govoplan-admin.no_package_restart_pending.efe6b8ce": "No package restart pending",
"i18n:govoplan-admin.no_plan_items.7108c582": "No plan items.",
"i18n:govoplan-admin.none.6eef6648": "None",
"i18n:govoplan-admin.not_available.d1a17af1": "Not available",
"i18n:govoplan-admin.not_configured.811931bb": "Not configured",
"i18n:govoplan-admin.not_recorded.9925ee3c": "not recorded",
"i18n:govoplan-admin.notes.70440046": "Notes",
"i18n:govoplan-admin.object.2883f191": "Object",
"i18n:govoplan-admin.observe_only.c6a1f8bc": "Observe-only",
"i18n:govoplan-admin.off.e3de5ab0": "Off",
"i18n:govoplan-admin.on.e0049a66": "On",
"i18n:govoplan-admin.operator_install_plan.b203aabc": "Operator install plan",
"i18n:govoplan-admin.optional.166b8369": "Optional:",
"i18n:govoplan-admin.overview.0efc2e6b": "Overview",
"i18n:govoplan-admin.owner.89ff3122": "Owner",
"i18n:govoplan-admin.package_applied.63782ce7": "Package applied.",
"i18n:govoplan-admin.package_catalog_is_configured_but_contains_no_en.b6b053cb": "Package catalog is configured but contains no entries.",
"i18n:govoplan-admin.package_catalog.b36e6d31": "Package catalog",
"i18n:govoplan-admin.package_json_must_be_an_object.db825bb3": "Package JSON must be an object.",
"i18n:govoplan-admin.package_json.a2b10f38": "Package JSON",
"i18n:govoplan-admin.package_uninstall_planned.56e8566d": "Package uninstall planned.",
"i18n:govoplan-admin.package.7431e3df": "Package",
"i18n:govoplan-admin.packages.0a999012": "Packages",
"i18n:govoplan-admin.pending.c515ec74": " pending",
"i18n:govoplan-admin.permissions.d06d5557": "Permissions",
"i18n:govoplan-admin.plan_install.e82bffe6": "Plan install",
"i18n:govoplan-admin.plan_package_installs_and_removals_here_then_app.4aeb03bf": "Plan package installs and removals here, then apply the rendered commands from an operator shell during maintenance mode.",
"i18n:govoplan-admin.plan_uninstall.e62804ab": "Plan uninstall",
"i18n:govoplan-admin.plan.ae2f98a0": "Plan",
"i18n:govoplan-admin.planned.9cbe42aa": "Planned",
"i18n:govoplan-admin.preflight_approve_apply_and_export_configuration.276bd0f8": "Preflight, approve, apply and export configuration packages.",
"i18n:govoplan-admin.preflight_finished_with_blockers.7e2ca12b": "Preflight finished with blockers.",
"i18n:govoplan-admin.preflight_passed.c0c99055": "Preflight passed.",
"i18n:govoplan-admin.preflight.8016a487": "Preflight",
"i18n:govoplan-admin.python_package.34591c71": "Python package",
"i18n:govoplan-admin.python_ref.1af35d0d": "Python ref",
"i18n:govoplan-admin.queue_supervised_run.a53f248c": "Queue supervised run",
"i18n:govoplan-admin.queue_the_saved_plan_for_a_separate_installer_da.92d4c96e": "Queue the saved plan for a separate installer daemon process.",
"i18n:govoplan-admin.queued_daemon_handoffs_created_from_the_admin_ui.d7ae7914": "Queued daemon handoffs created from the admin UI or CLI.",
"i18n:govoplan-admin.queueing_installer_requests_requires_maintenance.ae81f9ac": "Queueing installer requests requires maintenance access.",
"i18n:govoplan-admin.recent_supervised_installer_records_and_the_curr.29860245": "Recent supervised installer records and the current package-install lock.",
"i18n:govoplan-admin.registered_tenant_spaces.61e17d70": "Registered tenant spaces.",
"i18n:govoplan-admin.reload.cce71553": "Reload",
"i18n:govoplan-admin.remove.e963907d": "Remove",
"i18n:govoplan-admin.request.43c2e7a7": "Request:",
"i18n:govoplan-admin.request_approval.6245aea1": "Request approval",
"i18n:govoplan-admin.requested.c26bf60f": "Requested",
"i18n:govoplan-admin.requests.f7194e6a": "Requests",
"i18n:govoplan-admin.required_data.1b1c1b34": "Required data",
"i18n:govoplan-admin.required_means_the_definition_must_remain_presen.8462063c": "Required means the definition must remain present and system-controlled. It does not automatically assign users or grant permissions.",
"i18n:govoplan-admin.required.d5793988": "Required:",
"i18n:govoplan-admin.required.eed6bfb4": "Required",
"i18n:govoplan-admin.requires.a4fc9357": "Requires:",
"i18n:govoplan-admin.restart_commands.035997f9": "Restart commands",
"i18n:govoplan-admin.restart_reload_required_after_package_changes.3a99cd63": "Restart/reload required after package changes",
"i18n:govoplan-admin.restrict_authenticated_api_access_to_maintenance.2bc47195": "Restrict authenticated API access to maintenance operators",
"i18n:govoplan-admin.retention.c7199d9e": "Retention",
"i18n:govoplan-admin.retry_of.3a6bb304": "Retry of:",
"i18n:govoplan-admin.retry.9f5cd8a2": "Retry",
"i18n:govoplan-admin.reusable_encrypted_smtp_imap_profiles_and_mail_p.31f150d1": "Reusable encrypted SMTP/IMAP profiles and mail policy.",
"i18n:govoplan-admin.reusable_file_server_connections_credentials_and.8e5c7d43": "Reusable file server connections, credentials and policy.",
"i18n:govoplan-admin.role.c3f104d1": "Role",
"i18n:govoplan-admin.roles.47dcc27d": "Roles",
"i18n:govoplan-admin.rollback_value.47e9c00b": "Rollback: {value0}",
"i18n:govoplan-admin.run_migrations.db6e0ce2": "Run migrations",
"i18n:govoplan-admin.running_registry.5274b24b": "Running registry",
"i18n:govoplan-admin.runtime_differs.43d1fd78": "Runtime differs",
"i18n:govoplan-admin.runtime_matches.a84afa48": "Runtime matches",
"i18n:govoplan-admin.safety_controlled_configuration_requests_approva.e8259509": "Safety-controlled configuration requests, approvals and rollback history.",
"i18n:govoplan-admin.save_plan.842dc280": "Save plan",
"i18n:govoplan-admin.save_settings.913aba9f": "Save settings",
"i18n:govoplan-admin.save_template.0885fab2": "Save template",
"i18n:govoplan-admin.save_tenant_role.8fc0d37d": "Save tenant role",
"i18n:govoplan-admin.save_the_install_plan_before_queueing_a_daemon_r.3ba00691": "Save the install plan before queueing a daemon request.",
"i18n:govoplan-admin.saved.c0ae8f6e": "Saved",
"i18n:govoplan-admin.saving.56a2285c": "Saving…",
"i18n:govoplan-admin.saving.ae7e8875": "Saving...",
"i18n:govoplan-admin.scoped_automation_credentials_capped_by_owner_pe.b3e20e54": "Scoped automation credentials capped by owner permissions.",
"i18n:govoplan-admin.secret.f4e7a874": "Secret",
"i18n:govoplan-admin.seq_value.0ae5fa47": "Seq {value0}",
"i18n:govoplan-admin.setting.fb449f71": "Setting",
"i18n:govoplan-admin.severity.de314fa0": "Severity",
"i18n:govoplan-admin.signed_and_trusted.bee31302": "Signed and trusted",
"i18n:govoplan-admin.signed_untrusted.864ed50c": "Signed, untrusted",
"i18n:govoplan-admin.signed.6e3665d8": "Signed",
"i18n:govoplan-admin.slug.094da9b9": "Slug",
"i18n:govoplan-admin.started.fe3824e9": "Started:",
"i18n:govoplan-admin.startup_state.d1300be6": "Startup state",
"i18n:govoplan-admin.status.bae7d5be": "Status",
"i18n:govoplan-admin.summary.12b71c3e": "Summary",
"i18n:govoplan-admin.supervisor_value.d9f0c8eb": "Supervisor: {value0}",
"i18n:govoplan-admin.supplied_data_json_must_be_an_object.b265eb92": "Supplied data JSON must be an object.",
"i18n:govoplan-admin.supplied_data_json.6932bfb7": "Supplied data JSON",
"i18n:govoplan-admin.system_administration.b151acea": "System administration",
"i18n:govoplan-admin.system_general_settings.7cd662ed": "System general settings",
"i18n:govoplan-admin.system_governed_group_definitions.de8e5dc9": "System-governed group definitions.",
"i18n:govoplan-admin.system_level_administrative_history.49f76723": "System-level administrative history.",
"i18n:govoplan-admin.system_modules.629539f2": "System modules",
"i18n:govoplan-admin.system_roles.a9461aa6": "System roles",
"i18n:govoplan-admin.system_settings_saved.dac4f17b": "System settings saved.",
"i18n:govoplan-admin.system_wide_governance_and_tenant_local_access_m.cda72499": "System-wide governance and tenant-local access management, separated by scope and enforced by the backend.",
"i18n:govoplan-admin.system.bc0792d8": "System",
"i18n:govoplan-admin.target.61ad50a9": "Target",
"i18n:govoplan-admin.template_details.d5d75e4d": "Template details",
"i18n:govoplan-admin.tenant_administration_capabilities.5d265972": "Tenant administration capabilities",
"i18n:govoplan-admin.tenant_administration.ffab51f1": "Tenant administration",
"i18n:govoplan-admin.tenant_availability.067a4f31": "Tenant availability",
"i18n:govoplan-admin.tenant_id.59eba244": "Tenant id",
"i18n:govoplan-admin.tenant_level_administrative_history_only.55495c3c": "Tenant-level administrative history only.",
"i18n:govoplan-admin.tenant_level_privacy_retention_limits_inherited_.48f4989d": "Tenant-level privacy retention limits inherited by owned objects.",
"i18n:govoplan-admin.tenant_local_and_centrally_managed_groups.4a6296b5": "Tenant-local and centrally managed groups.",
"i18n:govoplan-admin.tenant_local_and_centrally_managed_roles.4995a7b2": "Tenant-local and centrally managed roles.",
"i18n:govoplan-admin.tenant_locale_and_tenant_specific_settings.ac49c83b": "Tenant locale and tenant-specific settings.",
"i18n:govoplan-admin.tenant_memberships_and_inherited_roles.16eda9f6": "Tenant memberships and inherited roles.",
"i18n:govoplan-admin.tenant_permission_bundles_and_system_managed_rol.bb563bea": "Tenant permission bundles and system-managed role copies.",
"i18n:govoplan-admin.tenant_permissions.246294bc": "Tenant permissions",
"i18n:govoplan-admin.tenant_role.6b53115d": "Tenant role",
"i18n:govoplan-admin.tenant_roles.51aca82d": "Tenant roles",
"i18n:govoplan-admin.tenant_users.cb800b38": "Tenant users",
"i18n:govoplan-admin.tenants.1f7ae776": "Tenants",
"i18n:govoplan-admin.these_settings_are_enforced_by_the_backend_centr.8112ed6f": "These settings are enforced by the backend. Central groups and tenant roles remain available even when local creation is disabled.",
"i18n:govoplan-admin.trace.04a75036": "Trace:",
"i18n:govoplan-admin.trusted.99f7ed54": "Trusted",
"i18n:govoplan-admin.type.3deb7456": "Type",
"i18n:govoplan-admin.uninstall.a735da1d": "Uninstall",
"i18n:govoplan-admin.unlocked.b3da7025": "Unlocked",
"i18n:govoplan-admin.unsigned.e91344ea": "Unsigned",
"i18n:govoplan-admin.untrusted.cdc7838a": "Untrusted",
"i18n:govoplan-admin.update_enabled_modules.9b7ae790": "Update enabled modules",
"i18n:govoplan-admin.updated.f2f8570d": "Updated",
"i18n:govoplan-admin.user_file_connector_policy_limits": "File connector policy limits for user-owned spaces.",
"i18n:govoplan-admin.user_mail_server_policy_limits": "Mail server policy limits for user-owned work.",
"i18n:govoplan-admin.user_retention_policy_limits": "Retention limits for user-owned records.",
"i18n:govoplan-admin.users.57f2b181": "Users",
"i18n:govoplan-admin.valid_after.c615c873": "· Valid after:",
"i18n:govoplan-admin.valid_and_trusted.73cf89bc": "Valid and trusted",
"i18n:govoplan-admin.valid.a4aefa35": "Valid",
"i18n:govoplan-admin.valid.b374b8f9": "Valid:",
"i18n:govoplan-admin.value_deleted.3c4bf574": "{value0} deleted.",
"i18n:govoplan-admin.value_has_a_webui_reference_but_no_webui_package.18f44149": "{value0} has a WebUI reference but no WebUI package.",
"i18n:govoplan-admin.value_needs_a_python_package_name_for_rollback.6c89ed75": "{value0} needs a Python package name for rollback.",
"i18n:govoplan-admin.value_needs_a_python_package_name_for_uninstall.004ba6fa": "{value0} needs a Python package name for uninstall.",
"i18n:govoplan-admin.value_needs_a_python_package_reference.950b898e": "{value0} needs a Python package reference.",
"i18n:govoplan-admin.value_needs_both_webui_package_and_webui_referen.d2bab073": "{value0} needs both WebUI package and WebUI reference, or neither.",
"i18n:govoplan-admin.value_template_created.d68a5166": "{value0} template created.",
"i18n:govoplan-admin.value_updated_and_synchronized_to_assigned_tenan.d136eef2": "{value0} updated and synchronized to assigned tenants.",
"i18n:govoplan-admin.value_value.c189e8bc": "{value0} ({value1})",
"i18n:govoplan-admin.version.2da600bf": "Version",
"i18n:govoplan-admin.waiting_for_maintenance.60d00019": "Waiting for maintenance",
"i18n:govoplan-admin.webui_package.19645536": "WebUI package",
"i18n:govoplan-admin.webui_rebuild_required.010ce49f": "WebUI rebuild required",
"i18n:govoplan-admin.webui_ref.cbae3559": "WebUI ref",
"i18n:govoplan-admin.will_disable.14bb193a": "Will disable",
"i18n:govoplan-admin.will_enable.5d52d70b": "Will enable",
"i18n:govoplan-admin.working.049ac820": "Working..."
},
"de": {
"i18n:govoplan-admin.access_configuration_exported.098f200d": "Access configuration exported.",
"i18n:govoplan-admin.action.97c89a4d": "Action",
"i18n:govoplan-admin.actions.c3cd636a": "Aktionen",
"i18n:govoplan-admin.activate_installs.731d22a1": "Installationen aktivieren",
"i18n:govoplan-admin.active_and_total_memberships.c0c20f10": "Active and total memberships.",
"i18n:govoplan-admin.active_tenant_automation_credentials.240c659e": "Active tenant automation credentials.",
"i18n:govoplan-admin.active_tenant.dfadf0aa": "Active tenant:",
"i18n:govoplan-admin.active.a733b809": "Aktiv",
"i18n:govoplan-admin.add_group_template.b74d8f0f": "Add group template",
"i18n:govoplan-admin.add_plan_item.fa55f028": "Add plan item",
"i18n:govoplan-admin.add_tenant_role.fcd904ea": "Add tenant role",
"i18n:govoplan-admin.admin.4e7afebc": "Administration",
"i18n:govoplan-admin.administration.b8be3d12": "Administration",
"i18n:govoplan-admin.platform_administration": "Plattformadministration",
"i18n:govoplan-admin.alert_warning.804f617a": " alert warning",
"i18n:govoplan-admin.allow_tenant_api_keys_by_default.58a6cf17": "Mandanten-API-Schlüssel standardmäßig erlauben",
"i18n:govoplan-admin.allow_tenant_defined_groups_by_default.32099d5a": "Mandantendefinierte Gruppen standardmäßig erlauben",
"i18n:govoplan-admin.allow_tenant_defined_roles_by_default.b1f79ee9": "Mandantendefinierte Rollen standardmäßig erlauben",
"i18n:govoplan-admin.api_keys.94fcf3c2": "API-Schlüssel",
"i18n:govoplan-admin.applied.a3e4a569": "Angewendet",
"i18n:govoplan-admin.apply_approved_request.ab218d9a": "Apply approved request",
"i18n:govoplan-admin.apply_finished_with_blockers.78487a12": "Apply finished with blockers.",
"i18n:govoplan-admin.apply_result.0fde1c3c": "Apply result",
"i18n:govoplan-admin.apply.cfea419c": "Anwenden",
"i18n:govoplan-admin.approvals.deb9d03c": "Approvals",
"i18n:govoplan-admin.approve.7b2c7f14": "Freigeben",
"i18n:govoplan-admin.approved_package_references_that_can_be_added_to.cc911c16": "Approved package references that can be added to the operator install plan.",
"i18n:govoplan-admin.approved_value.52da808b": "Approved {value0}.",
"i18n:govoplan-admin.approvers.0e2de1fb": "Approvers",
"i18n:govoplan-admin.audit.fa1703dd": "Audit",
"i18n:govoplan-admin.available.7c62a142": "Verfügbar",
"i18n:govoplan-admin.blocked.99613c74": "Blockiert",
"i18n:govoplan-admin.build_webui.ed5ef1fd": " --build-webui",
"i18n:govoplan-admin.build_webui.fe8ccad7": "WebUI bauen",
"i18n:govoplan-admin.bypass_allowed.4e347c27": "Bypass allowed",
"i18n:govoplan-admin.bypass_denied.ab987400": "Bypass denied",
"i18n:govoplan-admin.cached_from.d73c5b2a": "· cached from",
"i18n:govoplan-admin.cancel.77dfd213": "Abbrechen",
"i18n:govoplan-admin.cancelled.8c9dcfa8": "Cancelled:",
"i18n:govoplan-admin.case_clerk.b78a314a": "Case clerk",
"i18n:govoplan-admin.catalog_install_entry_planned.13b23f4f": "Catalog install entry planned.",
"i18n:govoplan-admin.catalog.4a88d27b": "Katalog",
"i18n:govoplan-admin.central_groups.5c9b5b66": "Central groups",
"i18n:govoplan-admin.centrally_defined_group_identities_that_are_prov.7761fce9": "Centrally defined group identities that are provisioned into selected tenants as available or required definitions. Membership remains tenant-local.",
"i18n:govoplan-admin.centrally_governed_tenant_roles_and_their_availa.d879d5d1": "Centrally governed tenant roles and their availability across tenants.",
"i18n:govoplan-admin.centrally_governed_tenant_roles_that_are_provisi.8739fca3": "Centrally governed tenant roles that are provisioned into selected tenants as available or required definitions.",
"i18n:govoplan-admin.centrally_governed_tenant_roles.797c689b": "Centrally governed tenant roles.",
"i18n:govoplan-admin.change_request_created_value.4af6d3d2": "Change request created: {value0}",
"i18n:govoplan-admin.change_request_id.96ee3239": "Change request id",
"i18n:govoplan-admin.change_request_ready.b17e8c57": "Change request ready:",
"i18n:govoplan-admin.changes.8aa57de6": "Änderungen",
"i18n:govoplan-admin.changing_enabled_modules_requires_a_dry_run_chan.10b6f5ed": "Changing enabled modules requires a dry-run change request and active maintenance mode.",
"i18n:govoplan-admin.changing_the_maintenance_mode_flag_requires_syst.4492050f": "Changing the maintenance-mode flag requires system:maintenance:access. Login remains available so an operator can sign in during maintenance.",
"i18n:govoplan-admin.channel_value.d3894efe": "Channel: {value0}",
"i18n:govoplan-admin.clear.719ea396": "Leeren",
"i18n:govoplan-admin.close.bbfa773e": "Schließen",
"i18n:govoplan-admin.code.adac6937": "Code",
"i18n:govoplan-admin.commands.f6a34aed": "Commands:",
"i18n:govoplan-admin.configuration_changes.82933bbb": "Konfigurationsänderungen",
"i18n:govoplan-admin.configuration_package_apply.c270e5f3": "Configuration package apply",
"i18n:govoplan-admin.configuration_packages.eb2f05f1": "Konfigurationspakete",
"i18n:govoplan-admin.configuration_requests_approvals_and_version_his.19f37335": "Configuration requests, approvals and version history.",
"i18n:govoplan-admin.configured_license.3e73f8f5": "Configured license",
"i18n:govoplan-admin.create_dry_run_request.05b9cbdb": "Create dry-run request",
"i18n:govoplan-admin.create_group_template.72407248": "Create group template",
"i18n:govoplan-admin.create_request_and_apply.da79e153": "Create request and apply",
"i18n:govoplan-admin.create_suspend_and_govern_tenant_spaces.77992a39": "Create, suspend and govern tenant spaces.",
"i18n:govoplan-admin.create_tenant_role.f58db104": "Create tenant role",
"i18n:govoplan-admin.created.0c78dab1": "Created:",
"i18n:govoplan-admin.created.accf40c8": "Erstellt",
"i18n:govoplan-admin.daemon_execution.cc0fad8d": "Daemon-Ausführung",
"i18n:govoplan-admin.daemon_offline.314f19a4": "Daemon offline",
"i18n:govoplan-admin.daemon_value.e54f5a31": "Daemon: {value0}",
"i18n:govoplan-admin.db_backup_command.5bded3ac": "DB-Backup-Befehl",
"i18n:govoplan-admin.db_restore_check_command.750ae936": "DB-Wiederherstellungsprüfung",
"i18n:govoplan-admin.db_restore_command.de11eb01": "DB-Wiederherstellungsbefehl",
"i18n:govoplan-admin.db.e355c23a": "DB:",
"i18n:govoplan-admin.default_locale.b99d021f": "Standardsprache",
"i18n:govoplan-admin.install_language_package": "Sprachpaket installieren",
"i18n:govoplan-admin.language_code": "Sprachcode",
"i18n:govoplan-admin.language_name": "Sprachname",
"i18n:govoplan-admin.language_name_placeholder": "Franzoesisch",
"i18n:govoplan-admin.language_packages": "Sprachpakete",
"i18n:govoplan-admin.language_packages_help": "Installierte Pakete legen fest, welche Sprachcodes aktiviert werden koennen. Ein Paket ist erst vollstaendig uebersetzt, wenn passende Uebersetzungskataloge der Module vorhanden sind.",
"i18n:govoplan-admin.native_language_name": "Nativer Sprachname",
"i18n:govoplan-admin.native_language_name_placeholder": "Francais",
"i18n:govoplan-admin.defaults_for_newly_created_tenants.9d0f4ccd": "Standards für neu erstellte Mandanten",
"i18n:govoplan-admin.delete_group_template.8745d842": "Delete group template",
"i18n:govoplan-admin.delete_template.399bf72a": "Delete template",
"i18n:govoplan-admin.delete_tenant_role.4efa0813": "Delete tenant role",
"i18n:govoplan-admin.delete_value_removal_is_blocked_while_a_material.d4eba73d": "Delete {value0}? Removal is blocked while a materialized tenant definition still has members or assignments.",
"i18n:govoplan-admin.delete_value.4d18989e": "Delete {value0}",
"i18n:govoplan-admin.dependents.072665c4": "Dependents:",
"i18n:govoplan-admin.description.55f8ebc8": "Beschreibung",
"i18n:govoplan-admin.destroy_data.34557c3c": "Daten zerstören",
"i18n:govoplan-admin.disabled.f4f4473d": "Disabled",
"i18n:govoplan-admin.discovered_packages.660902de": "Discovered packages",
"i18n:govoplan-admin.drift.4876f7b9": "Drift",
"i18n:govoplan-admin.drop_uninstalls_from_startup.0e2a5c5b": "Deinstallationen aus dem Start entfernen",
"i18n:govoplan-admin.dry_run_change_request_created_value_enable_main.49826d07": "Dry-run change request created: {value0}. Enable maintenance mode, then apply the request.",
"i18n:govoplan-admin.dry_run.3d14659c": "Dry-run",
"i18n:govoplan-admin.edit_group_template.9bc72d21": "Edit group template",
"i18n:govoplan-admin.edit_tenant_role.c15a260d": "Edit tenant role",
"i18n:govoplan-admin.edit_value.fad75899": "Edit {value0}",
"i18n:govoplan-admin.enable_maintenance_mode.75f98d57": "Enable maintenance mode",
"i18n:govoplan-admin.enabled.df174a3f": "Aktiviert",
"i18n:govoplan-admin.enabling_maintenance_mode_requires_system_mainte.062d3968": "Enabling maintenance mode requires system:maintenance:access.",
"i18n:govoplan-admin.enabling.2b8e03e6": "Enabling...",
"i18n:govoplan-admin.enforced.93ff909c": "Erzwungen",
"i18n:govoplan-admin.every_plan_item_needs_a_module_id.23a7f206": "Every plan item needs a module id.",
"i18n:govoplan-admin.expires.2d21b7de": "· Expires:",
"i18n:govoplan-admin.export_access.9a2eb91e": "Export access",
"i18n:govoplan-admin.export_finished_with_blockers.e2f70611": "Export finished with blockers.",
"i18n:govoplan-admin.export.f3e4fadb": "Export",
"i18n:govoplan-admin.exported_system_access_configuration.2e1c6f4c": "Exported system access configuration",
"i18n:govoplan-admin.exported_tenant_access_configuration.9122c85c": "Exported tenant access configuration",
"i18n:govoplan-admin.features.5df81ffa": "Features:",
"i18n:govoplan-admin.file_connections.1e362326": "Dateiverbindungen",
"i18n:govoplan-admin.finished.4b52fe3f": "Finished:",
"i18n:govoplan-admin.fragment.3f19d616": "Fragment",
"i18n:govoplan-admin.front_office.d9dcfee1": "Front office",
"i18n:govoplan-admin.general.9239ee2c": "Allgemein",
"i18n:govoplan-admin.generated.a2edf57c": "Generated:",
"i18n:govoplan-admin.global": "Global",
"i18n:govoplan-admin.global_accounts_tenant_memberships_and_system_ro.939ed01f": "Global accounts, tenant memberships and system-role assignments.",
"i18n:govoplan-admin.global_login_accounts_across_all_tenants.2aab129d": "Global login accounts across all tenants.",
"i18n:govoplan-admin.govoplan_module_installer_format_shell.226d1450": "govoplan-module-installer --format shell",
"i18n:govoplan-admin.group_definitions_made_available_or_required_in_.55402e16": "Group definitions made available or required in selected tenants.",
"i18n:govoplan-admin.group_file_connector_policy_limits": "Dateiverbindungsrichtlinien fuer gruppeneigene Bereiche.",
"i18n:govoplan-admin.group_mail_server_policy_limits": "Mailserver-Richtlinien fuer gruppeneigene Arbeit.",
"i18n:govoplan-admin.group_retention_policy_limits": "Aufbewahrungslimits fuer gruppeneigene Datensaetze.",
"i18n:govoplan-admin.group_template.973e0fa6": "Group template",
"i18n:govoplan-admin.group.171a0606": "Group",
"i18n:govoplan-admin.groups.ae9629f4": "Gruppen",
"i18n:govoplan-admin.handles_incoming_administrative_work.dc80c349": "Handles incoming administrative work.",
"i18n:govoplan-admin.health_urls.3b86647c": "Health-URLs",
"i18n:govoplan-admin.history.90ccd649": "History",
"i18n:govoplan-admin.id.474ae526": "Id",
"i18n:govoplan-admin.import_preflight_approve_apply_and_export_module.29aec929": "Modul-eigene Konfigurationspakete importieren, prüfen, freigeben, anwenden und exportieren.",
"i18n:govoplan-admin.inactive.09af574c": "Inaktiv",
"i18n:govoplan-admin.inspect_value.9d5d1071": "Inspect {value0}",
"i18n:govoplan-admin.install.fd6c3ebf": "Installieren",
"i18n:govoplan-admin.installed_modules_active_runtime_state_and_saved.f33907dd": "Installed modules, active runtime state, and saved startup state.",
"i18n:govoplan-admin.installed_modules_runtime_state_and_startup_stat.38dd7028": "Installed modules, runtime state and startup state.",
"i18n:govoplan-admin.installed.7bb4405c": "Installed",
"i18n:govoplan-admin.installer_preflight_blocked.cf83bf79": "Installer preflight blocked",
"i18n:govoplan-admin.installer_preflight_passed.6d5f8060": "Installer preflight passed",
"i18n:govoplan-admin.installer_request_cancelled_value.1d6d099d": "Installer request cancelled: {value0}",
"i18n:govoplan-admin.installer_request_queued_value.00726abb": "Installer request queued: {value0}",
"i18n:govoplan-admin.installer_requests.b88db439": "Installer-Anfragen",
"i18n:govoplan-admin.installer_runs.e9e344e1": "Installer-Läufe",
"i18n:govoplan-admin.instance_defaults_and_tenant_governance_capabili.99d6b2fa": "Instance defaults and tenant governance capabilities.",
"i18n:govoplan-admin.instance_privacy_retention_policy_and_lower_leve.b8d14069": "Instance privacy retention policy and lower-level override permissions.",
"i18n:govoplan-admin.instance_wide_defaults_and_tenant_governance_cap.096a4f97": "Instanzweite Standards und Governance-Funktionen für Mandanten. Die Aufbewahrungsrichtlinien werden in einem eigenen Systembereich verwaltet.",
"i18n:govoplan-admin.instance_wide_roles_assigned_directly_to_global_.91050488": "Instance-wide roles assigned directly to global accounts.",
"i18n:govoplan-admin.invalid_json.01ccb74f": "Invalid JSON.",
"i18n:govoplan-admin.invalid_license.f16bf749": "Invalid license",
"i18n:govoplan-admin.json_must_be_an_object.848569c9": "JSON must be an object.",
"i18n:govoplan-admin.key_value.847b914f": "Key: {value0}",
"i18n:govoplan-admin.key.c67dd20e": "Key",
"i18n:govoplan-admin.kind.e00ac23f": "Kind",
"i18n:govoplan-admin.label.74341e3c": "Label",
"i18n:govoplan-admin.last_request.4508ef35": "Last request:",
"i18n:govoplan-admin.license.de13bf1a": "License:",
"i18n:govoplan-admin.locked.a798882f": "Gesperrt",
"i18n:govoplan-admin.mail_servers.d627326a": "Mail servers",
"i18n:govoplan-admin.maintenance_message.ca62571f": "Wartungsmeldung",
"i18n:govoplan-admin.maintenance_mode_enabled_apply_the_module_state_.dda13e5c": "Maintenance mode enabled. Apply the module-state request when ready.",
"i18n:govoplan-admin.maintenance_mode_is_off_package_changes_should_b.1d262629": "Maintenance mode is off. Package changes should be applied only after enabling maintenance mode.",
"i18n:govoplan-admin.maintenance_mode_must_be_enabled_before_queueing.d233a32f": "Der Wartungsmodus muss aktiviert sein, bevor eine Daemon-Anfrage eingereiht wird.",
"i18n:govoplan-admin.maintenance_mode.98cca5c6": "Wartungsmodus",
"i18n:govoplan-admin.maintenance.94de303b": "Wartung",
"i18n:govoplan-admin.membership_status_groups_and_direct_roles_in_the.ab6c10b6": "Membership status, groups and direct roles in the active tenant.",
"i18n:govoplan-admin.message.68f4145f": "Nachricht",
"i18n:govoplan-admin.minimal_office_access.af48f49a": "Minimal office access",
"i18n:govoplan-admin.missing_entitlement.0cafa611": "Missing entitlement",
"i18n:govoplan-admin.missing.feb2bbaa": "Missing:",
"i18n:govoplan-admin.module_install_plan_cleared.afa2698d": "Module install plan cleared.",
"i18n:govoplan-admin.module_install_plan_saved.5d43d944": "Module install plan saved.",
"i18n:govoplan-admin.module_maintenance_in_progress.f6395152": "Module maintenance in progress.",
"i18n:govoplan-admin.module_state_saved.e3e22105": "Module state saved.",
"i18n:govoplan-admin.module.b8ff0289": "Modul",
"i18n:govoplan-admin.modules.04e9462c": "Module",
"i18n:govoplan-admin.modules.2dc3b299": "Modules:",
"i18n:govoplan-admin.name.709a2322": "Name",
"i18n:govoplan-admin.needs_attention.a126722e": "Benötigt Aufmerksamkeit",
"i18n:govoplan-admin.no_applied_configuration_changes.6a3ae4a7": "No applied configuration changes.",
"i18n:govoplan-admin.no_central_value_templates_found.081149fa": "No central {value0} templates found.",
"i18n:govoplan-admin.no_diagnostics.2b6e2630": "No diagnostics.",
"i18n:govoplan-admin.no_installer_requests_recorded_yet.17e630fa": "Noch keine Installer-Anfragen aufgezeichnet.",
"i18n:govoplan-admin.no_installer_runs_recorded_yet.f8b61964": "Noch keine Installer-Läufe aufgezeichnet.",
"i18n:govoplan-admin.no_license_configured.693cea1a": "No license configured",
"i18n:govoplan-admin.no_license.eb4bfe19": "No license",
"i18n:govoplan-admin.no_open_requests.580c95f9": "No open requests.",
"i18n:govoplan-admin.no_package_catalog_configured_set_govoplan_modul.2c5fceb3": "No package catalog configured. Set GOVOPLAN_MODULE_PACKAGE_CATALOG or GOVOPLAN_MODULE_PACKAGE_CATALOG_URL to enable curated install entries.",
"i18n:govoplan-admin.no_package_changes_planned.f12d8b33": "No package changes planned.",
"i18n:govoplan-admin.no_package_restart_pending.efe6b8ce": "No package restart pending",
"i18n:govoplan-admin.no_plan_items.7108c582": "No plan items.",
"i18n:govoplan-admin.none.6eef6648": "Keine",
"i18n:govoplan-admin.not_available.d1a17af1": "Not available",
"i18n:govoplan-admin.not_configured.811931bb": "Nicht konfiguriert",
"i18n:govoplan-admin.not_recorded.9925ee3c": "not recorded",
"i18n:govoplan-admin.notes.70440046": "Notes",
"i18n:govoplan-admin.object.2883f191": "Object",
"i18n:govoplan-admin.observe_only.c6a1f8bc": "Nur beobachten",
"i18n:govoplan-admin.off.e3de5ab0": "Off",
"i18n:govoplan-admin.on.e0049a66": "On",
"i18n:govoplan-admin.operator_install_plan.b203aabc": "Operator install plan",
"i18n:govoplan-admin.optional.166b8369": "Optional:",
"i18n:govoplan-admin.overview.0efc2e6b": "Übersicht",
"i18n:govoplan-admin.owner.89ff3122": "Eigentümer",
"i18n:govoplan-admin.package_applied.63782ce7": "Package applied.",
"i18n:govoplan-admin.package_catalog_is_configured_but_contains_no_en.b6b053cb": "Package catalog is configured but contains no entries.",
"i18n:govoplan-admin.package_catalog.b36e6d31": "Package catalog",
"i18n:govoplan-admin.package_json_must_be_an_object.db825bb3": "Package JSON must be an object.",
"i18n:govoplan-admin.package_json.a2b10f38": "Package JSON",
"i18n:govoplan-admin.package_uninstall_planned.56e8566d": "Package uninstall planned.",
"i18n:govoplan-admin.package.7431e3df": "Package",
"i18n:govoplan-admin.packages.0a999012": "Pakete",
"i18n:govoplan-admin.pending.c515ec74": " pending",
"i18n:govoplan-admin.permissions.d06d5557": "Berechtigungen",
"i18n:govoplan-admin.plan_install.e82bffe6": "Plan install",
"i18n:govoplan-admin.plan_package_installs_and_removals_here_then_app.4aeb03bf": "Plan package installs and removals here, then apply the rendered commands from an operator shell during maintenance mode.",
"i18n:govoplan-admin.plan_uninstall.e62804ab": "Plan uninstall",
"i18n:govoplan-admin.plan.ae2f98a0": "Plan",
"i18n:govoplan-admin.planned.9cbe42aa": "Geplant",
"i18n:govoplan-admin.preflight_approve_apply_and_export_configuration.276bd0f8": "Preflight, approve, apply and export configuration packages.",
"i18n:govoplan-admin.preflight_finished_with_blockers.7e2ca12b": "Preflight finished with blockers.",
"i18n:govoplan-admin.preflight_passed.c0c99055": "Preflight passed.",
"i18n:govoplan-admin.preflight.8016a487": "Preflight",
"i18n:govoplan-admin.python_package.34591c71": "Python-Paket",
"i18n:govoplan-admin.python_ref.1af35d0d": "Python-Referenz",
"i18n:govoplan-admin.queue_supervised_run.a53f248c": "Queue supervised run",
"i18n:govoplan-admin.queue_the_saved_plan_for_a_separate_installer_da.92d4c96e": "Den gespeicherten Plan für einen separaten Installer-Daemon-Prozess einreihen.",
"i18n:govoplan-admin.queued_daemon_handoffs_created_from_the_admin_ui.d7ae7914": "Queued daemon handoffs created from the admin UI or CLI.",
"i18n:govoplan-admin.queueing_installer_requests_requires_maintenance.ae81f9ac": "Das Einreihen von Installer-Anfragen erfordert Wartungszugriff.",
"i18n:govoplan-admin.recent_supervised_installer_records_and_the_curr.29860245": "Aktuelle überwachte Installer-Datensätze und die aktuelle Paketinstallationssperre.",
"i18n:govoplan-admin.registered_tenant_spaces.61e17d70": "Registered tenant spaces.",
"i18n:govoplan-admin.reload.cce71553": "Neu laden",
"i18n:govoplan-admin.remove.e963907d": "Entfernen",
"i18n:govoplan-admin.request.43c2e7a7": "Anfrage:",
"i18n:govoplan-admin.request_approval.6245aea1": "Request approval",
"i18n:govoplan-admin.requested.c26bf60f": "Requested",
"i18n:govoplan-admin.requests.f7194e6a": "Requests",
"i18n:govoplan-admin.required_data.1b1c1b34": "Required data",
"i18n:govoplan-admin.required_means_the_definition_must_remain_presen.8462063c": "Required means the definition must remain present and system-controlled. It does not automatically assign users or grant permissions.",
"i18n:govoplan-admin.required.d5793988": "Required:",
"i18n:govoplan-admin.required.eed6bfb4": "Required",
"i18n:govoplan-admin.requires.a4fc9357": "Requires:",
"i18n:govoplan-admin.restart_commands.035997f9": "Neustartbefehle",
"i18n:govoplan-admin.restart_reload_required_after_package_changes.3a99cd63": "Restart/reload required after package changes",
"i18n:govoplan-admin.restrict_authenticated_api_access_to_maintenance.2bc47195": "Authentifizierten API-Zugriff auf Wartungsoperatoren beschränken",
"i18n:govoplan-admin.retention.c7199d9e": "Retention",
"i18n:govoplan-admin.retry_of.3a6bb304": "Retry of:",
"i18n:govoplan-admin.retry.9f5cd8a2": "Erneut versuchen",
"i18n:govoplan-admin.reusable_encrypted_smtp_imap_profiles_and_mail_p.31f150d1": "Reusable encrypted SMTP/IMAP profiles and mail policy.",
"i18n:govoplan-admin.reusable_file_server_connections_credentials_and.8e5c7d43": "Reusable file server connections, credentials and policy.",
"i18n:govoplan-admin.role.c3f104d1": "Role",
"i18n:govoplan-admin.roles.47dcc27d": "Roles",
"i18n:govoplan-admin.rollback_value.47e9c00b": "Rollback: {value0}",
"i18n:govoplan-admin.run_migrations.db6e0ce2": "Migrationen ausführen",
"i18n:govoplan-admin.running_registry.5274b24b": "Running registry",
"i18n:govoplan-admin.runtime_differs.43d1fd78": "Runtime differs",
"i18n:govoplan-admin.runtime_matches.a84afa48": "Runtime matches",
"i18n:govoplan-admin.safety_controlled_configuration_requests_approva.e8259509": "Safety-controlled configuration requests, approvals and rollback history.",
"i18n:govoplan-admin.save_plan.842dc280": "Save plan",
"i18n:govoplan-admin.save_settings.913aba9f": "Einstellungen speichern",
"i18n:govoplan-admin.save_template.0885fab2": "Save template",
"i18n:govoplan-admin.save_tenant_role.8fc0d37d": "Save tenant role",
"i18n:govoplan-admin.save_the_install_plan_before_queueing_a_daemon_r.3ba00691": "Speichern Sie den Installationsplan, bevor eine Daemon-Anfrage eingereiht wird.",
"i18n:govoplan-admin.saved.c0ae8f6e": "Gespeichert",
"i18n:govoplan-admin.saving.56a2285c": "Saving…",
"i18n:govoplan-admin.saving.ae7e8875": "Saving...",
"i18n:govoplan-admin.scoped_automation_credentials_capped_by_owner_pe.b3e20e54": "Scoped automation credentials capped by owner permissions.",
"i18n:govoplan-admin.secret.f4e7a874": "Secret",
"i18n:govoplan-admin.seq_value.0ae5fa47": "Seq {value0}",
"i18n:govoplan-admin.setting.fb449f71": "Setting",
"i18n:govoplan-admin.severity.de314fa0": "Severity",
"i18n:govoplan-admin.signed_and_trusted.bee31302": "Signed and trusted",
"i18n:govoplan-admin.signed_untrusted.864ed50c": "Signiert, nicht vertrauenswürdig",
"i18n:govoplan-admin.signed.6e3665d8": "Signiert",
"i18n:govoplan-admin.slug.094da9b9": "Slug",
"i18n:govoplan-admin.started.fe3824e9": "Started:",
"i18n:govoplan-admin.startup_state.d1300be6": "Startup state",
"i18n:govoplan-admin.status.bae7d5be": "Status",
"i18n:govoplan-admin.summary.12b71c3e": "Summary",
"i18n:govoplan-admin.supervisor_value.d9f0c8eb": "Supervisor: {value0}",
"i18n:govoplan-admin.supplied_data_json_must_be_an_object.b265eb92": "Supplied data JSON must be an object.",
"i18n:govoplan-admin.supplied_data_json.6932bfb7": "Supplied data JSON",
"i18n:govoplan-admin.system_administration.b151acea": "System administration",
"i18n:govoplan-admin.system_general_settings.7cd662ed": "Allgemeine Systemeinstellungen",
"i18n:govoplan-admin.system_governed_group_definitions.de8e5dc9": "System-governed group definitions.",
"i18n:govoplan-admin.system_level_administrative_history.49f76723": "System-level administrative history.",
"i18n:govoplan-admin.system_modules.629539f2": "System modules",
"i18n:govoplan-admin.system_roles.a9461aa6": "System roles",
"i18n:govoplan-admin.system_settings_saved.dac4f17b": "Systemeinstellungen gespeichert.",
"i18n:govoplan-admin.system_wide_governance_and_tenant_local_access_m.cda72499": "System-wide governance and tenant-local access management, separated by scope and enforced by the backend.",
"i18n:govoplan-admin.system.bc0792d8": "System",
"i18n:govoplan-admin.target.61ad50a9": "Target",
"i18n:govoplan-admin.template_details.d5d75e4d": "Template details",
"i18n:govoplan-admin.tenant_administration_capabilities.5d265972": "Tenant administration capabilities",
"i18n:govoplan-admin.tenant_administration.ffab51f1": "Tenant administration",
"i18n:govoplan-admin.tenant_availability.067a4f31": "Tenant availability",
"i18n:govoplan-admin.tenant_id.59eba244": "Tenant id",
"i18n:govoplan-admin.tenant_level_administrative_history_only.55495c3c": "Tenant-level administrative history only.",
"i18n:govoplan-admin.tenant_level_privacy_retention_limits_inherited_.48f4989d": "Tenant-level privacy retention limits inherited by owned objects.",
"i18n:govoplan-admin.tenant_local_and_centrally_managed_groups.4a6296b5": "Tenant-local and centrally managed groups.",
"i18n:govoplan-admin.tenant_local_and_centrally_managed_roles.4995a7b2": "Tenant-local and centrally managed roles.",
"i18n:govoplan-admin.tenant_locale_and_tenant_specific_settings.ac49c83b": "Tenant locale and tenant-specific settings.",
"i18n:govoplan-admin.tenant_memberships_and_inherited_roles.16eda9f6": "Tenant memberships and inherited roles.",
"i18n:govoplan-admin.tenant_permission_bundles_and_system_managed_rol.bb563bea": "Tenant permission bundles and system-managed role copies.",
"i18n:govoplan-admin.tenant_permissions.246294bc": "Tenant permissions",
"i18n:govoplan-admin.tenant_role.6b53115d": "Tenant role",
"i18n:govoplan-admin.tenant_roles.51aca82d": "Mandantenrollen",
"i18n:govoplan-admin.tenant_users.cb800b38": "Mandantenbenutzer",
"i18n:govoplan-admin.tenants.1f7ae776": "Mandanten",
"i18n:govoplan-admin.these_settings_are_enforced_by_the_backend_centr.8112ed6f": "Diese Einstellungen werden vom Backend erzwungen. Zentrale Gruppen und Mandantenrollen bleiben verfügbar, auch wenn lokale Erstellung deaktiviert ist.",
"i18n:govoplan-admin.trace.04a75036": "Trace:",
"i18n:govoplan-admin.trusted.99f7ed54": "Vertrauenswürdig",
"i18n:govoplan-admin.type.3deb7456": "Typ",
"i18n:govoplan-admin.uninstall.a735da1d": "Deinstallieren",
"i18n:govoplan-admin.unlocked.b3da7025": "Entsperrt",
"i18n:govoplan-admin.unsigned.e91344ea": "Unsigniert",
"i18n:govoplan-admin.untrusted.cdc7838a": "Nicht vertrauenswürdig",
"i18n:govoplan-admin.update_enabled_modules.9b7ae790": "Update enabled modules",
"i18n:govoplan-admin.updated.f2f8570d": "Aktualisiert",
"i18n:govoplan-admin.user_file_connector_policy_limits": "Dateiverbindungsrichtlinien fuer benutzereigene Bereiche.",
"i18n:govoplan-admin.user_mail_server_policy_limits": "Mailserver-Richtlinien fuer benutzereigene Arbeit.",
"i18n:govoplan-admin.user_retention_policy_limits": "Aufbewahrungslimits fuer benutzereigene Datensaetze.",
"i18n:govoplan-admin.users.57f2b181": "Benutzer",
"i18n:govoplan-admin.valid_after.c615c873": "· Valid after:",
"i18n:govoplan-admin.valid_and_trusted.73cf89bc": "Valid and trusted",
"i18n:govoplan-admin.valid.a4aefa35": "Gültig",
"i18n:govoplan-admin.valid.b374b8f9": "Valid:",
"i18n:govoplan-admin.value_deleted.3c4bf574": "{value0} deleted.",
"i18n:govoplan-admin.value_has_a_webui_reference_but_no_webui_package.18f44149": "{value0} has a WebUI reference but no WebUI package.",
"i18n:govoplan-admin.value_needs_a_python_package_name_for_rollback.6c89ed75": "{value0} needs a Python package name for rollback.",
"i18n:govoplan-admin.value_needs_a_python_package_name_for_uninstall.004ba6fa": "{value0} needs a Python package name for uninstall.",
"i18n:govoplan-admin.value_needs_a_python_package_reference.950b898e": "{value0} needs a Python package reference.",
"i18n:govoplan-admin.value_needs_both_webui_package_and_webui_referen.d2bab073": "{value0} needs both WebUI package and WebUI reference, or neither.",
"i18n:govoplan-admin.value_template_created.d68a5166": "{value0} template created.",
"i18n:govoplan-admin.value_updated_and_synchronized_to_assigned_tenan.d136eef2": "{value0} updated and synchronized to assigned tenants.",
"i18n:govoplan-admin.value_value.c189e8bc": "{value0} ({value1})",
"i18n:govoplan-admin.version.2da600bf": "Version",
"i18n:govoplan-admin.waiting_for_maintenance.60d00019": "Waiting for maintenance",
"i18n:govoplan-admin.webui_package.19645536": "WebUI-Paket",
"i18n:govoplan-admin.webui_rebuild_required.010ce49f": "WebUI rebuild required",
"i18n:govoplan-admin.webui_ref.cbae3559": "WebUI-Referenz",
"i18n:govoplan-admin.will_disable.14bb193a": "Wird deaktiviert",
"i18n:govoplan-admin.will_enable.5d52d70b": "Wird aktiviert",
"i18n:govoplan-admin.working.049ac820": "Working..."
}
};

View File

@@ -2,6 +2,7 @@ export { default } from "./module";
export * from "./module";
export * from "./api/admin";
export { default as AdminOverviewPanel } from "./features/admin/AdminOverviewPanel";
export { default as ConfigurationPackagesPanel } from "./features/admin/ConfigurationPackagesPanel";
export { default as GovernanceTemplatesPanel } from "./features/admin/GovernanceTemplatesPanel";
export { default as SystemSettingsPanel } from "./features/admin/SystemSettingsPanel";
export type { PlatformWebModule, PlatformNavItem, PlatformRouteContribution, PlatformRouteContext } from "@govoplan/core-webui";

View File

@@ -1,87 +1,118 @@
import { createElement, lazy } from "react";
import type { AdminSectionsUiCapability, PlatformWebModule } from "@govoplan/core-webui";
import { adminReadScopes, hasScope } from "@govoplan/core-webui";
import { generatedTranslations } from "./i18n/generatedTranslations";
const AdminOverviewPanel = lazy(() => import("./features/admin/AdminOverviewPanel"));
const ConfigurationChangesPanel = lazy(() => import("./features/admin/ConfigurationChangesPanel"));
const ConfigurationPackagesPanel = lazy(() => import("./features/admin/ConfigurationPackagesPanel"));
const GovernanceTemplatesPanel = lazy(() => import("./features/admin/GovernanceTemplatesPanel"));
const ModuleManagementPanel = lazy(() => import("./features/admin/ModuleManagementPanel"));
const SystemSettingsPanel = lazy(() => import("./features/admin/SystemSettingsPanel"));
const translations = {
en: generatedTranslations.en,
de: generatedTranslations.de
};
const adminSections: AdminSectionsUiCapability = {
sections: [
{
id: "overview",
label: "Overview",
group: "ROOT",
order: 0,
anyOf: adminReadScopes,
render: ({ settings, availableSections, selectSection }) => createElement(AdminOverviewPanel, {
settings,
availableSections,
onSelect: selectSection
})
},
{
id: "system-settings",
label: "General",
group: "SYSTEM",
order: 10,
allOf: ["system:settings:read"],
render: ({ settings, auth }) => createElement(SystemSettingsPanel, {
settings,
canWrite: hasScope(auth, "system:settings:write"),
canAccessMaintenance: hasScope(auth, "system:maintenance:access")
})
},
{
id: "system-role-templates",
label: "Tenant roles",
group: "SYSTEM",
order: 40,
allOf: ["system:governance:read"],
render: ({ settings, auth, refreshAuth }) => createElement(GovernanceTemplatesPanel, {
settings,
kind: "role",
canWrite: hasScope(auth, "system:governance:write"),
onAuthRefresh: refreshAuth
})
},
{
id: "system-modules",
label: "Modules",
group: "SYSTEM",
order: 85,
allOf: ["system:settings:read"],
render: ({ settings, auth }) => createElement(ModuleManagementPanel, {
settings,
canWrite: hasScope(auth, "system:settings:write"),
canAccessMaintenance: hasScope(auth, "system:maintenance:access")
})
},
{
id: "system-groups",
label: "Groups",
group: "SYSTEM",
order: 50,
allOf: ["system:governance:read"],
render: ({ settings, auth, refreshAuth }) => createElement(GovernanceTemplatesPanel, {
settings,
kind: "group",
canWrite: hasScope(auth, "system:governance:write"),
onAuthRefresh: refreshAuth
})
}
]
{
id: "overview",
label: "i18n:govoplan-admin.overview.0efc2e6b",
group: "ROOT",
order: 0,
anyOf: adminReadScopes,
render: ({ settings, availableSections, selectSection }) => createElement(AdminOverviewPanel, {
settings,
availableSections,
onSelect: selectSection
})
},
{
id: "system-settings",
label: "i18n:govoplan-admin.general.9239ee2c",
group: "SYSTEM",
order: 10,
allOf: ["system:settings:read"],
render: ({ settings, auth }) => createElement(SystemSettingsPanel, {
settings,
canWrite: hasScope(auth, "system:settings:write"),
canAccessMaintenance: hasScope(auth, "system:maintenance:access")
})
},
{
id: "system-configuration-changes",
label: "i18n:govoplan-admin.changes.8aa57de6",
group: "SYSTEM",
order: 20,
allOf: ["system:settings:read"],
render: ({ settings, auth }) => createElement(ConfigurationChangesPanel, {
settings,
canApprove: hasScope(auth, "system:settings:write") || hasScope(auth, "system:governance:write")
})
},
{
id: "system-configuration-packages",
label: "i18n:govoplan-admin.packages.0a999012",
group: "SYSTEM",
order: 30,
allOf: ["system:settings:read"],
render: ({ settings, auth }) => createElement(ConfigurationPackagesPanel, {
settings,
canWrite: hasScope(auth, "system:settings:write")
})
},
{
id: "system-role-templates",
label: "i18n:govoplan-admin.tenant_roles.51aca82d",
group: "SYSTEM",
order: 40,
allOf: ["system:governance:read"],
render: ({ settings, auth, refreshAuth }) => createElement(GovernanceTemplatesPanel, {
settings,
kind: "role",
canWrite: hasScope(auth, "system:governance:write"),
onAuthRefresh: refreshAuth
})
},
{
id: "system-modules",
label: "i18n:govoplan-admin.modules.04e9462c",
group: "SYSTEM",
order: 85,
allOf: ["system:settings:read"],
render: ({ settings, auth }) => createElement(ModuleManagementPanel, {
settings,
canWrite: hasScope(auth, "system:settings:write"),
canAccessMaintenance: hasScope(auth, "system:maintenance:access")
})
},
{
id: "system-groups",
label: "i18n:govoplan-admin.groups.ae9629f4",
group: "SYSTEM",
order: 50,
allOf: ["system:governance:read"],
render: ({ settings, auth, refreshAuth }) => createElement(GovernanceTemplatesPanel, {
settings,
kind: "group",
canWrite: hasScope(auth, "system:governance:write"),
onAuthRefresh: refreshAuth
})
}]
};
export const adminModule: PlatformWebModule = {
id: "admin",
label: "Admin",
label: "i18n:govoplan-admin.admin.4e7afebc",
version: "1.0.0",
dependencies: ["access"],
translations,
uiCapabilities: {
"admin.sections": adminSections
}
};
export default adminModule;
export default adminModule;