feat(license): add BSL license enforcement with device limit indicator

- Add LICENSE_DEVICES env var (default 250, matches BSL 1.1 free tier)
- Add /api/settings/license endpoint returning device count vs limit
- Header shows flashing red "502/500 licensed" badge when over limit
- About page shows license tier, device count, and over-limit warning
- Nothing is crippled — all features work regardless of device count
- Bump version to 9.7.1

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jason Staack
2026-03-19 22:28:56 -05:00
parent 0142107e68
commit fdc8d9cb68
9 changed files with 81 additions and 5 deletions

View File

@@ -1,8 +1,10 @@
import { createFileRoute } from '@tanstack/react-router'
import { useEffect, useRef, useState } from 'react'
import { useQuery } from '@tanstack/react-query'
import { RugLogo } from '@/components/brand/RugLogo'
import { APP_VERSION } from '@/lib/version'
import { AnsiNfoModal } from '@/components/about/AnsiNfoModal'
import { getLicenseStatus } from '@/lib/settingsApi'
export const Route = createFileRoute('/_authenticated/about')({
component: AboutPage,
@@ -483,6 +485,7 @@ function AboutPage() {
const [copied, setCopied] = useState(false)
const [showQR, setShowQR] = useState(false)
const [showNfo, setShowNfo] = useState(false)
const { data: license } = useQuery({ queryKey: ['license-status'], queryFn: getLicenseStatus })
const copyAddress = async () => {
try {
@@ -516,6 +519,28 @@ function AboutPage() {
</span>
</div>
{/* License */}
{license && (
<div className="rounded-lg border border-border bg-surface p-5 space-y-2">
<h2 className="text-sm font-semibold text-text-primary uppercase tracking-wider">
License
</h2>
<div className="flex items-center justify-between">
<span className="text-sm text-text-secondary">
{license.tier === 'commercial' ? 'Commercial License' : 'BSL 1.1 — Free Tier'}
</span>
<span className={`text-sm font-mono ${license.over_limit ? 'text-error' : 'text-text-secondary'}`}>
{license.actual_devices} / {license.licensed_devices === 0 ? 'Unlimited' : license.licensed_devices} devices
</span>
</div>
{license.over_limit && (
<p className="text-xs text-error">
Device count exceeds licensed limit. A commercial license is required.
</p>
)}
</div>
)}
{/* Features summary */}
<div className="rounded-lg border border-border bg-surface p-5 space-y-3">
<h2 className="text-sm font-semibold text-text-primary uppercase tracking-wider">