3 Commits
Author SHA1 Message Date
zemion aff2623496 feat: contribute Messages product surface
Module Package Release / publish-packages (push) Successful in 13s
2026-08-24 12:53:44 +02:00
zemion 4bdc3ab60c fix(webui): bind consequential postbox controls to help
Module Package Release / publish-packages (push) Successful in 12s
2026-08-24 11:36:42 +02:00
zemion aba6280f21 docs: complete German structured documentation
Module Package Release / publish-packages (push) Successful in 11s
2026-08-24 01:15:37 +02:00
8 changed files with 208 additions and 11 deletions
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "govoplan-postbox" name = "govoplan-postbox"
version = "0.1.19" version = "0.1.22"
description = "Function-bound institutional postboxes for GovOPlaN." description = "Function-bound institutional postboxes for GovOPlaN."
readme = "README.md" readme = "README.md"
requires-python = ">=3.12" requires-python = ">=3.12"
+1 -1
View File
@@ -1,3 +1,3 @@
"""GovOPlaN Postbox module.""" """GovOPlaN Postbox module."""
__version__ = "0.1.19" __version__ = "0.1.22"
@@ -0,0 +1,137 @@
"""German translations for public structured documentation metadata."""
from __future__ import annotations
from typing import Any
GERMAN_STRUCTURED_TRANSLATIONS: dict[str, dict[str, Any]] = {'postbox.content-protection-policy': {'privacy_notes': ['E2EE schützt Inhalte, nicht operative '
'Metadaten.',
'Verwaltete Umschläge sind von '
'autorisierten institutionellen '
'Schlüsselinhabern entschlüsselbar.',
'Profilübergänge bewahren '
'Autoritätsnachweise und inhaltliche '
'Kontinuität.']},
'postbox.function-bound-containers': {'privacy_notes': ['Der Verzeichniszugriff wird aus '
'aktuellen effektiven '
'Funktionszuweisungen abgeleitet.',
'Nicht verfügbare Nachrichtenzustände '
'zeigen nur gespeicherte Audit-Metadaten '
'an, die der aktuell handelnden Person '
'erlaubt sind.',
'Themen, Teilnehmer, Routing-Fakten und '
'Anhangreferenzen bleiben beobachtbare '
'Metadaten.']},
'postbox.lifecycle-notifications': {'privacy_notes': ['Lifecycle-Benachrichtigungen enthalten '
'niemals Nachrichten- oder Anhanginhalte.',
'Der aktuelle Zugriff wird erneut '
'überprüft, wenn ein Benachrichtigungslink '
'geöffnet wird.',
'Benachrichtigungen besitzen Präferenz und '
'Durchsetzung externer Kanäle.']},
'postbox.privacy.data-subject-requests': {'limitations': ['Verschlüsselte Inhalte werden vom '
'Schutzstaat gemeldet, aber nicht vom '
'Anbieter entschlüsselt oder '
'exportiert.',
'Attachment-Nutzlasten, '
'Produzentendatensätze, '
'Identitätsdatensätze und '
'Zuweisungsdatensätze verbleiben bei '
'ihren eigenen Modulen.'],
'steps': ['Führen Sie den Postbox-Anbieter aus und '
'überprüfen Sie Nachricht, Teilnehmer, '
'Empfang, Gruppierung, Zugriff und '
'Governance-Dispositionen.',
'Bewahren Sie die institutionelle Lieferung, '
'das Routing, die Bestätigung, den Zugang und '
'die Schutznachweise mit dem Grund auf.',
'Überprüfen Sie Klartextinhalte auf Daten von '
'Drittanbietern und entsprechende Datensätze '
'oder halten Sie Richtlinien vor einer '
'separaten Lifecycle-Aktion.',
'Führen Sie das Löschen von '
'Personengruppierungen nur aus einem neuen '
'Plan aus; überprüfen Sie, ob die '
'Quellpostfächer und Nachrichten unverändert '
'bleiben.']},
'postbox.reference.fields-and-consequences': {'consequence_classes': {'archive_postbox': 'Stoppt '
'neue '
'Lieferung, '
'während '
'Nachrichten, '
'Quittungen '
'und '
'Nachweise '
'beibehalten '
'werden.',
'delete_grouping': 'Löscht '
'nur die '
'persönliche '
'Projektion; '
'Quellpostfächer '
'und '
'Nachrichten '
'bleiben '
'unverändert.',
'preview_template': 'Lies '
'die '
'aktuelle '
'Organisation, '
'Hierarchie '
'und '
'den '
'Status '
'der '
'Amtsinhaber, '
'ohne '
'eine '
'Adresse '
'oder '
'Postbox '
'zu '
'materialisieren.',
'publish_template': 'Friert '
'eine '
'unveränderliche '
'Adress- '
'und '
'Routing-Revision '
'für '
'zukünftige '
'Materialisierung '
'ein.',
'retire_template': 'Stoppt '
'neue '
'Revisionen '
'und '
'Materialisierungen, '
'während '
'bestehende '
'Adressen '
'beibehalten '
'werden.',
'route_copy': 'Erstellt '
'einen '
'separat '
'aufbewahrten '
'Liefer- und '
'Beweisdatensatz '
'bei jedem '
'begrenzten '
'Ziel.',
'withdraw_or_expire': 'Blockiert '
'den '
'Zugriff '
'auf '
'zukünftige '
'Inhalte, '
'während '
'erlaubte '
'Audit-Metadaten '
'beibehalten '
'werden.'}},
'postbox.unified-inbox-policy': {'privacy_notes': ['Versteckte Gruppierungsquellen zeigen keine '
'Metadaten oder Zählungen.',
'Ein View wählt eine Projektion aus, gewährt '
'jedoch niemals Postbox-Zugriff.']}}
+54 -3
View File
@@ -1,5 +1,8 @@
from __future__ import annotations from __future__ import annotations
from govoplan_core.core.modules import with_documentation_structured_translations
from govoplan_postbox.backend.german_structured_documentation import GERMAN_STRUCTURED_TRANSLATIONS
from pathlib import Path from pathlib import Path
from govoplan_core.core.access import ( from govoplan_core.core.access import (
@@ -31,7 +34,9 @@ from govoplan_core.core.modules import (
ModuleManifest, ModuleManifest,
NavItem, NavItem,
PermissionDefinition, PermissionDefinition,
ProductAvailabilityExplanation,
ProductAreaContribution, ProductAreaContribution,
ProductSurfaceContribution,
QuickAccessTool, QuickAccessTool,
RoleTemplate, RoleTemplate,
) )
@@ -72,7 +77,7 @@ from govoplan_postbox.backend.permissions import (
MODULE_ID = "postbox" MODULE_ID = "postbox"
MODULE_NAME = "Postbox" MODULE_NAME = "Postbox"
MODULE_VERSION = "0.1.19" MODULE_VERSION = "0.1.22"
def _permission(scope: str, label: str, description: str) -> PermissionDefinition: def _permission(scope: str, label: str, description: str) -> PermissionDefinition:
@@ -401,6 +406,40 @@ manifest = ModuleManifest(
order=40, order=40,
), ),
), ),
product_surfaces=(
ProductSurfaceContribution(
id="communication.messages",
module_id=MODULE_ID,
label="i18n:govoplan-core.product_surface.messages",
description="i18n:govoplan-core.product_surface.messages_description",
icon="mail",
entry_path="/messages",
route_path="/postbox",
surface_ids=("postbox.nav.postbox", "postbox.route.postbox"),
presentations=("task", "reader"),
capability_ids=(CAPABILITY_POSTBOX_MESSAGES,),
search_source_ids=("postbox.messages",),
help_context_ids=("postbox.quick_access.messages",),
documentation_topic_ids=("postbox.quick-access-and-product-area",),
required_any=(READ_SCOPE,),
aliases=("/inbox",),
order=20,
unavailable=ProductAvailabilityExplanation(
reason="authorization",
title="i18n:govoplan-core.product_surface.messages_unavailable",
description="i18n:govoplan-core.product_surface.messages_unavailable_description",
resolution="i18n:govoplan-core.product_surface.messages_unavailable_resolution",
responsible_role="i18n:govoplan-core.access_administrator",
),
degraded=ProductAvailabilityExplanation(
reason="provider_degraded",
title="i18n:govoplan-core.product_surface.messages_degraded",
description="i18n:govoplan-core.product_surface.messages_degraded_description",
resolution="i18n:govoplan-core.product_surface.messages_degraded_resolution",
responsible_role="i18n:govoplan-core.integration_operator",
),
),
),
quick_access_tools=( quick_access_tools=(
QuickAccessTool( QuickAccessTool(
id="postbox.messages", id="postbox.messages",
@@ -620,7 +659,11 @@ manifest = ModuleManifest(
summary="Use function-bound Postboxes in Communication and the shared Messages Quick Access drawer.", summary="Use function-bound Postboxes in Communication and the shared Messages Quick Access drawer.",
body=( body=(
"Postbox contributes its inbox to Communication. With Quick Access enabled, its owner-rendered surface lists at most " "Postbox contributes its inbox to Communication. With Quick Access enabled, its owner-rendered surface lists at most "
"seven currently readable unread messages beside independent Mail and future chat providers. Selecting or opening a " "seven currently readable unread messages beside independent Mail and future chat providers. "
"The stable `/messages` product entry and `/inbox` migration alias select Postbox only when its route is "
"currently authorized and visible in the effective View; otherwise another Messages owner is selected or the shell "
"explains how access can be restored. "
"Selecting or opening a "
"message returns a typed Postbox reference; accounts with message-write permission can launch the full owner-rendered " "message returns a typed Postbox reference; accounts with message-write permission can launch the full owner-rendered "
"composer for a currently accessible function Postbox. Directory, message, and submission calls recheck the active " "composer for a currently accessible function Postbox. Directory, message, and submission calls recheck the active "
"assignment or acting context. Function assignment, classification, read-receipt, retention, encryption, and evidence " "assignment or acting context. Function assignment, classification, read-receipt, retention, encryption, and evidence "
@@ -637,6 +680,9 @@ manifest = ModuleManifest(
"body": ( "body": (
"Postbox ordnet seinen Eingang Kommunikation zu. Ist der Schnellzugriff aktiviert, erscheint die vom Modul gerenderte " "Postbox ordnet seinen Eingang Kommunikation zu. Ist der Schnellzugriff aktiviert, erscheint die vom Modul gerenderte "
"Oberfläche mit höchstens sieben aktuell lesbaren ungelesenen Nachrichten neben unabhängigen Beiträgen aus Mail. " "Oberfläche mit höchstens sieben aktuell lesbaren ungelesenen Nachrichten neben unabhängigen Beiträgen aus Mail. "
"Der stabile Produkteinstieg `/messages` und der Migrationsalias `/inbox` wählen Postbox nur, wenn seine Route "
"aktuell berechtigt und in der wirksamen View sichtbar ist. Andernfalls wird ein anderer Nachrichten-Eigentümer "
"gewählt oder die Shell erklärt die Wiederherstellung des Zugriffs. "
"Auswahl oder Öffnen liefert eine typisierte Postbox-Referenz; mit Schreibberechtigung lässt sich der vollständige " "Auswahl oder Öffnen liefert eine typisierte Postbox-Referenz; mit Schreibberechtigung lässt sich der vollständige "
"Editor für ein aktuell zugängliches Funktionspostfach öffnen. Verzeichnis, Nachricht und Versand prüfen die aktive " "Editor für ein aktuell zugängliches Funktionspostfach öffnen. Verzeichnis, Nachricht und Versand prüfen die aktive "
"Zuweisung beziehungsweise den Handlungskontext erneut. Klassifikation, Lesestatus, Aufbewahrung, Verschlüsselung " "Zuweisung beziehungsweise den Handlungskontext erneut. Klassifikation, Lesestatus, Aufbewahrung, Verschlüsselung "
@@ -722,7 +768,7 @@ manifest = ModuleManifest(
"kind": "guide", "kind": "guide",
"help_contexts": [ "help_contexts": [
"postbox.inbox.directory", "postbox.inbox.directory",
"postbox.action.delete-grouping", "postbox.action.delete-grouping",
"postbox.admin.templates", "postbox.admin.templates",
], ],
"privacy_notes": [ "privacy_notes": [
@@ -1048,5 +1094,10 @@ manifest = ModuleManifest(
) )
manifest = with_documentation_structured_translations(
manifest, locale="de", translations=GERMAN_STRUCTURED_TRANSLATIONS
)
def get_manifest() -> ModuleManifest: def get_manifest() -> ModuleManifest:
return manifest return manifest
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@govoplan/postbox-webui", "name": "@govoplan/postbox-webui",
"version": "0.1.19", "version": "0.1.22",
"private": true, "private": true,
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
@@ -1038,6 +1038,8 @@ function TemplateWorkspace({
</Button> </Button>
<Button <Button
onClick={onPublish} onClick={onPublish}
helpContextId="postbox.admin.templates"
helpModuleId="postbox"
disabled={busy || selected.status === "retired" || Boolean(revision.published_at)} disabled={busy || selected.status === "retired" || Boolean(revision.published_at)}
disabledReason={postboxBusyReason(false, busy) ?? (selected.status === "retired" ? POSTBOX_INTERFACE_I18N.retiredTemplate : revision.published_at ? POSTBOX_INTERFACE_I18N.publishedRevision : undefined)} disabledReason={postboxBusyReason(false, busy) ?? (selected.status === "retired" ? POSTBOX_INTERFACE_I18N.retiredTemplate : revision.published_at ? POSTBOX_INTERFACE_I18N.publishedRevision : undefined)}
> >
@@ -1678,7 +1680,7 @@ function TemplateDialog({
onChange={(require_expiry) => updateLinkedCopy({ require_expiry })} onChange={(require_expiry) => updateLinkedCopy({ require_expiry })}
/> />
</div> </div>
<FormField label="Maximum retention days" documentation={POSTBOX_FIELD_DOCUMENTATION}> <FormField label="Maximum retention days" documentation={POSTBOX_FIELD_DOCUMENTATION} helpContextId="postbox.field.retention" helpModuleId="postbox">
<input <input
type="number" type="number"
min={1} min={1}
@@ -1970,7 +1972,7 @@ function ProtectionConfigurationFields({
<small>{selected?.description}</small> <small>{selected?.description}</small>
</FormField> </FormField>
{profile === "server_envelope_v1" ? ( {profile === "server_envelope_v1" ? (
<FormField label="Encryption vault" documentation={POSTBOX_FIELD_DOCUMENTATION}> <FormField label="Encryption vault" documentation={POSTBOX_FIELD_DOCUMENTATION} helpContextId="postbox.field.protection-profile" helpModuleId="postbox">
<input <input
value={vaultId} value={vaultId}
disabled={profileLocked} disabled={profileLocked}
@@ -2410,7 +2412,7 @@ function ProtectionTransitionDialog({
<small>{selected?.description}</small> <small>{selected?.description}</small>
</FormField> </FormField>
{draft.target_profile === "server_envelope_v1" ? ( {draft.target_profile === "server_envelope_v1" ? (
<FormField label="Target encryption vault" documentation={POSTBOX_FIELD_DOCUMENTATION}> <FormField label="Target encryption vault" documentation={POSTBOX_FIELD_DOCUMENTATION} helpContextId="postbox.action.protection-transition" helpModuleId="postbox">
<input <input
value={draft.target_vault_id} value={draft.target_vault_id}
onChange={(event) => onChange({ ...draft, target_vault_id: event.target.value })} onChange={(event) => onChange({ ...draft, target_vault_id: event.target.value })}
@@ -2482,6 +2484,8 @@ function ProtectionTransitionDialog({
<div className="postbox-toggle-field"> <div className="postbox-toggle-field">
<ToggleSwitch <ToggleSwitch
label="Acknowledge residual disclosure" label="Acknowledge residual disclosure"
helpContextId="postbox.action.protection-transition"
helpModuleId="postbox"
help="Previously decrypted, copied, or exported content cannot be recalled by changing the storage profile." help="Previously decrypted, copied, or exported content cannot be recalled by changing the storage profile."
checked={draft.acknowledge_irreversibility} checked={draft.acknowledge_irreversibility}
onChange={(acknowledge_irreversibility) => onChange({ onChange={(acknowledge_irreversibility) => onChange({
@@ -997,6 +997,8 @@ export default function PostboxPage({
{groupingDraft.id ? ( {groupingDraft.id ? (
<Button <Button
variant="danger" variant="danger"
helpContextId="postbox.action.delete-grouping"
helpModuleId="postbox"
onClick={() => setDeleteGroupingTarget(groupingDraft)} onClick={() => setDeleteGroupingTarget(groupingDraft)}
disabled={busy} disabled={busy}
disabledReason={postboxBusyReason(false, busy)} disabledReason={postboxBusyReason(false, busy)}
@@ -1104,6 +1106,8 @@ export default function PostboxPage({
</Button> </Button>
<Button <Button
variant="primary" variant="primary"
helpContextId="postbox.inbox.messages"
helpModuleId="postbox"
onClick={() => void submitMessage()} onClick={() => void submitMessage()}
disabled={ disabled={
busy || busy ||
+3 -2
View File
@@ -1,6 +1,7 @@
import { createElement, lazy } from "react"; import { createElement, lazy } from "react";
import { import {
hasScope, hasScope,
messagesProductSurfaceTranslations,
type AdminSectionsUiCapability, type AdminSectionsUiCapability,
type DashboardWidgetsUiCapability, type DashboardWidgetsUiCapability,
type PlatformWebModule, type PlatformWebModule,
@@ -18,8 +19,8 @@ const PostboxAdminPanel = lazy(
); );
const translations = { const translations = {
en: generatedTranslations.en, en: { ...generatedTranslations.en, ...messagesProductSurfaceTranslations.en },
de: generatedTranslations.de de: { ...generatedTranslations.de, ...messagesProductSurfaceTranslations.de }
}; };
const readScope = ["postbox:postbox:read"]; const readScope = ["postbox:postbox:read"];