From 5793fc2139e1aed3b349e626b11811a8388c8a79 Mon Sep 17 00:00:00 2001 From: Jake Turner Date: Wed, 14 Jan 2026 20:52:03 -0800 Subject: [PATCH] feat: [wip] easy setup wizard --- .../app/controllers/easy_setup_controller.ts | 21 + admin/app/services/system_service.ts | 1 + admin/inertia/components/ActiveDownloads.tsx | 42 ++ admin/inertia/components/Alert.tsx | 4 +- .../components/CuratedCollectionCard.tsx | 8 +- .../components/InstallActivityFeed.tsx | 5 +- .../hooks/useServiceInstallationActivity.ts | 28 + admin/inertia/pages/easy-setup/complete.tsx | 53 ++ admin/inertia/pages/easy-setup/index.tsx | 550 ++++++++++++++++++ admin/inertia/pages/home.tsx | 14 +- admin/inertia/pages/settings/apps.tsx | 28 +- admin/inertia/pages/settings/maps.tsx | 28 +- .../pages/settings/zim/remote-explorer.tsx | 27 +- admin/start/routes.ts | 4 + admin/types/services.ts | 16 +- 15 files changed, 743 insertions(+), 86 deletions(-) create mode 100644 admin/app/controllers/easy_setup_controller.ts create mode 100644 admin/inertia/components/ActiveDownloads.tsx create mode 100644 admin/inertia/hooks/useServiceInstallationActivity.ts create mode 100644 admin/inertia/pages/easy-setup/complete.tsx create mode 100644 admin/inertia/pages/easy-setup/index.tsx 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 }