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_expires_at string | null (ISO)
|
||||||
* grace_days_remaining number | null
|
* grace_days_remaining number | null
|
||||||
* sync_failed bool
|
* 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:
|
* 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
|
* 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() {
|
export default function useLicenseStatus() {
|
||||||
const { data } = useQuery({
|
const { data } = useQuery({
|
||||||
@@ -41,9 +48,13 @@ export default function useLicenseStatus() {
|
|||||||
grace_expires_at: null,
|
grace_expires_at: null,
|
||||||
grace_days_remaining: null,
|
grace_days_remaining: null,
|
||||||
sync_failed: false,
|
sync_failed: false,
|
||||||
|
offline_days: null,
|
||||||
|
license_problem: null,
|
||||||
|
grace_over: false,
|
||||||
showExpiryWarning: false,
|
showExpiryWarning: false,
|
||||||
inGracePeriod: false,
|
inGracePeriod: false,
|
||||||
isBlocked: false,
|
isBlocked: false,
|
||||||
|
longOffline: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,16 +68,21 @@ export default function useLicenseStatus() {
|
|||||||
grace_expires_at = null,
|
grace_expires_at = null,
|
||||||
grace_days_remaining = null,
|
grace_days_remaining = null,
|
||||||
sync_failed = false,
|
sync_failed = false,
|
||||||
|
offline_days = null,
|
||||||
|
license_problem = null,
|
||||||
|
grace_over = false,
|
||||||
} = data
|
} = data
|
||||||
|
|
||||||
const showExpiryWarning =
|
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 =
|
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 isBlocked = locked || !licensed
|
||||||
|
|
||||||
|
const longOffline = offline_days != null && offline_days >= 7
|
||||||
|
|
||||||
return {
|
return {
|
||||||
licensed,
|
licensed,
|
||||||
locked,
|
locked,
|
||||||
@@ -77,8 +93,12 @@ export default function useLicenseStatus() {
|
|||||||
grace_expires_at,
|
grace_expires_at,
|
||||||
grace_days_remaining,
|
grace_days_remaining,
|
||||||
sync_failed,
|
sync_failed,
|
||||||
|
offline_days,
|
||||||
|
license_problem,
|
||||||
|
grace_over,
|
||||||
showExpiryWarning,
|
showExpiryWarning,
|
||||||
inGracePeriod,
|
inGracePeriod,
|
||||||
isBlocked,
|
isBlocked,
|
||||||
|
longOffline,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -83,7 +83,10 @@ function PageTitle() {
|
|||||||
|
|
||||||
function LicenseBanner({ license }) {
|
function LicenseBanner({ license }) {
|
||||||
const { lock_reason, locked, lock_pending, days_until_expiry, expires_at,
|
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) {
|
function fmtDate(iso) {
|
||||||
if (!iso) return ''
|
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') {
|
if (isBlocked && lock_reason === 'expired') {
|
||||||
const daysAgo = days_until_expiry != null ? Math.abs(days_until_expiry) : '?'
|
const daysAgo = days_until_expiry != null ? Math.abs(days_until_expiry) : '?'
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-2 px-4 py-2 bg-red-600 text-white text-[13px] font-medium">
|
<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" />
|
<ShieldAlert className="h-4 w-4 shrink-0" />
|
||||||
Η άδεια χρήσης έληξε πριν {daysAgo} {daysAgo === 1 ? 'μέρα' : 'μέρες'} ({fmtDate(expires_at)}). Ανανεώστε την άδεια ή επικοινωνήστε με την υποστήριξη.
|
Η άδεια χρήσης έληξε πριν {daysAgo} {daysAgo === 1 ? 'μέρα' : 'μέρες'} ({fmtDate(expires_at)}). Ανανεώστε την άδεια ή επικοινωνήστε με την υποστήριξη.{connectHint}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -119,7 +140,7 @@ function LicenseBanner({ license }) {
|
|||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-2 px-4 py-2 bg-orange-500 text-white text-[13px] font-medium">
|
<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" />
|
<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>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -129,7 +150,17 @@ function LicenseBanner({ license }) {
|
|||||||
return (
|
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">
|
<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" />
|
<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>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user