+ {/* Stripe owns the expiry for subscription licenses —
+ editing it here would be overwritten on renewal. */}
+ {lic.payment_provider !== "stripe" && (
+
+ )}
+
+ {/* An expired license stays dead no matter what date
+ is set — assertUsable short-circuits on the status
+ before it ever reads valid_until. Say so, or the
+ admin walks away thinking the edit revived it. */}
+ {lic.status === "expired" && (
+
{t("licenses.validUntilExpiredHint")}
+ )}
+
+ )}
{lic.payment_provider && (
diff --git a/web/src/pages/admin/webhooks.tsx b/web/src/pages/admin/webhooks.tsx
index 8f9c9c6..bacb80e 100644
--- a/web/src/pages/admin/webhooks.tsx
+++ b/web/src/pages/admin/webhooks.tsx
@@ -46,12 +46,22 @@ import { useI18n } from "@/i18n"
import { admin, type WebhookConfig } from "@/lib/api"
import { boolColor, formatDate } from "@/lib/utils"
+// Must stay in sync with the events the backend actually dispatches —
+// Dispatch only delivers to webhooks subscribed to the event, so an
+// event missing from this list can never be subscribed to and is
+// effectively undeliverable for anyone configuring from the dashboard.
const WEBHOOK_EVENTS = [
"license.created",
+ "license.activated",
+ "license.deactivated",
+ "license.expiry_changed",
+ "license.expired",
"license.canceled",
"license.suspended",
"license.reinstated",
"license.revoked",
+ "license.payment_failed",
+ "license.payment_recovered",
"quota.warning",
"quota.exceeded",
"seat.added",