feat(manager): license banners for offline licensing (KI-006)
Expiry reminders from 14 days (was 5) with a 'connect the server to the internet to renew' hint while offline; banners for a clock set back and for a not-yet-verified license; a quiet note after 7+ days without cloud contact stating the system keeps working until the license date. Grace period banner respects grace_over. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -15,11 +15,18 @@ import client from '../api/client'
|
||||
* grace_expires_at string | null (ISO)
|
||||
* grace_days_remaining number | null
|
||||
* sync_failed bool
|
||||
* offline_days number | null days since the last successful cloud contact
|
||||
* license_problem "clock" | "unverified" | "expired" | "inactive" | null
|
||||
* grace_over bool
|
||||
*
|
||||
* The site enforces its signed license offline (KI-006): it keeps running
|
||||
* until expiry however long it has no internet — offline_days is informational.
|
||||
*
|
||||
* Derived helpers:
|
||||
* showExpiryWarning bool — true when 0 < days_until_expiry <= 5
|
||||
* showExpiryWarning bool — true when 0 <= days_until_expiry <= 14
|
||||
* inGracePeriod bool — true when expired but grace not yet over
|
||||
* isBlocked bool — locked=true OR (unlicensed AND grace over)
|
||||
* isBlocked bool — locked=true OR unlicensed
|
||||
* longOffline bool — no cloud contact for 7+ days (renewal needs it)
|
||||
*/
|
||||
export default function useLicenseStatus() {
|
||||
const { data } = useQuery({
|
||||
@@ -41,9 +48,13 @@ export default function useLicenseStatus() {
|
||||
grace_expires_at: null,
|
||||
grace_days_remaining: null,
|
||||
sync_failed: false,
|
||||
offline_days: null,
|
||||
license_problem: null,
|
||||
grace_over: false,
|
||||
showExpiryWarning: false,
|
||||
inGracePeriod: false,
|
||||
isBlocked: false,
|
||||
longOffline: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,16 +68,21 @@ export default function useLicenseStatus() {
|
||||
grace_expires_at = null,
|
||||
grace_days_remaining = null,
|
||||
sync_failed = false,
|
||||
offline_days = null,
|
||||
license_problem = null,
|
||||
grace_over = false,
|
||||
} = data
|
||||
|
||||
const showExpiryWarning =
|
||||
days_until_expiry != null && days_until_expiry >= 0 && days_until_expiry <= 5
|
||||
days_until_expiry != null && days_until_expiry >= 0 && days_until_expiry <= 14
|
||||
|
||||
const inGracePeriod =
|
||||
days_until_expiry != null && days_until_expiry < 0 && licensed
|
||||
days_until_expiry != null && days_until_expiry < 0 && licensed && !grace_over
|
||||
|
||||
const isBlocked = locked || !licensed
|
||||
|
||||
const longOffline = offline_days != null && offline_days >= 7
|
||||
|
||||
return {
|
||||
licensed,
|
||||
locked,
|
||||
@@ -77,8 +93,12 @@ export default function useLicenseStatus() {
|
||||
grace_expires_at,
|
||||
grace_days_remaining,
|
||||
sync_failed,
|
||||
offline_days,
|
||||
license_problem,
|
||||
grace_over,
|
||||
showExpiryWarning,
|
||||
inGracePeriod,
|
||||
isBlocked,
|
||||
longOffline,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,7 +83,10 @@ function PageTitle() {
|
||||
|
||||
function LicenseBanner({ license }) {
|
||||
const { lock_reason, locked, lock_pending, days_until_expiry, expires_at,
|
||||
grace_days_remaining, showExpiryWarning, inGracePeriod, isBlocked } = license
|
||||
grace_days_remaining, showExpiryWarning, inGracePeriod, isBlocked,
|
||||
sync_failed, offline_days, longOffline } = license
|
||||
// Renewal needs the cloud: say so whenever the site can't reach it right now
|
||||
const connectHint = sync_failed ? ' Συνδέστε τον server στο internet για να ανανεωθεί.' : ''
|
||||
|
||||
function fmtDate(iso) {
|
||||
if (!iso) return ''
|
||||
@@ -103,12 +106,30 @@ function LicenseBanner({ license }) {
|
||||
)
|
||||
}
|
||||
|
||||
if (lock_reason === 'clock') {
|
||||
return (
|
||||
<div className="flex items-center gap-2 px-4 py-2 bg-red-600 text-white text-[13px] font-medium">
|
||||
<ShieldAlert className="h-4 w-4 shrink-0" />
|
||||
Η ώρα του server έχει γυρίσει πίσω. Διορθώστε την ημερομηνία/ώρα του server ή συνδέστε τον στο internet.
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (lock_reason === 'unverified') {
|
||||
return (
|
||||
<div className="flex items-center gap-2 px-4 py-2 bg-red-600 text-white text-[13px] font-medium">
|
||||
<ShieldAlert className="h-4 w-4 shrink-0" />
|
||||
Δεν υπάρχει επιβεβαιωμένη άδεια χρήσης. Συνδέστε τον server στο internet για να ενεργοποιηθεί.
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (isBlocked && lock_reason === 'expired') {
|
||||
const daysAgo = days_until_expiry != null ? Math.abs(days_until_expiry) : '?'
|
||||
return (
|
||||
<div className="flex items-center gap-2 px-4 py-2 bg-red-600 text-white text-[13px] font-medium">
|
||||
<ShieldAlert className="h-4 w-4 shrink-0" />
|
||||
Η άδεια χρήσης έληξε πριν {daysAgo} {daysAgo === 1 ? 'μέρα' : 'μέρες'} ({fmtDate(expires_at)}). Ανανεώστε την άδεια ή επικοινωνήστε με την υποστήριξη.
|
||||
Η άδεια χρήσης έληξε πριν {daysAgo} {daysAgo === 1 ? 'μέρα' : 'μέρες'} ({fmtDate(expires_at)}). Ανανεώστε την άδεια ή επικοινωνήστε με την υποστήριξη.{connectHint}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -119,7 +140,7 @@ function LicenseBanner({ license }) {
|
||||
return (
|
||||
<div className="flex items-center gap-2 px-4 py-2 bg-orange-500 text-white text-[13px] font-medium">
|
||||
<AlertTriangle className="h-4 w-4 shrink-0" />
|
||||
Η άδεια χρήσης έληξε στις {fmtDate(expires_at)} (πριν {daysAgo} {daysAgo === 1 ? 'μέρα' : 'μέρες'}). Απομένουν {remaining} {remaining === 1 ? 'μέρα' : 'μέρες'} περιόδου χάριτος. Ανανεώστε την άδεια για να αποφύγετε το κλείδωμα.
|
||||
Η άδεια χρήσης έληξε στις {fmtDate(expires_at)} (πριν {daysAgo} {daysAgo === 1 ? 'μέρα' : 'μέρες'}). Απομένουν {remaining} {remaining === 1 ? 'μέρα' : 'μέρες'} περιόδου χάριτος. Ανανεώστε την άδεια για να αποφύγετε το κλείδωμα.{connectHint}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -129,7 +150,17 @@ function LicenseBanner({ license }) {
|
||||
return (
|
||||
<div className="flex items-center gap-2 px-4 py-2 bg-amber-50 border-b border-amber-200 text-amber-700 text-[13px] font-medium">
|
||||
<AlertTriangle className="h-4 w-4 shrink-0" />
|
||||
Η άδεια χρήσης λήγει σε {days} {days === 1 ? 'μέρα' : 'μέρες'} ({fmtDate(expires_at)}). Ανανεώστε έγκαιρα.
|
||||
Η άδεια χρήσης λήγει σε {days} {days === 1 ? 'μέρα' : 'μέρες'} ({fmtDate(expires_at)}). Ανανεώστε έγκαιρα.{connectHint}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Informational: the license keeps working offline, but renewal needs the cloud
|
||||
if (longOffline) {
|
||||
return (
|
||||
<div className="flex items-center gap-2 px-4 py-2 bg-slate-50 border-b border-slate-200 text-slate-600 text-[13px] font-medium">
|
||||
<AlertTriangle className="h-4 w-4 shrink-0" />
|
||||
Χωρίς σύνδεση με το cloud εδώ και {offline_days} μέρες. Το σύστημα λειτουργεί κανονικά — η άδεια ισχύει έως {fmtDate(expires_at)}.
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user