diff --git a/admin/app/controllers/easy_setup_controller.ts b/admin/app/controllers/easy_setup_controller.ts
new file mode 100644
index 0000000..4329106
--- /dev/null
+++ b/admin/app/controllers/easy_setup_controller.ts
@@ -0,0 +1,21 @@
+import { SystemService } from '#services/system_service'
+import { inject } from '@adonisjs/core'
+import type { HttpContext } from '@adonisjs/core/http'
+
+@inject()
+export default class EasySetupController {
+ constructor(private systemService: SystemService) {}
+
+ async index({ inertia }: HttpContext) {
+ const services = await this.systemService.getServices({ installedOnly: false })
+ return inertia.render('easy-setup/index', {
+ system: {
+ services: services,
+ },
+ })
+ }
+
+ async complete({ inertia }: HttpContext) {
+ return inertia.render('easy-setup/complete')
+ }
+}
diff --git a/admin/app/services/system_service.ts b/admin/app/services/system_service.ts
index 08fddf2..d47c5dc 100644
--- a/admin/app/services/system_service.ts
+++ b/admin/app/services/system_service.ts
@@ -85,6 +85,7 @@ export class SystemService {
friendly_name: service.friendly_name,
description: service.description,
installed: service.installed,
+ installation_status: service.installation_status,
status: status ? status.status : 'unknown',
ui_location: service.ui_location || '',
})
diff --git a/admin/inertia/components/ActiveDownloads.tsx b/admin/inertia/components/ActiveDownloads.tsx
new file mode 100644
index 0000000..5eb30f4
--- /dev/null
+++ b/admin/inertia/components/ActiveDownloads.tsx
@@ -0,0 +1,42 @@
+import useDownloads, { useDownloadsProps } from '~/hooks/useDownloads'
+import HorizontalBarChart from './HorizontalBarChart'
+import { extractFileName } from '~/lib/util'
+import StyledSectionHeader from './StyledSectionHeader'
+
+interface ActiveDownloadProps {
+ filetype?: useDownloadsProps['filetype']
+ withHeader?: boolean
+}
+
+const ActiveDownloads = ({ filetype, withHeader = false }: ActiveDownloadProps) => {
+ const { data: downloads } = useDownloads({ filetype })
+
+ return (
+ <>
+ {withHeader && }
+
+ {downloads && downloads.length > 0 ? (
+ downloads.map((download) => (
+
+
+
+ ))
+ ) : (
+
No active downloads
+ )}
+
+ >
+ )
+}
+
+export default ActiveDownloads
diff --git a/admin/inertia/components/Alert.tsx b/admin/inertia/components/Alert.tsx
index 0d41772..e6c0245 100644
--- a/admin/inertia/components/Alert.tsx
+++ b/admin/inertia/components/Alert.tsx
@@ -89,7 +89,7 @@ export default function Alert({
: type === 'success'
? 'bg-desert-olive text-desert-white border-desert-olive-dark'
: type === 'info'
- ? 'bg-desert-stone text-desert-white border-desert-stone-dark'
+ ? 'bg-desert-green text-desert-white border-desert-green-dark'
: ''
)
return classNames(baseStyles, 'shadow-sm', ...variantStyles)
@@ -102,7 +102,7 @@ export default function Alert({
: type === 'success'
? 'bg-desert-olive-lighter bg-opacity-20 border-desert-olive-light'
: type === 'info'
- ? 'bg-desert-stone-lighter bg-opacity-20 border-desert-stone-light'
+ ? 'bg-desert-green bg-opacity-20 border-desert-green-light'
: ''
)
return classNames(baseStyles, 'border shadow-sm', ...variantStyles)
diff --git a/admin/inertia/components/CuratedCollectionCard.tsx b/admin/inertia/components/CuratedCollectionCard.tsx
index 85e5722..22703f6 100644
--- a/admin/inertia/components/CuratedCollectionCard.tsx
+++ b/admin/inertia/components/CuratedCollectionCard.tsx
@@ -6,10 +6,11 @@ import { IconCircleCheck } from '@tabler/icons-react'
export interface CuratedCollectionCardProps {
collection: CuratedCollectionWithStatus
- onClick?: (collection: CuratedCollectionWithStatus) => void
+ onClick?: (collection: CuratedCollectionWithStatus) => void;
+ size?: 'small' | 'large'
}
-const CuratedCollectionCard: React.FC = ({ collection, onClick }) => {
+const CuratedCollectionCard: React.FC = ({ collection, onClick, size = 'small' }) => {
const totalSizeBytes = collection.resources?.reduce(
(acc, resource) => acc + resource.size_mb * 1024 * 1024,
0
@@ -18,7 +19,8 @@ const CuratedCollectionCard: React.FC = ({ collectio
{
if (collection.all_downloaded) {
diff --git a/admin/inertia/components/InstallActivityFeed.tsx b/admin/inertia/components/InstallActivityFeed.tsx
index d7a742d..ab7a9da 100644
--- a/admin/inertia/components/InstallActivityFeed.tsx
+++ b/admin/inertia/components/InstallActivityFeed.tsx
@@ -20,12 +20,13 @@ export type InstallActivityFeedProps = {
message: string
}>
className?: string
+ withHeader?: boolean
}
-const InstallActivityFeed: React.FC
= ({ activity, className }) => {
+const InstallActivityFeed: React.FC = ({ activity, className, withHeader = false }) => {
return (
-
Installation Activity
+ {withHeader &&
Installation Activity
}
{activity.map((activityItem, activityItemIdx) => (
-
diff --git a/admin/inertia/hooks/useServiceInstallationActivity.ts b/admin/inertia/hooks/useServiceInstallationActivity.ts
new file mode 100644
index 0000000..4586580
--- /dev/null
+++ b/admin/inertia/hooks/useServiceInstallationActivity.ts
@@ -0,0 +1,28 @@
+import { useEffect, useState } from 'react'
+import { useTransmit } from 'react-adonis-transmit'
+import { InstallActivityFeedProps } from '~/components/InstallActivityFeed'
+
+export default function useServiceInstallationActivity() {
+ const { subscribe } = useTransmit()
+ const [installActivity, setInstallActivity] = useState([])
+
+ useEffect(() => {
+ const unsubscribe = subscribe('service-installation', (data: any) => {
+ setInstallActivity((prev) => [
+ ...prev,
+ {
+ service_name: data.service_name ?? 'unknown',
+ type: data.status ?? 'unknown',
+ timestamp: new Date().toISOString(),
+ message: data.message ?? 'No message provided',
+ },
+ ])
+ })
+
+ return () => {
+ unsubscribe()
+ }
+ }, [])
+
+ return installActivity
+}
diff --git a/admin/inertia/pages/easy-setup/complete.tsx b/admin/inertia/pages/easy-setup/complete.tsx
new file mode 100644
index 0000000..bfc9942
--- /dev/null
+++ b/admin/inertia/pages/easy-setup/complete.tsx
@@ -0,0 +1,53 @@
+import { Head, router } from '@inertiajs/react'
+import AppLayout from '~/layouts/AppLayout'
+import StyledButton from '~/components/StyledButton'
+import Alert from '~/components/Alert'
+import useInternetStatus from '~/hooks/useInternetStatus'
+import useServiceInstallationActivity from '~/hooks/useServiceInstallationActivity'
+import InstallActivityFeed from '~/components/InstallActivityFeed'
+import ActiveDownloads from '~/components/ActiveDownloads'
+import StyledSectionHeader from '~/components/StyledSectionHeader'
+
+export default function EasySetupWizardComplete() {
+ const { isOnline } = useInternetStatus()
+ const installActivity = useServiceInstallationActivity()
+
+ return (
+
+
+ {!isOnline && (
+
+ )}
+
+
+
+
+
+
+
+
+ router.visit('/home')} icon="HomeIcon">
+ Go to Home
+
+
+
+
+
+
+ )
+}
diff --git a/admin/inertia/pages/easy-setup/index.tsx b/admin/inertia/pages/easy-setup/index.tsx
new file mode 100644
index 0000000..f420b76
--- /dev/null
+++ b/admin/inertia/pages/easy-setup/index.tsx
@@ -0,0 +1,550 @@
+import { Head, router } from '@inertiajs/react'
+import { useQuery } from '@tanstack/react-query'
+import { useState, useMemo } from 'react'
+import AppLayout from '~/layouts/AppLayout'
+import StyledButton from '~/components/StyledButton'
+import api from '~/lib/api'
+import { ServiceSlim } from '../../../types/services'
+import CuratedCollectionCard from '~/components/CuratedCollectionCard'
+import LoadingSpinner from '~/components/LoadingSpinner'
+import Alert from '~/components/Alert'
+import { IconCheck } from '@tabler/icons-react'
+import { useNotifications } from '~/context/NotificationContext'
+import useInternetStatus from '~/hooks/useInternetStatus'
+import classNames from 'classnames'
+
+type WizardStep = 1 | 2 | 3 | 4
+
+export default function EasySetupWizard(props: { system: { services: ServiceSlim[] } }) {
+ const [currentStep, setCurrentStep] = useState(1)
+ const [selectedServices, setSelectedServices] = useState([])
+ const [selectedMapCollections, setSelectedMapCollections] = useState([])
+ const [selectedZimCollections, setSelectedZimCollections] = useState([])
+ const [isProcessing, setIsProcessing] = useState(false)
+
+ const { addNotification } = useNotifications()
+ const { isOnline } = useInternetStatus()
+
+ const anySelectionMade =
+ selectedServices.length > 0 ||
+ selectedMapCollections.length > 0 ||
+ selectedZimCollections.length > 0
+
+ const { data: mapCollections, isLoading: isLoadingMaps } = useQuery({
+ queryKey: ['curated-map-collections'],
+ queryFn: () => api.listCuratedMapCollections(),
+ refetchOnWindowFocus: false,
+ })
+
+ const { data: zimCollections, isLoading: isLoadingZims } = useQuery({
+ queryKey: ['curated-zim-collections'],
+ queryFn: () => api.listCuratedZimCollections(),
+ refetchOnWindowFocus: false,
+ })
+
+ const availableServices = useMemo(() => {
+ return props.system.services.filter((service) => !service.installed)
+ }, [props.system.services])
+
+ const toggleServiceSelection = (serviceName: string) => {
+ setSelectedServices((prev) =>
+ prev.includes(serviceName) ? prev.filter((s) => s !== serviceName) : [...prev, serviceName]
+ )
+ }
+
+ const toggleMapCollection = (slug: string) => {
+ setSelectedMapCollections((prev) =>
+ prev.includes(slug) ? prev.filter((s) => s !== slug) : [...prev, slug]
+ )
+ }
+
+ const toggleZimCollection = (slug: string) => {
+ setSelectedZimCollections((prev) =>
+ prev.includes(slug) ? prev.filter((s) => s !== slug) : [...prev, slug]
+ )
+ }
+
+ const canProceedToNextStep = () => {
+ if (!isOnline) return false // Must be online to proceed
+ if (currentStep === 1) return true // Can skip app installation
+ if (currentStep === 2) return true // Can skip map downloads
+ if (currentStep === 3) return true // Can skip ZIM downloads
+ return false
+ }
+
+ const handleNext = () => {
+ if (currentStep < 4) {
+ setCurrentStep((prev) => (prev + 1) as WizardStep)
+ }
+ }
+
+ const handleBack = () => {
+ if (currentStep > 1) {
+ setCurrentStep((prev) => (prev - 1) as WizardStep)
+ }
+ }
+
+ const handleFinish = async () => {
+ if (!isOnline) {
+ addNotification({
+ type: 'error',
+ message: 'You must have an internet connection to complete the setup.',
+ })
+ return
+ }
+
+ setIsProcessing(true)
+
+ try {
+ // All of these ops don't actually wait for completion, they just kick off the process, so we can run them in parallel without awaiting each one sequentially
+ // const installPromises = selectedServices.map((serviceName) => api.installService(serviceName))
+
+ // await Promise.all(installPromises)
+
+ // const downloadPromises = [
+ // ...selectedMapCollections.map((slug) => api.downloadMapCollection(slug)),
+ // ...selectedZimCollections.map((slug) => api.downloadZimCollection(slug)),
+ // ]
+
+ // await Promise.all(downloadPromises)
+
+ addNotification({
+ type: 'success',
+ message: 'Setup wizard completed! Your selections are being processed.',
+ })
+
+ // Wait a moment then redirect to completion page to show progress
+ setTimeout(() => {
+ router.visit('/easy-setup/complete')
+ }, 2000)
+ } catch (error) {
+ console.error('Error during setup:', error)
+ addNotification({
+ type: 'error',
+ message: 'An error occurred during setup. Some items may not have been processed.',
+ })
+ } finally {
+ setIsProcessing(false)
+ }
+ }
+
+ const renderStepIndicator = () => {
+ const steps = [
+ { number: 1, label: 'Apps' },
+ { number: 2, label: 'Maps' },
+ { number: 3, label: 'ZIM Files' },
+ { number: 4, label: 'Review' },
+ ]
+
+ return (
+
+ )
+ }
+
+ const renderStep1 = () => (
+
+
+
Choose Apps to Install
+
+ Select the applications you'd like to install. You can always add more later.
+
+
+ {availableServices.length === 0 ? (
+
+
All available apps are already installed!
+
router.visit('/settings/apps')}
+ >
+ Manage Apps
+
+
+ ) : (
+
+ {availableServices.map((service) => {
+ const selectedOrInstalled =
+ selectedServices.includes(service.service_name) ||
+ service.installed ||
+ service.installation_status === 'installing'
+
+ const installedOrInstalling =
+ service.installed || service.installation_status === 'installing'
+
+ return (
+
+ !installedOrInstalling && toggleServiceSelection(service.service_name)
+ }
+ className={classNames(
+ 'p-6 rounded-lg border-2 cursor-pointer transition-all',
+ selectedOrInstalled
+ ? 'border-desert-green bg-desert-green bg-opacity-10 shadow-md text-white'
+ : 'border-desert-stone-light bg-white hover:border-desert-green hover:shadow-sm',
+ installedOrInstalling ? 'opacity-50 cursor-not-allowed' : ''
+ )}
+ >
+
+
+
+ {service.friendly_name || service.service_name}
+
+
+ {service.description}
+
+
+
+ {selectedOrInstalled ? (
+
+ ) : (
+
+ )}
+
+
+
+ )
+ })}
+
+ )}
+
+ )
+
+ const renderStep2 = () => (
+
+
+
Choose Map Regions
+
+ Select map region collections to download for offline use. You can always download more
+ regions later.
+
+
+ {isLoadingMaps ? (
+
+
+
+ ) : mapCollections && mapCollections.length > 0 ? (
+
+ {mapCollections.map((collection) => (
+
+ isOnline && !collection.all_downloaded && toggleMapCollection(collection.slug)
+ }
+ className={classNames(
+ 'relative',
+ selectedMapCollections.includes(collection.slug) &&
+ 'ring-4 ring-desert-green rounded-lg',
+ collection.all_downloaded && 'opacity-75',
+ !isOnline && 'opacity-50 cursor-not-allowed'
+ )}
+ >
+
+ {selectedMapCollections.includes(collection.slug) && (
+
+
+
+ )}
+
+ ))}
+
+ ) : (
+
+
No map collections available at this time.
+
+ )}
+
+ )
+
+ const renderStep3 = () => (
+
+
+
Choose ZIM Files
+
+ Select ZIM file collections for offline knowledge. You can always download more later.
+
+
+ {isLoadingZims ? (
+
+
+
+ ) : zimCollections && zimCollections.length > 0 ? (
+
+ {zimCollections.map((collection) => (
+
+ isOnline && !collection.all_downloaded && toggleZimCollection(collection.slug)
+ }
+ className={classNames(
+ 'relative',
+ selectedZimCollections.includes(collection.slug) &&
+ 'ring-4 ring-desert-green rounded-lg',
+ collection.all_downloaded && 'opacity-75',
+ !isOnline && 'opacity-50 cursor-not-allowed'
+ )}
+ >
+
+ {selectedZimCollections.includes(collection.slug) && (
+
+
+
+ )}
+
+ ))}
+
+ ) : (
+
+
No ZIM collections available at this time.
+
+ )}
+
+ )
+
+ const renderStep4 = () => {
+ const hasSelections =
+ selectedServices.length > 0 ||
+ selectedMapCollections.length > 0 ||
+ selectedZimCollections.length > 0
+
+ return (
+
+
+
Review Your Selections
+
Review your choices before starting the setup process.
+
+
+ {!hasSelections ? (
+
+ ) : (
+
+ {selectedServices.length > 0 && (
+
+
+ Apps to Install ({selectedServices.length})
+
+
+ {selectedServices.map((serviceName) => {
+ const service = availableServices.find((s) => s.service_name === serviceName)
+ return (
+ -
+
+
+ {service?.friendly_name || serviceName}
+
+
+ )
+ })}
+
+
+ )}
+
+ {selectedMapCollections.length > 0 && (
+
+
+ Map Collections to Download ({selectedMapCollections.length})
+
+
+ {selectedMapCollections.map((slug) => {
+ const collection = mapCollections?.find((c) => c.slug === slug)
+ return (
+ -
+
+ {collection?.name || slug}
+
+ )
+ })}
+
+
+ )}
+
+ {selectedZimCollections.length > 0 && (
+
+
+ ZIM Collections to Download ({selectedZimCollections.length})
+
+
+ {selectedZimCollections.map((slug) => {
+ const collection = zimCollections?.find((c) => c.slug === slug)
+ return (
+ -
+
+ {collection?.name || slug}
+
+ )
+ })}
+
+
+ )}
+
+
+
+ )}
+
+ )
+ }
+
+ return (
+
+
+ {!isOnline && (
+
+ )}
+
+
+ {renderStepIndicator()}
+
+ {currentStep === 1 && renderStep1()}
+ {currentStep === 2 && renderStep2()}
+ {currentStep === 3 && renderStep3()}
+ {currentStep === 4 && renderStep4()}
+
+
+
+ {currentStep > 1 && (
+
+ Back
+
+ )}
+
+
+ {selectedServices.length} app{selectedServices.length !== 1 && 's'},{' '}
+ {selectedMapCollections.length} map collection
+ {selectedMapCollections.length !== 1 && 's'}, {selectedZimCollections.length} ZIM
+ collection{selectedZimCollections.length !== 1 && 's'} selected
+
+
+
+
+ router.visit('/home')}
+ disabled={isProcessing}
+ variant="outline"
+ >
+ Cancel & Go to Home
+
+
+ {currentStep < 4 ? (
+
+ Next
+
+ ) : (
+
+ Complete Setup
+
+ )}
+
+
+
+
+
+
+ )
+}
diff --git a/admin/inertia/pages/home.tsx b/admin/inertia/pages/home.tsx
index a4a294e..c32b7dd 100644
--- a/admin/inertia/pages/home.tsx
+++ b/admin/inertia/pages/home.tsx
@@ -1,10 +1,18 @@
-import { IconHelp, IconMapRoute, IconPlus, IconSettings, IconWifiOff } from '@tabler/icons-react'
+import { IconBolt, IconHelp, IconMapRoute, IconPlus, IconSettings, IconWifiOff } from '@tabler/icons-react'
import { Head } from '@inertiajs/react'
import BouncingLogo from '~/components/BouncingLogo'
import AppLayout from '~/layouts/AppLayout'
import { getServiceLink } from '~/lib/navigation'
const STATIC_ITEMS = [
+ {
+ label: 'Easy Setup',
+ to: '/easy-setup',
+ target: '',
+ description: "Not sure where to start? Use the setup wizard to quickly configure your N.O.M.A.D.!",
+ icon: ,
+ installed: true,
+ },
{
label: 'Install Apps',
to: '/settings/apps',
@@ -66,11 +74,11 @@ export default function Home(props: {
{item.icon}
{item.label}
-
{item.description}
+
{item.description}
))}
diff --git a/admin/inertia/pages/settings/apps.tsx b/admin/inertia/pages/settings/apps.tsx
index a79230d..3ffec47 100644
--- a/admin/inertia/pages/settings/apps.tsx
+++ b/admin/inertia/pages/settings/apps.tsx
@@ -8,41 +8,23 @@ import { useModals } from '~/context/ModalContext'
import StyledModal from '~/components/StyledModal'
import api from '~/lib/api'
import { useEffect, useState } from 'react'
-import InstallActivityFeed, { InstallActivityFeedProps } from '~/components/InstallActivityFeed'
-import { useTransmit } from 'react-adonis-transmit'
+import InstallActivityFeed from '~/components/InstallActivityFeed'
import LoadingSpinner from '~/components/LoadingSpinner'
import useErrorNotification from '~/hooks/useErrorNotification'
import useInternetStatus from '~/hooks/useInternetStatus'
+import useServiceInstallationActivity from '~/hooks/useServiceInstallationActivity'
import { ArrowDownTrayIcon } from '@heroicons/react/24/outline'
import { IconCheck } from '@tabler/icons-react'
export default function SettingsPage(props: { system: { services: ServiceSlim[] } }) {
const { openModal, closeAllModals } = useModals()
- const { subscribe } = useTransmit()
const { showError } = useErrorNotification()
const { isOnline } = useInternetStatus()
- const [installActivity, setInstallActivity] = useState([])
+ const installActivity = useServiceInstallationActivity()
+
const [isInstalling, setIsInstalling] = useState(false)
const [loading, setLoading] = useState(false)
- useEffect(() => {
- const unsubscribe = subscribe('service-installation', (data: any) => {
- setInstallActivity((prev) => [
- ...prev,
- {
- service_name: data.service_name ?? 'unknown',
- type: data.status ?? 'unknown',
- timestamp: new Date().toISOString(),
- message: data.message ?? 'No message provided',
- },
- ])
- })
-
- return () => {
- unsubscribe()
- }
- }, [])
-
useEffect(() => {
if (installActivity.length === 0) return
if (installActivity.some((activity) => activity.type === 'completed')) {
@@ -270,7 +252,7 @@ export default function SettingsPage(props: { system: { services: ServiceSlim[]
/>
)}
{installActivity.length > 0 && (
-
+
)}
diff --git a/admin/inertia/pages/settings/maps.tsx b/admin/inertia/pages/settings/maps.tsx
index 8f57cbe..dfe7e64 100644
--- a/admin/inertia/pages/settings/maps.tsx
+++ b/admin/inertia/pages/settings/maps.tsx
@@ -13,10 +13,9 @@ import DownloadURLModal from '~/components/DownloadURLModal'
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import useDownloads from '~/hooks/useDownloads'
import StyledSectionHeader from '~/components/StyledSectionHeader'
-import HorizontalBarChart from '~/components/HorizontalBarChart'
-import { extractFileName } from '~/lib/util'
import CuratedCollectionCard from '~/components/CuratedCollectionCard'
import { CuratedCollectionWithStatus } from '../../../types/downloads'
+import ActiveDownloads from '~/components/ActiveDownloads'
const CURATED_COLLECTIONS_KEY = 'curated-map-collections'
@@ -34,7 +33,7 @@ export default function MapsManager(props: {
refetchOnWindowFocus: false,
})
- const { data: downloads, invalidate: invalidateDownloads } = useDownloads({
+ const { invalidate: invalidateDownloads } = useDownloads({
filetype: 'map',
enabled: true,
})
@@ -242,28 +241,7 @@ export default function MapsManager(props: {
]}
data={props.maps.regionFiles || []}
/>
-
-
- {downloads && downloads.length > 0 ? (
- downloads.map((download) => (
-
-
-
- ))
- ) : (
-
No active downloads
- )}
-
+
diff --git a/admin/inertia/pages/settings/zim/remote-explorer.tsx b/admin/inertia/pages/settings/zim/remote-explorer.tsx
index cecac1c..20cd1d4 100644
--- a/admin/inertia/pages/settings/zim/remote-explorer.tsx
+++ b/admin/inertia/pages/settings/zim/remote-explorer.tsx
@@ -12,7 +12,7 @@ import StyledTable from '~/components/StyledTable'
import SettingsLayout from '~/layouts/SettingsLayout'
import { Head } from '@inertiajs/react'
import { ListRemoteZimFilesResponse, RemoteZimFileEntry } from '../../../../types/zim'
-import { extractFileName, formatBytes } from '~/lib/util'
+import { formatBytes } from '~/lib/util'
import StyledButton from '~/components/StyledButton'
import { useModals } from '~/context/ModalContext'
import StyledModal from '~/components/StyledModal'
@@ -27,7 +27,7 @@ import CuratedCollectionCard from '~/components/CuratedCollectionCard'
import StyledSectionHeader from '~/components/StyledSectionHeader'
import { CuratedCollectionWithStatus } from '../../../../types/downloads'
import useDownloads from '~/hooks/useDownloads'
-import HorizontalBarChart from '~/components/HorizontalBarChart'
+import ActiveDownloads from '~/components/ActiveDownloads'
const CURATED_COLLECTIONS_KEY = 'curated-zim-collections'
@@ -313,28 +313,7 @@ export default function ZimRemoteExplorer() {
compact
rowLines
/>
-
-
- {downloads && downloads.length > 0 ? (
- downloads.map((download) => (
-
-
-
- ))
- ) : (
-
No active downloads
- )}
-
+
diff --git a/admin/start/routes.ts b/admin/start/routes.ts
index 4dcbf45..29be319 100644
--- a/admin/start/routes.ts
+++ b/admin/start/routes.ts
@@ -8,6 +8,7 @@
*/
import DocsController from '#controllers/docs_controller'
import DownloadsController from '#controllers/downloads_controller'
+import EasySetupController from '#controllers/easy_setup_controller'
import HomeController from '#controllers/home_controller'
import MapsController from '#controllers/maps_controller'
import SettingsController from '#controllers/settings_controller'
@@ -22,6 +23,9 @@ router.get('/', [HomeController, 'index'])
router.get('/home', [HomeController, 'home'])
router.on('/about').renderInertia('about')
+router.get('/easy-setup', [EasySetupController, 'index'])
+router.get('/easy-setup/complete', [EasySetupController, 'complete'])
+
router
.group(() => {
router.get('/system', [SettingsController, 'system'])
diff --git a/admin/types/services.ts b/admin/types/services.ts
index 4b4bb70..a7de1d4 100644
--- a/admin/types/services.ts
+++ b/admin/types/services.ts
@@ -1,5 +1,13 @@
-import Service from "#models/service";
+import Service from '#models/service'
-
-export type ServiceStatus = 'unknown' | 'running' | 'stopped';
-export type ServiceSlim = Pick & { status?: ServiceStatus };
\ No newline at end of file
+export type ServiceStatus = 'unknown' | 'running' | 'stopped'
+export type ServiceSlim = Pick<
+ Service,
+ | 'id'
+ | 'service_name'
+ | 'installed'
+ | 'installation_status'
+ | 'ui_location'
+ | 'friendly_name'
+ | 'description'
+> & { status?: ServiceStatus }