- {t('app.cutoverDrawer.repoint', 'Repoint')} {domain} {t('app.cutoverDrawer.atTheBoxWhereTheImported', 'at the server where the imported site now lives. Its current records are snapshotted first, so the switch can always be reverted.')}
-
-
-
-
- setTarget(e.target.value)}
- placeholder="203.0.113.10"
- disabled={busy}
- />
- {t('app.cutoverDrawer.theIpOrHostnameForCname', 'The IP (or hostname for CNAME) the records should point to.')}
-
-
-
-
- setZoneId(e.target.value)}
- placeholder={t('app.cutoverDrawer.theDnsProviderSZoneIdentifier', 'the DNS provider\'s zone identifier')}
- disabled={busy}
- />
- {noZone && (
-
- {t('app.cutoverDrawer.thisDomainHasNoConnectedProvider', 'This domain has no connected provider zone. Cutover needs one to read and write records.')}
-
- )}
-
- {t('app.cutoverDrawer.publicResolversCacheTheOldAddress', 'Public resolvers cache the old address until their TTL expires. Check where the new address has taken effect.')}
-
- {t('app.cutoverDrawer.oneClickRestoresTheSnapshotCaptured', 'One click restores the snapshot captured before this cutover, including deleting any records the cutover created.')}
-
- )}
-
-
-
-
-
-
-
-
- )}
-
- setConfirmOpen(false)}
- />
-
-
- );
-};
-
-export default CutoverDrawer;
diff --git a/frontend/src/components/marketplace/SignatureBadge.jsx b/frontend/src/components/marketplace/SignatureBadge.jsx
new file mode 100644
index 00000000..3db05977
--- /dev/null
+++ b/frontend/src/components/marketplace/SignatureBadge.jsx
@@ -0,0 +1,55 @@
+import { ShieldAlert, ShieldCheck } from 'lucide-react';
+import { useTranslation } from 'react-i18next';
+import { Badge } from '@/components/ui/badge';
+
+// Release-signature verdict shared by the registry detail modal and the manual
+// install consent card. `signature.status`: 'verified' (ed25519 signature
+// checked against a publisher key this panel pins), 'untrusted_key',
+// 'invalid', or anything else = unsigned.
+export default function SignatureBadge({ signature, className }) {
+ const { t } = useTranslation();
+ const status = signature?.status || 'unsigned';
+ const signer = signature?.publisher || signature?.key_id;
+ if (status === 'verified') {
+ return (
+
+
+ {t('app.signatureBadge.signed', 'Signed')}{signer ? ` · ${signer}` : ''}
+
+ );
+ }
+ if (status === 'untrusted_key') {
+ return (
+
+
+ {t('app.signatureBadge.unknownSigner', 'Unknown signer')}
+
+ );
+ }
+ if (status === 'invalid') {
+ return (
+
+
+ {t('app.signatureBadge.invalid', 'Signature invalid')}
+
+ );
+ }
+ return (
+
+
+ {t('app.signatureBadge.unsigned', 'Unsigned')}
+
+ );
+}
diff --git a/frontend/src/components/settings/MemberActionsCard.jsx b/frontend/src/components/settings/MemberActionsCard.jsx
deleted file mode 100644
index 11fd2f6e..00000000
--- a/frontend/src/components/settings/MemberActionsCard.jsx
+++ /dev/null
@@ -1,79 +0,0 @@
-import { useEffect, useState } from 'react';
-import api from '../../services/api';
-import { Button } from '@/components/ui/button';
-import { useTranslation } from 'react-i18next';
-
-// Admin control for the member-action escape hatch.
-// Curated, parameterized actions a workspace member may run for an app they can
-// reach (no free text ever reaches a shell; every run is audit-logged). On by
-// default — this surface just makes the flag visible so an operator can turn the
-// whole thing off. The templates are deliberately conservative, so the default
-// stays ON.
-
-const MemberActionsCard = () => {
- const { t } = useTranslation();
- const [enabled, setEnabled] = useState(true);
- const [loading, setLoading] = useState(true);
- const [saving, setSaving] = useState(false);
- const [message, setMessage] = useState(null);
-
- useEffect(() => {
- api.getSystemSettings()
- .then((data) => {
- if (data.security_member_actions_enabled != null) {
- setEnabled(Boolean(data.security_member_actions_enabled));
- }
- })
- .catch(() => { /* leave default */ })
- .finally(() => setLoading(false));
- }, []);
-
- async function handleSave() {
- setSaving(true);
- setMessage(null);
- try {
- await api.updateSystemSettings({ security_member_actions_enabled: enabled });
- setMessage({ type: 'success', text: 'Member actions setting saved.' });
- } catch (err) {
- setMessage({ type: 'error', text: err.message || 'Failed to save setting' });
- } finally {
- setSaving(false);
- setTimeout(() => setMessage(null), 6000);
- }
- }
-
- if (loading) return null;
-
- return (
-
- {t('app.memberActionsCard.letWorkspaceMembersRunASmall', "Let workspace members run a small set of curated, parameterized actions (like adjusting a service's backup frequency) for services they can reach. Every action is validated against a fixed schema and audit-logged, so no free text ever reaches the server shell. Turn this off to disable the whole member-action surface.")}
-
-
-
-
-
-
-
-
- {message && (
-
- {message.text}
-
- )}
-
-
- );
-};
-
-export default MemberActionsCard;
diff --git a/frontend/src/i18n/locales/en.json b/frontend/src/i18n/locales/en.json
index 642f0d12..b546f613 100644
--- a/frontend/src/i18n/locales/en.json
+++ b/frontend/src/i18n/locales/en.json
@@ -228,10 +228,6 @@
"viewRelease": "View release",
"youReUpToDate": "You're up to date"
},
- "accountSecurityNudge": {
- "addAPasskeyOr2fa": "Add a passkey or 2FA",
- "secureYourAccount": "Secure your account"
- },
"activityTab": {
"actionType": "Action type",
"actionsThisWeek": "actions this week",
@@ -1654,56 +1650,6 @@
"scheduleIsRequired": "Schedule is required",
"serverOffline": "Server offline"
},
- "cutoverDrawer": {
- "answer": "Answer",
- "applyCutover": "Apply cutover",
- "applyDnsCutover": "Apply DNS cutover?",
- "applying": "Applying…",
- "atTheBoxWhereTheImported": "at the server where the imported site now lives. Its current records are snapshotted first, so the switch can always be reverted.",
- "capturing": "Capturing…",
- "checkPropagation": "Check propagation",
- "cutoverApplied": "Cutover applied:",
- "cutoverAppliedNowPointsAt": "Cutover applied. {{domain}} now points at {{value}}",
- "cutoverFinishedWithErrorsReviewBelow": "Cutover finished with errors. Review below and revert if needed.",
- "cutoverFinishedWithErrorsReviewThe": "Cutover finished with errors. Review the results and consider reverting.",
- "deletedCreatedRecords_one": "Deleted 1 record the cutover created.",
- "deletedCreatedRecords_other": "Deleted {{count}} records the cutover created.",
- "dnsCutover": "DNS cutover",
- "matchesNewTarget": "Matches new target",
- "new": "New",
- "newAddressTarget": "New address (target)",
- "newTarget": "new target",
- "noChangesToApplyForThe": "No changes to apply for the selected record types.",
- "notFullyPropagated": "Not fully propagated",
- "nowPointsAt": "now points at",
- "oneClickRestoresTheSnapshotCaptured": "One click restores the snapshot captured before this cutover, including deleting any records the cutover created.",
- "plannedChanges": "Planned changes",
- "propagated": "Propagated",
- "propagating": "Propagating",
- "propagation": "Propagation",
- "providerZoneId": "Provider zone id",
- "publicResolversCacheTheOldAddress": "Public resolvers cache the old address until their TTL expires. Check where the new address has taken effect.",
- "recordTypes": "Record types",
- "recordTypes2": "record types",
- "recordsCaptured": "records captured",
- "repoint": "Repoint",
- "repointsRecords_one": "This repoints the {{types}} record for {{domain}} at {{target}}. The snapshot lets you revert.",
- "repointsRecords_other": "This repoints the {{types}} records for {{domain}} at {{target}}. The snapshot lets you revert.",
- "resolver": "Resolver",
- "restoredRecords_one": "Restored 1 record.",
- "restoredRecords_other": "Restored {{count}} records.",
- "revertCutover": "Revert cutover",
- "revertFinishedWithErrorsCheckThe": "Revert finished with errors. Check the results.",
- "revertedRestoredSPreCutoverRecords": "Reverted: restored {{domain}}'s pre-cutover records",
- "reverting": "Reverting…",
- "review": "Review",
- "rollBack": "Roll back",
- "snapshotPreview": "Snapshot and preview",
- "theDnsProviderSZoneIdentifier": "the DNS provider's zone identifier",
- "theIpOrHostnameForCname": "The IP (or hostname for CNAME) the records should point to.",
- "thisDomainHasNoConnectedProvider": "This domain has no connected provider zone. Cutover needs one to read and write records.",
- "verify": "Verify"
- },
"dashGrid": {
"noWidgetsOnThisDashboardYet": "No widgets on this dashboard yet."
},
@@ -3617,15 +3563,11 @@
"noHostPermissionsDeclared": "No host permissions declared.",
"pasteAGithubRepoOwnerRepo": "Paste a GitHub repo (owner/repo or owner/repo@tag), a release URL, or a direct zip link. You'll preview what installs before it lands.",
"preview": "Preview",
- "signatureInvalid": "Signature invalid",
"signatureMismatch": "The signature does not match this archive.",
- "signed": "Signed ·",
- "signedUnknownPublisher": "Signed · unknown publisher",
"theDownloadMayHaveBeenTampered": "The download may have been tampered with. Do not install it.",
"thisExtensionRequests": "This extension requests:",
"thisReleaseCarriesNoPublisherSignature": "This release carries no publisher signature. It will run with full panel privileges, so install it only if you trust the source.",
"thisReleaseIsSignedButThe": "This release is signed, but the publisher key is not pinned by this panel, so the signature cannot be verified. Treat it as unsigned.",
- "unsigned": "Unsigned",
"uploadAnExtensionZipWith": "Upload an extension zip with",
"useAnAbsolutePathThatExists": "Use an absolute path that exists on the backend host or inside the backend container.",
"zip": "Zip"
@@ -3694,34 +3636,23 @@
"purgeData": "Delete data",
"registry": "Registry",
"registryInstallFailed": "Couldn't install from the registry.",
- "releaseZipIsSignedThePanel": "Release zip is signed; the panel verifies the ed25519 signature at install",
"removingThisExtensionStopsItsRoutes": "Uninstalling this extension stops its routes and UI contributions.",
"resetFilters": "Reset filters",
"reviewed": "Reviewed",
"screenshots": "Screenshots",
"searchExtensions": "Search extensions…",
- "signed": "Signed",
- "signedWithPublisherKeyWhichThis": "Signed with publisher key '{{publisherKeyId}}', which this panel does not pin",
"sourceRepo": "Source repo",
"thisExtensionDeclaresNoConfigurationFields": "This extension has no settings.",
"thisExtensionRequests": "This extension requests:",
- "thisReleaseCarriesNoPublisherSignature": "This release carries no publisher signature",
"uninstall": "Uninstall {{displayname}}?",
"uninstalling": "Uninstalling…",
- "unknownSigner": "Unknown signer",
"unreviewed": "Unreviewed",
- "unsigned": "Unsigned",
"update": "Update",
"updateAnyway": "Update anyway",
"updateAvailableV": "Update available → v",
"updating": "Updating…",
"versionByAuthor": "v{{version}} · by {{author}}"
},
- "memberActionsCard": {
- "allowMembersToRunCuratedActions": "Allow members to run curated actions",
- "letWorkspaceMembersRunASmall": "Let workspace members run a small set of curated, parameterized actions (like adjusting a service's backup frequency) for services they can reach. Every action is validated against a fixed schema and audit-logged, so no free text ever reaches the server shell. Turn this off to disable the whole member-action surface.",
- "memberActions": "Member actions"
- },
"message": {
"somethingWentWrong": "Something went wrong."
},
@@ -6414,6 +6345,15 @@
"viewProfiles": "View profiles",
"visibleCount": "{{visible}} of {{total}} visible"
},
+ "signatureBadge": {
+ "invalid": "Signature invalid",
+ "signed": "Signed",
+ "unknownSigner": "Unknown signer",
+ "unknownSignerHint": "Signed with publisher key '{{key}}', which this panel does not pin",
+ "unsigned": "Unsigned",
+ "unsignedHint": "This release carries no publisher signature",
+ "verifiedHint": "Release zip is signed; the panel verifies the ed25519 signature at install"
+ },
"siteSettingsTab": {
"addBaseDomain": "Add base domain",
"adding": "Adding…",
diff --git a/frontend/src/styles/components/_setup-health.scss b/frontend/src/styles/components/_setup-health.scss
index 20d5585e..11c6eb5d 100644
--- a/frontend/src/styles/components/_setup-health.scss
+++ b/frontend/src/styles/components/_setup-health.scss
@@ -107,50 +107,3 @@
&__summary { display: none; }
}
}
-
-// Per-user "secure your account" nudge banner (dashboard top).
-.account-security-nudge {
- display: flex;
- align-items: center;
- gap: $space-3;
- padding: $space-3 $space-4;
- margin-bottom: $space-4;
- border: 1px solid var(--warning-border, rgba(245, 179, 66, 0.35));
- border-radius: var(--radius, #{$radius-card});
- background: var(--warning-bg, rgba(245, 179, 66, 0.08));
-
- &__icon {
- color: $warning;
- flex-shrink: 0;
- }
-
- &__body {
- display: flex;
- flex-direction: column;
- gap: $space-hair;
- flex: 1;
- min-width: 0;
- }
-
- &__title {
- font-size: $text-sm;
- font-weight: $font-weight-semibold;
- color: $text-primary;
- }
-
- &__detail {
- font-size: $text-xs;
- color: $text-secondary;
- }
-
- &__close {
- border: 0;
- background: transparent;
- color: $text-tertiary;
- cursor: pointer;
- padding: $space-1;
- display: inline-flex;
-
- &:hover { color: $text-primary; }
- }
-}
diff --git a/frontend/src/styles/pages/_cutover.scss b/frontend/src/styles/pages/_cutover.scss
deleted file mode 100644
index 961f3f92..00000000
--- a/frontend/src/styles/pages/_cutover.scss
+++ /dev/null
@@ -1,266 +0,0 @@
-// ============================================
-// DNS CUTOVER (reversible per-domain repoint)
-// Slide-over launched from /domains and the Import wizard. Mirrors the
-// import-wizard idiom (stepper + callouts + compact tables).
-// ============================================
-
-.cutover {
- display: flex;
- flex-direction: column;
- gap: $space-5;
-
- h3 {
- display: flex;
- align-items: center;
- gap: $space-2;
- margin: 0 0 $space-3;
- font-size: $text-base;
- font-weight: $font-weight-semibold;
- color: $text-primary;
- }
-
- // --------------------------------------------
- // Stepper
- // --------------------------------------------
- &__stepper {
- display: flex;
- align-items: center;
- gap: $space-2;
- margin: 0;
- padding: 0;
- list-style: none;
- }
-
- &__step {
- display: flex;
- align-items: center;
- gap: $space-2;
- padding: $space-2 $space-3;
- border-radius: $radius-full;
- font-size: $text-xs;
- color: $text-tertiary;
- white-space: nowrap;
-
- &.is-active {
- @include status-variant($text-primary, $accent-bg);
-
- .cutover__step-dot {
- background: $accent-primary;
- border-color: $accent-primary;
- color: #fff;
- }
- }
-
- &.is-done {
- color: $text-secondary;
-
- .cutover__step-dot {
- color: $success;
- border-color: $success-border;
- }
- }
-
- & + &::before {
- content: '';
- width: 16px;
- height: 1px;
- margin-right: $space-2;
- background: $border-default;
- }
- }
-
- &__step-dot {
- display: inline-flex;
- align-items: center;
- justify-content: center;
- width: 20px;
- height: 20px; // geometry: stepper dot
- border: 1px solid $border-default;
- border-radius: $radius-full;
- font-size: $text-xs;
- font-weight: $font-weight-semibold;
- flex-shrink: 0;
- }
-
- // --------------------------------------------
- // Stage body
- // --------------------------------------------
- &__stage {
- display: flex;
- flex-direction: column;
- gap: $space-4;
- }
-
- &__lead {
- margin: 0;
- font-size: $text-xs;
- line-height: 1.6;
- color: $text-secondary;
-
- code {
- font-family: $font-mono;
- font-size: $text-xs;
- padding: 1px $space-tight;
- background: $bg-secondary;
- border-radius: $radius-badge;
- }
- }
-
- &__muted {
- margin: 0;
- color: $text-tertiary;
- font-size: $text-xs;
- }
-
- &__field {
- display: flex;
- flex-direction: column;
- gap: $space-2;
- }
-
- &__hint {
- font-size: $text-xs;
- color: $text-tertiary;
-
- &--warn {
- color: $warning;
- }
- }
-
- &__types {
- display: flex;
- gap: $space-3;
- flex-wrap: wrap;
- }
-
- &__type {
- display: inline-flex;
- align-items: center;
- gap: $space-2;
- padding: $space-2 $space-3;
- background: $bg-elevated;
- border: 1px solid $border-subtle;
- border-radius: $radius-control;
- font-size: $text-xs;
- color: $text-primary;
- cursor: pointer;
- }
-
- &__actions {
- display: flex;
- justify-content: space-between;
- align-items: center;
- gap: $space-3;
- margin-top: $space-2;
- }
-
- // --------------------------------------------
- // Review summary
- // --------------------------------------------
- &__summary {
- display: grid;
- grid-template-columns: repeat(3, minmax(0, 1fr));
- gap: $space-3;
- }
-
- &__stat {
- display: flex;
- flex-direction: column;
- gap: $space-hair;
- padding: $space-3 $space-4;
- background: $bg-elevated;
- border: 1px solid $border-subtle;
- border-radius: $radius-control;
- }
-
- &__stat-val {
- font-size: $text-base;
- font-weight: $font-weight-semibold;
- color: $text-primary;
- word-break: break-word;
- }
-
- &__stat-label {
- font-size: $text-xs;
- color: $text-tertiary;
- }
-
- // --------------------------------------------
- // Tables (ops diff + resolver rows)
- // --------------------------------------------
- &__table-wrap {
- margin-bottom: $space-3;
- }
-
- &__verify-head {
- display: flex;
- gap: $space-2;
- margin-bottom: $space-3;
- flex-wrap: wrap;
- }
-
- &__section--revert {
- padding-top: $space-4;
- border-top: 1px solid $border-subtle;
- }
-
- // --------------------------------------------
- // Callouts
- // --------------------------------------------
- &__callout {
- display: flex;
- gap: $space-3;
- padding: $space-3 $space-4;
- border-radius: $radius-control;
- font-size: $text-xs;
-
- svg {
- flex-shrink: 0;
- margin-top: $space-hair;
- }
-
- p {
- margin: 0;
-
- code {
- font-family: $font-mono;
- font-size: $text-xs;
- padding: 1px $space-tight;
- background: $bg-secondary;
- border-radius: $radius-badge;
- }
- }
-
- &--info {
- background: $accent-bg;
- border: 1px solid $accent-primary;
- color: $text-secondary;
-
- svg { color: $accent-primary; }
- }
-
- &--warning {
- background: $warning-bg;
- border: 1px solid $warning-border;
- color: $warning;
-
- p { color: $text-secondary; }
- }
-
- &--danger {
- background: $danger-bg;
- border: 1px solid $danger-border;
- color: $danger;
-
- p { color: $text-secondary; }
- }
-
- &--success {
- background: $success-bg;
- border: 1px solid $success-border;
- color: $success;
-
- p { color: $text-secondary; }
- }
- }
-}