-

+
- {tenantDomain}
- Customer Dashboard
+ {businessName || tenantDomain}
+ powered by Meshkee.app
-
Store domain: {tenantDomain}
-
{view === 'login' && (
<>
Welcome back
diff --git a/apps/super-admin/index.html b/apps/super-admin/index.html
index 1f72e73..9fec3de 100644
--- a/apps/super-admin/index.html
+++ b/apps/super-admin/index.html
@@ -2,7 +2,7 @@
-
+
diff --git a/apps/super-admin/public/favicon.png b/apps/super-admin/public/favicon.png
new file mode 100755
index 0000000..f80989a
Binary files /dev/null and b/apps/super-admin/public/favicon.png differ
diff --git a/apps/super-admin/public/favicon.svg b/apps/super-admin/public/favicon.svg
deleted file mode 100644
index 6893eb1..0000000
--- a/apps/super-admin/public/favicon.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/apps/super-admin/src/assets/meshkee-logo.png b/apps/super-admin/src/assets/meshkee-logo.png
old mode 100644
new mode 100755
index 9d12a5b..9b7eaf1
Binary files a/apps/super-admin/src/assets/meshkee-logo.png and b/apps/super-admin/src/assets/meshkee-logo.png differ
diff --git a/apps/super-admin/src/pages/BusinessesPage.module.css b/apps/super-admin/src/pages/BusinessesPage.module.css
index 3535d17..8997249 100644
--- a/apps/super-admin/src/pages/BusinessesPage.module.css
+++ b/apps/super-admin/src/pages/BusinessesPage.module.css
@@ -287,6 +287,14 @@
gap: 6px;
}
+.sslIcon {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ line-height: 0;
+ cursor: help;
+}
+
.sslOk {
color: #16a34a;
flex-shrink: 0;
@@ -319,12 +327,18 @@
transition: background 0.2s, color 0.2s;
}
-.controlBtn:hover {
+.controlBtn:hover:not(:disabled) {
background: rgba(var(--primary-rgb) / 0.1);
color: var(--primary);
}
-.controlBtnDanger:hover {
+.controlBtn:disabled {
+ opacity: 0.55;
+ cursor: not-allowed;
+ pointer-events: none;
+}
+
+.controlBtnDanger:hover:not(:disabled) {
background: rgba(239, 68, 68, 0.1);
color: #ef4444;
}
diff --git a/apps/super-admin/src/pages/BusinessesPage.tsx b/apps/super-admin/src/pages/BusinessesPage.tsx
index 82659b6..dfc3ea5 100644
--- a/apps/super-admin/src/pages/BusinessesPage.tsx
+++ b/apps/super-admin/src/pages/BusinessesPage.tsx
@@ -601,13 +601,25 @@ export function BusinessesPage() {
{b.domain}
{b.sslEnabled ? (
-
+
+
+
) : (
-
+ >
+
+
)}
) : (
diff --git a/apps/super-admin/src/pages/WebsitesPage.module.css b/apps/super-admin/src/pages/WebsitesPage.module.css
index c859571..7f1e44e 100644
--- a/apps/super-admin/src/pages/WebsitesPage.module.css
+++ b/apps/super-admin/src/pages/WebsitesPage.module.css
@@ -16,3 +16,41 @@
.inactiveRow {
opacity: 0.55;
}
+
+.spin {
+ display: block;
+ animation: spin 0.8s linear infinite;
+}
+
+@keyframes spin {
+ to {
+ transform: rotate(360deg);
+ }
+}
+
+.deployBusy {
+ opacity: 0.55;
+ cursor: not-allowed;
+ pointer-events: none;
+ color: var(--primary);
+}
+
+.deployCell {
+ display: flex;
+ flex-direction: column;
+ gap: 2px;
+ min-width: 9rem;
+}
+
+.deployStatus {
+ font-size: 12px;
+ font-weight: 600;
+}
+
+.deployOk {
+ color: #15803d;
+}
+
+.deployFail {
+ color: #b91c1c;
+}
diff --git a/apps/super-admin/src/pages/WebsitesPage.tsx b/apps/super-admin/src/pages/WebsitesPage.tsx
index 34c3e28..9819c4f 100644
--- a/apps/super-admin/src/pages/WebsitesPage.tsx
+++ b/apps/super-admin/src/pages/WebsitesPage.tsx
@@ -1,5 +1,16 @@
import { useEffect, useMemo, useState } from 'react'
-import { AlertTriangle, Lock, Pencil, RotateCcw, Search, Trash2, Unlock } from 'lucide-react'
+import { flushSync } from 'react-dom'
+import {
+ AlertTriangle,
+ Loader2,
+ Lock,
+ Pencil,
+ Rocket,
+ RotateCcw,
+ Search,
+ Trash2,
+ Unlock,
+} from 'lucide-react'
import { ConfirmDeleteModal } from '../components/ConfirmDeleteModal'
import { Modal } from '../components/Modal'
import { ToggleSwitch } from '../components/ToggleSwitch'
@@ -7,6 +18,7 @@ import type { DomainListItem, DomainsListResponse } from '../types/domain'
import type { ListDomainsParams } from '../services/domainService'
import { ApiError, isAbortError } from '../lib/api'
import {
+ deployDomain,
listDomains,
removeDomain,
setDomainActive,
@@ -51,6 +63,31 @@ function daysLeftClass(expiresAt: string | null) {
return styles.daysOk
}
+function formatDeployAt(iso: string | null) {
+ if (!iso) return '—'
+ const d = new Date(iso)
+ if (Number.isNaN(d.getTime())) return '—'
+ return d.toLocaleString(undefined, {
+ year: 'numeric',
+ month: 'short',
+ day: 'numeric',
+ hour: '2-digit',
+ minute: '2-digit',
+ })
+}
+
+function deployStatusLabel(status: DomainListItem['lastDeployStatus']) {
+ if (status === 'started') return 'Started'
+ if (status === 'failed') return 'Failed'
+ return null
+}
+
+function deployStatusClass(status: DomainListItem['lastDeployStatus']) {
+ if (status === 'started') return styles.deployOk
+ if (status === 'failed') return styles.deployFail
+ return undefined
+}
+
export function WebsitesPage() {
const { showToast } = useToast()
const [data, setData] = useState
(null)
@@ -70,6 +107,7 @@ export function WebsitesPage() {
const [removeTarget, setRemoveTarget] = useState(null)
const [togglingActiveId, setTogglingActiveId] = useState(null)
const [togglingSslId, setTogglingSslId] = useState(null)
+ const [deployingId, setDeployingId] = useState(null)
useEffect(() => {
const controller = new AbortController()
@@ -229,6 +267,59 @@ export function WebsitesPage() {
}
}
+ async function handleDeploy(domain: DomainListItem) {
+ if (!domain.deploySlug) return
+ if (deployingId != null) return
+
+ // Paint spinner + disabled state before the network call starts
+ flushSync(() => {
+ setDeployingId(domain.id)
+ })
+ setError('')
+ showToast(`Deploying "${domain.host}"…`, 'info')
+
+ try {
+ const result = await deployDomain(domain.id)
+ setData((prev) => {
+ if (!prev) return prev
+ return {
+ ...prev,
+ items: prev.items.map((item) =>
+ item.id === domain.id
+ ? {
+ ...item,
+ lastDeployedAt: result.lastDeployedAt,
+ lastDeployStatus: result.lastDeployStatus,
+ }
+ : item,
+ ),
+ }
+ })
+ showToast(`Deploy started for "${result.host}".`, 'success')
+ } catch (err) {
+ const message = err instanceof ApiError ? err.message : 'Deploy failed to start.'
+ setError(message)
+ setData((prev) => {
+ if (!prev) return prev
+ return {
+ ...prev,
+ items: prev.items.map((item) =>
+ item.id === domain.id
+ ? {
+ ...item,
+ lastDeployedAt: new Date().toISOString(),
+ lastDeployStatus: 'failed',
+ }
+ : item,
+ ),
+ }
+ })
+ showToast(message, 'error')
+ } finally {
+ setDeployingId(null)
+ }
+ }
+
async function confirmRemove() {
if (!removeTarget) return
setError('')
@@ -330,13 +421,14 @@ export function WebsitesPage() {
Owner business |
Days to expire |
SSL |
+ Latest deploy |
Actions |
{loading && (
- |
+ |
Loading...
|
@@ -344,7 +436,7 @@ export function WebsitesPage() {
{!loading && data?.items?.length === 0 && (
- |
+ |
No results found.
|
@@ -366,19 +458,49 @@ export function WebsitesPage() {
{domain.sslEnabled ? (
-
+
+
+
) : (
-
+ >
+
+
)}
{domain.sslEnabled ? 'Enabled' : 'Disabled'}
|
+
+ {domain.lastDeployedAt ? (
+
+
+ {formatDeployAt(domain.lastDeployedAt)}
+
+ {deployStatusLabel(domain.lastDeployStatus) ? (
+
+ {deployStatusLabel(domain.lastDeployStatus)}
+
+ ) : null}
+
+ ) : (
+ —
+ )}
+ |
@@ -389,6 +511,32 @@ export function WebsitesPage() {
onChange={(isActive) => void handleToggleActive(domain, isActive)}
/>
+ {domain.deploySlug ? (
+
+ ) : null}
|