mirror of
https://github.com/Crosstalk-Solutions/project-nomad.git
synced 2026-03-28 03:29:25 +01:00
feat: initial commit
This commit is contained in:
commit
b33a1b3e37
40
.gitignore
vendored
Normal file
40
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
# Logs
|
||||
logs
|
||||
*.log
|
||||
|
||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# dotenv environment variables file
|
||||
.env
|
||||
|
||||
# Build / Dist
|
||||
dist
|
||||
build
|
||||
tmp
|
||||
|
||||
# macOS Metafiles
|
||||
.DS_Store
|
||||
|
||||
# Fonts
|
||||
.ttf
|
||||
|
||||
# Runtime-generated Files
|
||||
server/public
|
||||
server/temp
|
||||
|
||||
# IDE Files
|
||||
.vscode
|
||||
.idea
|
||||
|
||||
# Frontend assets compiled code
|
||||
admin/public/assets
|
||||
22
admin/.editorconfig
Normal file
22
admin/.editorconfig
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
# http://editorconfig.org
|
||||
|
||||
[*]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
|
||||
[*.json]
|
||||
insert_final_newline = unset
|
||||
|
||||
[**.min.js]
|
||||
indent_style = unset
|
||||
insert_final_newline = unset
|
||||
|
||||
[MakeFile]
|
||||
indent_style = space
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
13
admin/.env.example
Normal file
13
admin/.env.example
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
PORT=8080
|
||||
HOST=localhost
|
||||
LOG_LEVEL=info
|
||||
APP_KEY=some_random_key
|
||||
NODE_ENV=development
|
||||
SESSION_DRIVER=cookie
|
||||
DRIVE_DISK=fs
|
||||
DB_HOST=localhost
|
||||
DB_PORT=3306
|
||||
DB_USER=root
|
||||
DB_DATABASE=nomad
|
||||
DB_PASSWORD=password
|
||||
DB_SSL=false
|
||||
32
admin/Dockerfile
Normal file
32
admin/Dockerfile
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
FROM node:22.16.0-alpine3.22 AS base
|
||||
|
||||
# Install bash & curl for entrypoint script compatibility
|
||||
RUN apk add --no-cache bash curl
|
||||
|
||||
# All deps stage
|
||||
FROM base AS deps
|
||||
WORKDIR /app
|
||||
ADD package.json package-lock.json ./
|
||||
RUN npm ci
|
||||
|
||||
# Production only deps stage
|
||||
FROM base AS production-deps
|
||||
WORKDIR /app
|
||||
ADD package.json package-lock.json ./
|
||||
RUN npm ci --omit=dev
|
||||
|
||||
# Build stage
|
||||
FROM base AS build
|
||||
WORKDIR /app
|
||||
COPY --from=deps /app/node_modules /app/node_modules
|
||||
ADD . .
|
||||
RUN node ace build
|
||||
|
||||
# Production stage
|
||||
FROM base
|
||||
ENV NODE_ENV=production
|
||||
WORKDIR /app
|
||||
COPY --from=production-deps /app/node_modules /app/node_modules
|
||||
COPY --from=build /app/build /app
|
||||
EXPOSE 8080
|
||||
CMD ["node", "./bin/server.js"]
|
||||
5
admin/README.md
Normal file
5
admin/README.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
|
||||
## Docker container
|
||||
```
|
||||
docker run --rm -it -p 8080:8080 jturnercosmistack/projectnomad:admin-latest -e PORT=8080 -e HOST=0.0.0.0 -e APP_KEY=secretlongpasswordsecret -e LOG_LEVEL=debug -e DRIVE_DISK=fs
|
||||
```
|
||||
27
admin/ace.js
Normal file
27
admin/ace.js
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| JavaScript entrypoint for running ace commands
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| DO NOT MODIFY THIS FILE AS IT WILL BE OVERRIDDEN DURING THE BUILD
|
||||
| PROCESS.
|
||||
|
|
||||
| See docs.adonisjs.com/guides/typescript-build-process#creating-production-build
|
||||
|
|
||||
| Since, we cannot run TypeScript source code using "node" binary, we need
|
||||
| a JavaScript entrypoint to run ace commands.
|
||||
|
|
||||
| This file registers the "ts-node/esm" hook with the Node.js module system
|
||||
| and then imports the "bin/console.ts" file.
|
||||
|
|
||||
*/
|
||||
|
||||
/**
|
||||
* Register hook to process TypeScript files using ts-node-maintained
|
||||
*/
|
||||
import 'ts-node-maintained/register/esm'
|
||||
|
||||
/**
|
||||
* Import ace console entrypoint
|
||||
*/
|
||||
await import('./bin/console.js')
|
||||
118
admin/adonisrc.ts
Normal file
118
admin/adonisrc.ts
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
import { defineConfig } from '@adonisjs/core/app'
|
||||
|
||||
export default defineConfig({
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Experimental flags
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The following features will be enabled by default in the next major release
|
||||
| of AdonisJS. You can opt into them today to avoid any breaking changes
|
||||
| during upgrade.
|
||||
|
|
||||
*/
|
||||
experimental: {
|
||||
mergeMultipartFieldsAndFiles: true,
|
||||
shutdownInReverseOrder: true,
|
||||
},
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Commands
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| List of ace commands to register from packages. The application commands
|
||||
| will be scanned automatically from the "./commands" directory.
|
||||
|
|
||||
*/
|
||||
commands: [() => import('@adonisjs/core/commands'), () => import('@adonisjs/lucid/commands')],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Service providers
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| List of service providers to import and register when booting the
|
||||
| application
|
||||
|
|
||||
*/
|
||||
providers: [
|
||||
() => import('@adonisjs/core/providers/app_provider'),
|
||||
() => import('@adonisjs/core/providers/hash_provider'),
|
||||
{
|
||||
file: () => import('@adonisjs/core/providers/repl_provider'),
|
||||
environment: ['repl', 'test'],
|
||||
},
|
||||
() => import('@adonisjs/core/providers/vinejs_provider'),
|
||||
() => import('@adonisjs/core/providers/edge_provider'),
|
||||
() => import('@adonisjs/session/session_provider'),
|
||||
() => import('@adonisjs/vite/vite_provider'),
|
||||
() => import('@adonisjs/shield/shield_provider'),
|
||||
() => import('@adonisjs/static/static_provider'),
|
||||
() => import('@adonisjs/cors/cors_provider'),
|
||||
() => import('@adonisjs/lucid/database_provider'),
|
||||
() => import('@adonisjs/inertia/inertia_provider'),
|
||||
() => import('@adonisjs/drive/drive_provider'),
|
||||
() => import('@adonisjs/transmit/transmit_provider')
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Preloads
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| List of modules to import before starting the application.
|
||||
|
|
||||
*/
|
||||
preloads: [() => import('#start/routes'), () => import('#start/kernel')],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Tests
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| List of test suites to organize tests by their type. Feel free to remove
|
||||
| and add additional suites.
|
||||
|
|
||||
*/
|
||||
tests: {
|
||||
suites: [
|
||||
{
|
||||
files: ['tests/unit/**/*.spec(.ts|.js)'],
|
||||
name: 'unit',
|
||||
timeout: 2000,
|
||||
},
|
||||
{
|
||||
files: ['tests/functional/**/*.spec(.ts|.js)'],
|
||||
name: 'functional',
|
||||
timeout: 30000,
|
||||
},
|
||||
],
|
||||
forceExit: false,
|
||||
},
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Metafiles
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| A collection of files you want to copy to the build folder when creating
|
||||
| the production build.
|
||||
|
|
||||
*/
|
||||
metaFiles: [
|
||||
{
|
||||
pattern: 'resources/views/**/*.edge',
|
||||
reloadServer: false,
|
||||
},
|
||||
{
|
||||
pattern: 'public/**',
|
||||
reloadServer: false,
|
||||
},
|
||||
],
|
||||
|
||||
assetsBundler: false,
|
||||
hooks: {
|
||||
onBuildStarting: [() => import('@adonisjs/vite/build_hook')],
|
||||
},
|
||||
})
|
||||
19
admin/app/controllers/home_controller.ts
Normal file
19
admin/app/controllers/home_controller.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import { SystemService } from '#services/system_service'
|
||||
import { inject } from '@adonisjs/core'
|
||||
import type { HttpContext } from '@adonisjs/core/http'
|
||||
|
||||
@inject()
|
||||
export default class HomeController {
|
||||
constructor(
|
||||
private systemService: SystemService,
|
||||
) { }
|
||||
|
||||
async index({ inertia }: HttpContext) {
|
||||
const services = await this.systemService.getServices();
|
||||
return inertia.render('home', {
|
||||
system: {
|
||||
services
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
29
admin/app/controllers/system_controller.ts
Normal file
29
admin/app/controllers/system_controller.ts
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
import { DockerService } from '#services/docker_service';
|
||||
import { SystemService } from '#services/system_service'
|
||||
import { installServiceValidator } from '#validators/system';
|
||||
import { inject } from '@adonisjs/core'
|
||||
import type { HttpContext } from '@adonisjs/core/http'
|
||||
|
||||
@inject()
|
||||
export default class SystemController {
|
||||
constructor(
|
||||
private systemService: SystemService,
|
||||
private dockerService: DockerService
|
||||
) { }
|
||||
|
||||
async getServices({ response }: HttpContext) {
|
||||
const services = await this.systemService.getServices();
|
||||
response.send(services);
|
||||
}
|
||||
|
||||
async installService({ request, response }: HttpContext) {
|
||||
const payload = await request.validateUsing(installServiceValidator);
|
||||
|
||||
const result = await this.dockerService.createContainerPreflight(payload.service_name);
|
||||
if (result.success) {
|
||||
response.send({ message: result.message });
|
||||
} else {
|
||||
response.status(400).send({ error: result.message });
|
||||
}
|
||||
}
|
||||
}
|
||||
45
admin/app/exceptions/handler.ts
Normal file
45
admin/app/exceptions/handler.ts
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
import app from '@adonisjs/core/services/app'
|
||||
import { HttpContext, ExceptionHandler } from '@adonisjs/core/http'
|
||||
import type { StatusPageRange, StatusPageRenderer } from '@adonisjs/core/types/http'
|
||||
|
||||
export default class HttpExceptionHandler extends ExceptionHandler {
|
||||
/**
|
||||
* In debug mode, the exception handler will display verbose errors
|
||||
* with pretty printed stack traces.
|
||||
*/
|
||||
protected debug = !app.inProduction
|
||||
|
||||
/**
|
||||
* Status pages are used to display a custom HTML pages for certain error
|
||||
* codes. You might want to enable them in production only, but feel
|
||||
* free to enable them in development as well.
|
||||
*/
|
||||
protected renderStatusPages = app.inProduction
|
||||
|
||||
/**
|
||||
* Status pages is a collection of error code range and a callback
|
||||
* to return the HTML contents to send as a response.
|
||||
*/
|
||||
protected statusPages: Record<StatusPageRange, StatusPageRenderer> = {
|
||||
'404': (error, { inertia }) => inertia.render('errors/not_found', { error }),
|
||||
'500..599': (error, { inertia }) => inertia.render('errors/server_error', { error }),
|
||||
}
|
||||
|
||||
/**
|
||||
* The method is used for handling errors and returning
|
||||
* response to the client
|
||||
*/
|
||||
async handle(error: unknown, ctx: HttpContext) {
|
||||
return super.handle(error, ctx)
|
||||
}
|
||||
|
||||
/**
|
||||
* The method is used to report error to the logging service or
|
||||
* the a third party error monitoring service.
|
||||
*
|
||||
* @note You should not attempt to send a response from this method.
|
||||
*/
|
||||
async report(error: unknown, ctx: HttpContext) {
|
||||
return super.report(error, ctx)
|
||||
}
|
||||
}
|
||||
19
admin/app/middleware/container_bindings_middleware.ts
Normal file
19
admin/app/middleware/container_bindings_middleware.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import { Logger } from '@adonisjs/core/logger'
|
||||
import { HttpContext } from '@adonisjs/core/http'
|
||||
import { NextFn } from '@adonisjs/core/types/http'
|
||||
|
||||
/**
|
||||
* The container bindings middleware binds classes to their request
|
||||
* specific value using the container resolver.
|
||||
*
|
||||
* - We bind "HttpContext" class to the "ctx" object
|
||||
* - And bind "Logger" class to the "ctx.logger" object
|
||||
*/
|
||||
export default class ContainerBindingsMiddleware {
|
||||
handle(ctx: HttpContext, next: NextFn) {
|
||||
ctx.containerResolver.bindValue(HttpContext, ctx)
|
||||
ctx.containerResolver.bindValue(Logger, ctx.logger)
|
||||
|
||||
return next()
|
||||
}
|
||||
}
|
||||
16
admin/app/middleware/force_json_response_middleware.ts
Normal file
16
admin/app/middleware/force_json_response_middleware.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import type { HttpContext } from '@adonisjs/core/http'
|
||||
import type { NextFn } from '@adonisjs/core/types/http'
|
||||
|
||||
/**
|
||||
* Updating the "Accept" header to always accept "application/json" response
|
||||
* from the server. This will force the internals of the framework like
|
||||
* validator errors or auth errors to return a JSON response.
|
||||
*/
|
||||
export default class ForceJsonResponseMiddleware {
|
||||
async handle({ request }: HttpContext, next: NextFn) {
|
||||
const headers = request.headers()
|
||||
headers.accept = 'application/json'
|
||||
|
||||
return next()
|
||||
}
|
||||
}
|
||||
36
admin/app/models/service.ts
Normal file
36
admin/app/models/service.ts
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
import { BaseModel, column, SnakeCaseNamingStrategy } from '@adonisjs/lucid/orm'
|
||||
import { DateTime } from 'luxon'
|
||||
|
||||
export default class Service extends BaseModel {
|
||||
static namingStrategy = new SnakeCaseNamingStrategy()
|
||||
|
||||
@column({ isPrimary: true })
|
||||
declare id: number
|
||||
|
||||
@column()
|
||||
declare service_name: string
|
||||
|
||||
@column()
|
||||
declare container_image: string
|
||||
|
||||
@column()
|
||||
declare container_command: string
|
||||
|
||||
@column()
|
||||
declare container_config: string | null
|
||||
|
||||
@column()
|
||||
declare installed: boolean
|
||||
|
||||
@column()
|
||||
declare ui_location: string
|
||||
|
||||
@column()
|
||||
declare metadata: string | null
|
||||
|
||||
@column.dateTime({ autoCreate: true })
|
||||
declare created_at: DateTime
|
||||
|
||||
@column.dateTime({ autoCreate: true, autoUpdate: true })
|
||||
declare updated_at: DateTime | null
|
||||
}
|
||||
212
admin/app/services/docker_service.ts
Normal file
212
admin/app/services/docker_service.ts
Normal file
|
|
@ -0,0 +1,212 @@
|
|||
import Service from "#models/service";
|
||||
import Docker from "dockerode";
|
||||
import drive from '@adonisjs/drive/services/main'
|
||||
import axios from 'axios';
|
||||
import logger from '@adonisjs/core/services/logger'
|
||||
import transmit from '@adonisjs/transmit/services/main'
|
||||
|
||||
export class DockerService {
|
||||
private docker: Docker;
|
||||
|
||||
constructor() {
|
||||
this.docker = new Docker({ socketPath: '/var/run/docker.sock' });
|
||||
}
|
||||
|
||||
async createContainerPreflight(serviceName: string): Promise<{ success: boolean; message: string }> {
|
||||
const service = await Service.findBy('service_name', serviceName);
|
||||
if (!service) {
|
||||
return {
|
||||
success: false,
|
||||
message: `Service ${serviceName} not found`,
|
||||
};
|
||||
}
|
||||
|
||||
if (service.installed) {
|
||||
return {
|
||||
success: false,
|
||||
message: `Service ${serviceName} is already installed`,
|
||||
};
|
||||
}
|
||||
|
||||
// Check if a service wasn't marked as installed but has an existing container
|
||||
// This can happen if the service was created but not properly installed
|
||||
// or if the container was removed manually without updating the service status.
|
||||
if (await this._checkIfServiceContainerExists(serviceName)) {
|
||||
const removeResult = await this._removeServiceContainer(serviceName);
|
||||
if (!removeResult.success) {
|
||||
return {
|
||||
success: false,
|
||||
message: `Failed to remove existing container for service ${serviceName}: ${removeResult.message}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Attempt to parse any special container configuration
|
||||
let containerConfig;
|
||||
if (service.container_config) {
|
||||
try {
|
||||
containerConfig = JSON.parse(JSON.stringify(service.container_config));
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
message: `Failed to parse container configuration for service ${service.service_name}: ${error.message}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
this._createContainer(service, containerConfig); // Don't await this method - we will use server-sent events to notify the client of progress
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: `Service ${serviceName} installation initiated successfully. You can receive updates via server-sent events.`,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the long-running process of creating a Docker container for a service.
|
||||
* NOTE: This method should not be called directly. Instead, use `createContainerPreflight` to check prerequisites first
|
||||
* and return an HTTP response to the client, if needed. This method will then transmit server-sent events to the client
|
||||
* to notify them of the progress.
|
||||
* @param serviceName
|
||||
* @returns
|
||||
*/
|
||||
async _createContainer(service: Service, containerConfig: any): Promise<void> {
|
||||
|
||||
transmit.broadcast('service-installation', {
|
||||
service_name: service.service_name,
|
||||
status: 'starting',
|
||||
})
|
||||
|
||||
// Start pulling the Docker image and wait for it to complete
|
||||
const pullStream = await this.docker.pull(service.container_image);
|
||||
transmit.broadcast('service-installation', {
|
||||
service_name: service.service_name,
|
||||
status: 'pulling',
|
||||
message: `Pulling Docker image ${service.container_image}...`,
|
||||
});
|
||||
|
||||
await new Promise(res => this.docker.modem.followProgress(pullStream, res));
|
||||
transmit.broadcast('service-installation', {
|
||||
service_name: service.service_name,
|
||||
status: 'pulled',
|
||||
message: `Docker image ${service.container_image} pulled successfully.`,
|
||||
});
|
||||
|
||||
transmit.broadcast('service-installation', {
|
||||
service_name: service.service_name,
|
||||
status: 'creating',
|
||||
message: `Creating Docker container for service ${service.service_name}...`,
|
||||
});
|
||||
|
||||
const container = await this.docker.createContainer({
|
||||
Image: service.container_image,
|
||||
Cmd: service.container_command.split(' '),
|
||||
name: service.service_name,
|
||||
HostConfig: containerConfig?.HostConfig || undefined,
|
||||
WorkingDir: containerConfig?.WorkingDir || undefined,
|
||||
ExposedPorts: containerConfig?.ExposedPorts || undefined,
|
||||
});
|
||||
|
||||
transmit.broadcast('service-installation', {
|
||||
service_name: service.service_name,
|
||||
status: 'created',
|
||||
message: `Docker container for service ${service.service_name} created successfully.`,
|
||||
});
|
||||
|
||||
if (service.service_name === 'kiwix-serve') {
|
||||
transmit.broadcast('service-installation', {
|
||||
service_name: service.service_name,
|
||||
status: 'preinstall',
|
||||
message: `Running pre-install actions for Kiwix Serve...`,
|
||||
});
|
||||
|
||||
await this._runPreinstallActions__KiwixServe();
|
||||
|
||||
transmit.broadcast('service-installation', {
|
||||
service_name: service.service_name,
|
||||
status: 'preinstall-complete',
|
||||
message: `Pre-install actions for Kiwix Serve completed successfully.`,
|
||||
});
|
||||
}
|
||||
|
||||
transmit.broadcast('service-installation', {
|
||||
service_name: service.service_name,
|
||||
status: 'starting',
|
||||
message: `Starting Docker container for service ${service.service_name}...`,
|
||||
});
|
||||
await container.start();
|
||||
transmit.broadcast('service-installation', {
|
||||
service_name: service.service_name,
|
||||
status: 'started',
|
||||
message: `Docker container for service ${service.service_name} started successfully.`,
|
||||
});
|
||||
|
||||
transmit.broadcast('service-installation', {
|
||||
service_name: service.service_name,
|
||||
status: 'finalizing',
|
||||
message: `Finalizing installation of service ${service.service_name}...`,
|
||||
});
|
||||
|
||||
service.installed = true;
|
||||
await service.save();
|
||||
|
||||
transmit.broadcast('service-installation', {
|
||||
service_name: service.service_name,
|
||||
status: 'completed',
|
||||
message: `Service ${service.service_name} installed successfully.`,
|
||||
});
|
||||
}
|
||||
|
||||
async _checkIfServiceContainerExists(serviceName: string): Promise<boolean> {
|
||||
try {
|
||||
const containers = await this.docker.listContainers({ all: true });
|
||||
return containers.some(container => container.Names.includes(`/${serviceName}`));
|
||||
} catch (error) {
|
||||
console.error(`Error checking if service container exists: ${error.message}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async _removeServiceContainer(serviceName: string): Promise<{ success: boolean; message: string }> {
|
||||
try {
|
||||
const containers = await this.docker.listContainers({ all: true });
|
||||
const container = containers.find(c => c.Names.includes(`/${serviceName}`));
|
||||
if (!container) {
|
||||
return { success: false, message: `Container for service ${serviceName} not found` };
|
||||
}
|
||||
|
||||
const dockerContainer = this.docker.getContainer(container.Id);
|
||||
await dockerContainer.stop();
|
||||
await dockerContainer.remove();
|
||||
|
||||
return { success: true, message: `Service ${serviceName} container removed successfully` };
|
||||
} catch (error) {
|
||||
console.error(`Error removing service container: ${error.message}`);
|
||||
return { success: false, message: `Failed to remove service ${serviceName} container: ${error.message}` };
|
||||
}
|
||||
}
|
||||
|
||||
async _runPreinstallActions__KiwixServe(): Promise<void> {
|
||||
/**
|
||||
* At least one .zim file must be available before we can start the kiwix container.
|
||||
* We'll download the lightweight mini Wikipedia Top 100 zim file for this purpose.
|
||||
**/
|
||||
const WIKIPEDIA_ZIM_URL = "https://download.kiwix.org/zim/wikipedia/wikipedia_en_100_mini_2025-06.zim"
|
||||
|
||||
const response = await axios.get(WIKIPEDIA_ZIM_URL, {
|
||||
responseType: 'stream',
|
||||
});
|
||||
|
||||
const stream = response.data;
|
||||
stream.on('error', (error: Error) => {
|
||||
logger.error(`Error downloading Wikipedia ZIM file: ${error.message}`);
|
||||
throw error;
|
||||
});
|
||||
|
||||
const disk = drive.use('fs');
|
||||
await disk.putStream('/zim/wikipedia_en_100_mini_2025-06.zim', stream);
|
||||
|
||||
|
||||
logger.info(`Downloaded Wikipedia ZIM file to /zim/wikipedia_en_100_mini_2025-06.zim`);
|
||||
}
|
||||
}
|
||||
7
admin/app/services/system_service.ts
Normal file
7
admin/app/services/system_service.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
import Service from "#models/service"
|
||||
|
||||
export class SystemService {
|
||||
async getServices(): Promise<{ id: number; service_name: string; installed: boolean }[]> {
|
||||
return await Service.query().orderBy('service_name', 'asc').select('id', 'service_name', 'installed', 'ui_location');
|
||||
}
|
||||
}
|
||||
5
admin/app/validators/system.ts
Normal file
5
admin/app/validators/system.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import vine from '@vinejs/vine'
|
||||
|
||||
export const installServiceValidator = vine.compile(vine.object({
|
||||
service_name: vine.string().trim()
|
||||
}))
|
||||
47
admin/bin/console.ts
Normal file
47
admin/bin/console.ts
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Ace entry point
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The "console.ts" file is the entrypoint for booting the AdonisJS
|
||||
| command-line framework and executing commands.
|
||||
|
|
||||
| Commands do not boot the application, unless the currently running command
|
||||
| has "options.startApp" flag set to true.
|
||||
|
|
||||
*/
|
||||
|
||||
import 'reflect-metadata'
|
||||
import { Ignitor, prettyPrintError } from '@adonisjs/core'
|
||||
|
||||
/**
|
||||
* URL to the application root. AdonisJS need it to resolve
|
||||
* paths to file and directories for scaffolding commands
|
||||
*/
|
||||
const APP_ROOT = new URL('../', import.meta.url)
|
||||
|
||||
/**
|
||||
* The importer is used to import files in context of the
|
||||
* application.
|
||||
*/
|
||||
const IMPORTER = (filePath: string) => {
|
||||
if (filePath.startsWith('./') || filePath.startsWith('../')) {
|
||||
return import(new URL(filePath, APP_ROOT).href)
|
||||
}
|
||||
return import(filePath)
|
||||
}
|
||||
|
||||
new Ignitor(APP_ROOT, { importer: IMPORTER })
|
||||
.tap((app) => {
|
||||
app.booting(async () => {
|
||||
await import('#start/env')
|
||||
})
|
||||
app.listen('SIGTERM', () => app.terminate())
|
||||
app.listenIf(app.managedByPm2, 'SIGINT', () => app.terminate())
|
||||
})
|
||||
.ace()
|
||||
.handle(process.argv.splice(2))
|
||||
.catch((error) => {
|
||||
process.exitCode = 1
|
||||
prettyPrintError(error)
|
||||
})
|
||||
45
admin/bin/server.ts
Normal file
45
admin/bin/server.ts
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| HTTP server entrypoint
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The "server.ts" file is the entrypoint for starting the AdonisJS HTTP
|
||||
| server. Either you can run this file directly or use the "serve"
|
||||
| command to run this file and monitor file changes
|
||||
|
|
||||
*/
|
||||
|
||||
import 'reflect-metadata'
|
||||
import { Ignitor, prettyPrintError } from '@adonisjs/core'
|
||||
|
||||
/**
|
||||
* URL to the application root. AdonisJS need it to resolve
|
||||
* paths to file and directories for scaffolding commands
|
||||
*/
|
||||
const APP_ROOT = new URL('../', import.meta.url)
|
||||
|
||||
/**
|
||||
* The importer is used to import files in context of the
|
||||
* application.
|
||||
*/
|
||||
const IMPORTER = (filePath: string) => {
|
||||
if (filePath.startsWith('./') || filePath.startsWith('../')) {
|
||||
return import(new URL(filePath, APP_ROOT).href)
|
||||
}
|
||||
return import(filePath)
|
||||
}
|
||||
|
||||
new Ignitor(APP_ROOT, { importer: IMPORTER })
|
||||
.tap((app) => {
|
||||
app.booting(async () => {
|
||||
await import('#start/env')
|
||||
})
|
||||
app.listen('SIGTERM', () => app.terminate())
|
||||
app.listenIf(app.managedByPm2, 'SIGINT', () => app.terminate())
|
||||
})
|
||||
.httpServer()
|
||||
.start()
|
||||
.catch((error) => {
|
||||
process.exitCode = 1
|
||||
prettyPrintError(error)
|
||||
})
|
||||
62
admin/bin/test.ts
Normal file
62
admin/bin/test.ts
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Test runner entrypoint
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The "test.ts" file is the entrypoint for running tests using Japa.
|
||||
|
|
||||
| Either you can run this file directly or use the "test"
|
||||
| command to run this file and monitor file changes.
|
||||
|
|
||||
*/
|
||||
|
||||
process.env.NODE_ENV = 'test'
|
||||
|
||||
import 'reflect-metadata'
|
||||
import { Ignitor, prettyPrintError } from '@adonisjs/core'
|
||||
import { configure, processCLIArgs, run } from '@japa/runner'
|
||||
|
||||
/**
|
||||
* URL to the application root. AdonisJS need it to resolve
|
||||
* paths to file and directories for scaffolding commands
|
||||
*/
|
||||
const APP_ROOT = new URL('../', import.meta.url)
|
||||
|
||||
/**
|
||||
* The importer is used to import files in context of the
|
||||
* application.
|
||||
*/
|
||||
const IMPORTER = (filePath: string) => {
|
||||
if (filePath.startsWith('./') || filePath.startsWith('../')) {
|
||||
return import(new URL(filePath, APP_ROOT).href)
|
||||
}
|
||||
return import(filePath)
|
||||
}
|
||||
|
||||
new Ignitor(APP_ROOT, { importer: IMPORTER })
|
||||
.tap((app) => {
|
||||
app.booting(async () => {
|
||||
await import('#start/env')
|
||||
})
|
||||
app.listen('SIGTERM', () => app.terminate())
|
||||
app.listenIf(app.managedByPm2, 'SIGINT', () => app.terminate())
|
||||
})
|
||||
.testRunner()
|
||||
.configure(async (app) => {
|
||||
const { runnerHooks, ...config } = await import('../tests/bootstrap.js')
|
||||
|
||||
processCLIArgs(process.argv.splice(2))
|
||||
configure({
|
||||
...app.rcFile.tests,
|
||||
...config,
|
||||
...{
|
||||
setup: runnerHooks.setup,
|
||||
teardown: runnerHooks.teardown.concat([() => app.terminate()]),
|
||||
},
|
||||
})
|
||||
})
|
||||
.run(() => run())
|
||||
.catch((error) => {
|
||||
process.exitCode = 1
|
||||
prettyPrintError(error)
|
||||
})
|
||||
40
admin/config/app.ts
Normal file
40
admin/config/app.ts
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
import env from '#start/env'
|
||||
import app from '@adonisjs/core/services/app'
|
||||
import { Secret } from '@adonisjs/core/helpers'
|
||||
import { defineConfig } from '@adonisjs/core/http'
|
||||
|
||||
/**
|
||||
* The app key is used for encrypting cookies, generating signed URLs,
|
||||
* and by the "encryption" module.
|
||||
*
|
||||
* The encryption module will fail to decrypt data if the key is lost or
|
||||
* changed. Therefore it is recommended to keep the app key secure.
|
||||
*/
|
||||
export const appKey = new Secret(env.get('APP_KEY'))
|
||||
|
||||
/**
|
||||
* The configuration settings used by the HTTP server
|
||||
*/
|
||||
export const http = defineConfig({
|
||||
generateRequestId: true,
|
||||
allowMethodSpoofing: false,
|
||||
|
||||
/**
|
||||
* Enabling async local storage will let you access HTTP context
|
||||
* from anywhere inside your application.
|
||||
*/
|
||||
useAsyncLocalStorage: false,
|
||||
|
||||
/**
|
||||
* Manage cookies configuration. The settings for the session id cookie are
|
||||
* defined inside the "config/session.ts" file.
|
||||
*/
|
||||
cookie: {
|
||||
domain: '',
|
||||
path: '/',
|
||||
maxAge: '2h',
|
||||
httpOnly: true,
|
||||
secure: app.inProduction,
|
||||
sameSite: 'lax',
|
||||
},
|
||||
})
|
||||
55
admin/config/bodyparser.ts
Normal file
55
admin/config/bodyparser.ts
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
import { defineConfig } from '@adonisjs/core/bodyparser'
|
||||
|
||||
const bodyParserConfig = defineConfig({
|
||||
/**
|
||||
* The bodyparser middleware will parse the request body
|
||||
* for the following HTTP methods.
|
||||
*/
|
||||
allowedMethods: ['POST', 'PUT', 'PATCH', 'DELETE'],
|
||||
|
||||
/**
|
||||
* Config for the "application/x-www-form-urlencoded"
|
||||
* content-type parser
|
||||
*/
|
||||
form: {
|
||||
convertEmptyStringsToNull: true,
|
||||
types: ['application/x-www-form-urlencoded'],
|
||||
},
|
||||
|
||||
/**
|
||||
* Config for the JSON parser
|
||||
*/
|
||||
json: {
|
||||
convertEmptyStringsToNull: true,
|
||||
types: [
|
||||
'application/json',
|
||||
'application/json-patch+json',
|
||||
'application/vnd.api+json',
|
||||
'application/csp-report',
|
||||
],
|
||||
},
|
||||
|
||||
/**
|
||||
* Config for the "multipart/form-data" content-type parser.
|
||||
* File uploads are handled by the multipart parser.
|
||||
*/
|
||||
multipart: {
|
||||
/**
|
||||
* Enabling auto process allows bodyparser middleware to
|
||||
* move all uploaded files inside the tmp folder of your
|
||||
* operating system
|
||||
*/
|
||||
autoProcess: true,
|
||||
convertEmptyStringsToNull: true,
|
||||
processManually: [],
|
||||
|
||||
/**
|
||||
* Maximum limit of data to parse including all files
|
||||
* and fields
|
||||
*/
|
||||
limit: '20mb',
|
||||
types: ['multipart/form-data'],
|
||||
},
|
||||
})
|
||||
|
||||
export default bodyParserConfig
|
||||
19
admin/config/cors.ts
Normal file
19
admin/config/cors.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import { defineConfig } from '@adonisjs/cors'
|
||||
|
||||
/**
|
||||
* Configuration options to tweak the CORS policy. The following
|
||||
* options are documented on the official documentation website.
|
||||
*
|
||||
* https://docs.adonisjs.com/guides/security/cors
|
||||
*/
|
||||
const corsConfig = defineConfig({
|
||||
enabled: true,
|
||||
origin: ['*'],
|
||||
methods: ['GET', 'HEAD', 'POST', 'PUT', 'DELETE'],
|
||||
headers: true,
|
||||
exposeHeaders: [],
|
||||
credentials: true,
|
||||
maxAge: 90,
|
||||
})
|
||||
|
||||
export default corsConfig
|
||||
26
admin/config/database.ts
Normal file
26
admin/config/database.ts
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
import env from '#start/env'
|
||||
import { defineConfig } from '@adonisjs/lucid'
|
||||
|
||||
const dbConfig = defineConfig({
|
||||
connection: 'mysql',
|
||||
connections: {
|
||||
mysql: {
|
||||
client: 'mysql2',
|
||||
debug: env.get('NODE_ENV') === 'development',
|
||||
connection: {
|
||||
host: env.get('DB_HOST'),
|
||||
port: env.get('DB_PORT') ?? 3306, // Default MySQL port
|
||||
user: env.get('DB_USER'),
|
||||
password: env.get('DB_PASSWORD'),
|
||||
database: env.get('DB_DATABASE'),
|
||||
ssl: env.get('DB_SSL') ?? true, // Default to true
|
||||
},
|
||||
migrations: {
|
||||
naturalSort: true,
|
||||
paths: ['database/migrations'],
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
export default dbConfig
|
||||
26
admin/config/drive.ts
Normal file
26
admin/config/drive.ts
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
import env from '#start/env'
|
||||
import app from '@adonisjs/core/services/app'
|
||||
import { defineConfig, services } from '@adonisjs/drive'
|
||||
|
||||
const driveConfig = defineConfig({
|
||||
default: env.get('DRIVE_DISK'),
|
||||
|
||||
/**
|
||||
* The services object can be used to configure multiple file system
|
||||
* services each using the same or a different driver.
|
||||
*/
|
||||
services: {
|
||||
fs: services.fs({
|
||||
location: app.makePath('storage'),
|
||||
serveFiles: true,
|
||||
routeBasePath: '/storage',
|
||||
visibility: 'public',
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
export default driveConfig
|
||||
|
||||
declare module '@adonisjs/drive/types' {
|
||||
export interface DriveDisks extends InferDriveDisks<typeof driveConfig> { }
|
||||
}
|
||||
24
admin/config/hash.ts
Normal file
24
admin/config/hash.ts
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
import { defineConfig, drivers } from '@adonisjs/core/hash'
|
||||
|
||||
const hashConfig = defineConfig({
|
||||
default: 'scrypt',
|
||||
|
||||
list: {
|
||||
scrypt: drivers.scrypt({
|
||||
cost: 16384,
|
||||
blockSize: 8,
|
||||
parallelization: 1,
|
||||
maxMemory: 33554432,
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
export default hashConfig
|
||||
|
||||
/**
|
||||
* Inferring types for the list of hashers you have configured
|
||||
* in your application.
|
||||
*/
|
||||
declare module '@adonisjs/core/types' {
|
||||
export interface HashersList extends InferHashers<typeof hashConfig> {}
|
||||
}
|
||||
30
admin/config/inertia.ts
Normal file
30
admin/config/inertia.ts
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
import { defineConfig } from '@adonisjs/inertia'
|
||||
import type { InferSharedProps } from '@adonisjs/inertia/types'
|
||||
|
||||
const inertiaConfig = defineConfig({
|
||||
/**
|
||||
* Path to the Edge view that will be used as the root view for Inertia responses
|
||||
*/
|
||||
rootView: 'inertia_layout',
|
||||
|
||||
/**
|
||||
* Data that should be shared with all rendered pages
|
||||
*/
|
||||
sharedData: {
|
||||
// user: (ctx) => ctx.inertia.always(() => ctx.auth.user),
|
||||
},
|
||||
|
||||
/**
|
||||
* Options for the server-side rendering
|
||||
*/
|
||||
ssr: {
|
||||
enabled: false,
|
||||
entrypoint: 'inertia/app/ssr.tsx'
|
||||
}
|
||||
})
|
||||
|
||||
export default inertiaConfig
|
||||
|
||||
declare module '@adonisjs/inertia/types' {
|
||||
export interface SharedProps extends InferSharedProps<typeof inertiaConfig> {}
|
||||
}
|
||||
36
admin/config/logger.ts
Normal file
36
admin/config/logger.ts
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
import env from '#start/env'
|
||||
//import app from '@adonisjs/core/services/app'
|
||||
import { defineConfig, targets } from '@adonisjs/core/logger'
|
||||
|
||||
const loggerConfig = defineConfig({
|
||||
default: 'app',
|
||||
|
||||
/**
|
||||
* The loggers object can be used to define multiple loggers.
|
||||
* By default, we configure only one logger (named "app").
|
||||
*/
|
||||
loggers: {
|
||||
app: {
|
||||
enabled: true,
|
||||
name: env.get('APP_NAME'),
|
||||
level: env.get('LOG_LEVEL'),
|
||||
transport: {
|
||||
targets: targets().push(targets.pretty()).toArray(), // TODO: configure file target for production correctly
|
||||
// targets()
|
||||
// .pushIf(!app.inProduction, targets.pretty())
|
||||
// .pushIf(app.inProduction, targets.file({ destination: 1 }))
|
||||
// .toArray(),
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
export default loggerConfig
|
||||
|
||||
/**
|
||||
* Inferring types for the list of loggers you have configured
|
||||
* in your application.
|
||||
*/
|
||||
declare module '@adonisjs/core/types' {
|
||||
export interface LoggersList extends InferLoggers<typeof loggerConfig> {}
|
||||
}
|
||||
48
admin/config/session.ts
Normal file
48
admin/config/session.ts
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
// import env from '#start/env'
|
||||
// import app from '@adonisjs/core/services/app'
|
||||
// import { defineConfig, stores } from '@adonisjs/session'
|
||||
|
||||
// const sessionConfig = defineConfig({
|
||||
// enabled: false,
|
||||
// cookieName: 'adonis-session',
|
||||
|
||||
// /**
|
||||
// * When set to true, the session id cookie will be deleted
|
||||
// * once the user closes the browser.
|
||||
// */
|
||||
// clearWithBrowser: false,
|
||||
|
||||
// /**
|
||||
// * Define how long to keep the session data alive without
|
||||
// * any activity.
|
||||
// */
|
||||
// age: '2h',
|
||||
|
||||
// /**
|
||||
// * Configuration for session cookie and the
|
||||
// * cookie store
|
||||
// */
|
||||
// cookie: {
|
||||
// path: '/',
|
||||
// httpOnly: true,
|
||||
// secure: app.inProduction,
|
||||
// sameSite: 'lax',
|
||||
// },
|
||||
|
||||
// /**
|
||||
// * The store to use. Make sure to validate the environment
|
||||
// * variable in order to infer the store name without any
|
||||
// * errors.
|
||||
// */
|
||||
// store: env.get('SESSION_DRIVER'),
|
||||
|
||||
// /**
|
||||
// * List of configured stores. Refer documentation to see
|
||||
// * list of available stores and their config.
|
||||
// */
|
||||
// stores: {
|
||||
// cookie: stores.cookie(),
|
||||
// },
|
||||
// })
|
||||
|
||||
// export default sessionConfig
|
||||
51
admin/config/shield.ts
Normal file
51
admin/config/shield.ts
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
import { defineConfig } from '@adonisjs/shield'
|
||||
|
||||
const shieldConfig = defineConfig({
|
||||
/**
|
||||
* Configure CSP policies for your app. Refer documentation
|
||||
* to learn more
|
||||
*/
|
||||
csp: {
|
||||
enabled: false,
|
||||
directives: {},
|
||||
reportOnly: false,
|
||||
},
|
||||
|
||||
/**
|
||||
* Configure CSRF protection options. Refer documentation
|
||||
* to learn more
|
||||
*/
|
||||
csrf: {
|
||||
enabled: false, // TODO: Enable CSRF protection
|
||||
exceptRoutes: [],
|
||||
enableXsrfCookie: true,
|
||||
methods: ['POST', 'PUT', 'PATCH', 'DELETE'],
|
||||
},
|
||||
|
||||
/**
|
||||
* Control how your website should be embedded inside
|
||||
* iFrames
|
||||
*/
|
||||
xFrame: {
|
||||
enabled: true,
|
||||
action: 'DENY',
|
||||
},
|
||||
|
||||
/**
|
||||
* Force browser to always use HTTPS
|
||||
*/
|
||||
hsts: {
|
||||
enabled: false, // TODO: Enable HSTS in production
|
||||
maxAge: '180 days',
|
||||
},
|
||||
|
||||
/**
|
||||
* Disable browsers from sniffing the content type of a
|
||||
* response and always rely on the "content-type" header.
|
||||
*/
|
||||
contentTypeSniffing: {
|
||||
enabled: true,
|
||||
},
|
||||
})
|
||||
|
||||
export default shieldConfig
|
||||
17
admin/config/static.ts
Normal file
17
admin/config/static.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import { defineConfig } from '@adonisjs/static'
|
||||
|
||||
/**
|
||||
* Configuration options to tweak the static files middleware.
|
||||
* The complete set of options are documented on the
|
||||
* official documentation website.
|
||||
*
|
||||
* https://docs.adonisjs.com/guides/static-assets
|
||||
*/
|
||||
const staticServerConfig = defineConfig({
|
||||
enabled: true,
|
||||
etag: true,
|
||||
lastModified: true,
|
||||
dotFiles: 'ignore',
|
||||
})
|
||||
|
||||
export default staticServerConfig
|
||||
6
admin/config/transmit.ts
Normal file
6
admin/config/transmit.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
import { defineConfig } from '@adonisjs/transmit'
|
||||
|
||||
export default defineConfig({
|
||||
pingInterval: false,
|
||||
transport: null
|
||||
})
|
||||
28
admin/config/vite.ts
Normal file
28
admin/config/vite.ts
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
import { defineConfig } from '@adonisjs/vite'
|
||||
|
||||
const viteBackendConfig = defineConfig({
|
||||
/**
|
||||
* The output of vite will be written inside this
|
||||
* directory. The path should be relative from
|
||||
* the application root.
|
||||
*/
|
||||
buildDirectory: 'public/assets',
|
||||
|
||||
/**
|
||||
* The path to the manifest file generated by the
|
||||
* "vite build" command.
|
||||
*/
|
||||
manifestFile: 'public/assets/.vite/manifest.json',
|
||||
|
||||
/**
|
||||
* Feel free to change the value of the "assetsUrl" to
|
||||
* point to a CDN in production.
|
||||
*/
|
||||
assetsUrl: '/assets',
|
||||
|
||||
scriptAttributes: {
|
||||
defer: true,
|
||||
},
|
||||
})
|
||||
|
||||
export default viteBackendConfig
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
import { BaseSchema } from '@adonisjs/lucid/schema'
|
||||
|
||||
export default class extends BaseSchema {
|
||||
protected tableName = 'services'
|
||||
|
||||
async up() {
|
||||
this.schema.createTable(this.tableName, (table) => {
|
||||
table.increments('id')
|
||||
table.string('service_name')
|
||||
table.string('container_image')
|
||||
table.string('container_command')
|
||||
table.json('container_config').nullable()
|
||||
table.boolean('installed').defaultTo(false)
|
||||
table.string('ui_location')
|
||||
table.json('metadata').nullable()
|
||||
table.timestamp('created_at')
|
||||
table.timestamp('updated_at')
|
||||
})
|
||||
}
|
||||
|
||||
async down() {
|
||||
this.schema.dropTable(this.tableName)
|
||||
}
|
||||
}
|
||||
25
admin/database/seeders/service_seeder.ts
Normal file
25
admin/database/seeders/service_seeder.ts
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
import Service from '#models/service'
|
||||
import { BaseSeeder } from '@adonisjs/lucid/seeders'
|
||||
import { ModelAttributes } from '@adonisjs/lucid/types/model'
|
||||
|
||||
export default class ServiceSeeder extends BaseSeeder {
|
||||
private static DEFAULT_SERVICES: Omit<ModelAttributes<Service>, 'created_at' | 'updated_at' | 'metadata' | 'id'>[] = [
|
||||
{
|
||||
service_name: 'kiwix-serve',
|
||||
container_image: 'ghcr.io/kiwix/kiwix-serve',
|
||||
container_command: '*.zim --address=0.0.0.0',
|
||||
container_config: "{\"HostConfig\":{\"Binds\":[\"/opt/project-nomad/storage/zim:/data\"],\"PortBindings\":{\"8080/tcp\":[{\"HostPort\":\"8090\"}]}},\"ExposedPorts\":{\"8080/tcp\":{}}}",
|
||||
ui_location: '8090',
|
||||
installed: false,
|
||||
},
|
||||
]
|
||||
|
||||
async run() {
|
||||
const existingServices = await Service.query().select('service_name')
|
||||
const existingServiceNames = new Set(existingServices.map(service => service.service_name))
|
||||
|
||||
const newServices = ServiceSeeder.DEFAULT_SERVICES.filter(service => !existingServiceNames.has(service.service_name))
|
||||
|
||||
await Service.createMany([...newServices])
|
||||
}
|
||||
}
|
||||
2
admin/eslint.config.js
Normal file
2
admin/eslint.config.js
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
import { configApp } from '@adonisjs/eslint-config'
|
||||
export default configApp()
|
||||
28
admin/inertia/app/app.tsx
Normal file
28
admin/inertia/app/app.tsx
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
/// <reference path="../../adonisrc.ts" />
|
||||
/// <reference path="../../config/inertia.ts" />
|
||||
|
||||
import '../css/app.css';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { createInertiaApp } from '@inertiajs/react';
|
||||
import { resolvePageComponent } from '@adonisjs/inertia/helpers'
|
||||
|
||||
const appName = import.meta.env.VITE_APP_NAME || 'AdonisJS'
|
||||
|
||||
createInertiaApp({
|
||||
progress: { color: '#5468FF' },
|
||||
|
||||
title: (title) => `${title} - ${appName}`,
|
||||
|
||||
resolve: (name) => {
|
||||
return resolvePageComponent(
|
||||
`../pages/${name}.tsx`,
|
||||
import.meta.glob('../pages/**/*.tsx'),
|
||||
)
|
||||
},
|
||||
|
||||
setup({ el, App, props }) {
|
||||
|
||||
createRoot(el).render(<App {...props} />);
|
||||
|
||||
},
|
||||
});
|
||||
42
admin/inertia/components/BouncingLogo.tsx
Normal file
42
admin/inertia/components/BouncingLogo.tsx
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
import { useState, useEffect } from 'react';
|
||||
|
||||
// Fading Image Component
|
||||
const FadingImage = ({ alt = "Fading image", className = "" }) => {
|
||||
const [isVisible, setIsVisible] = useState(true);
|
||||
const [shouldShow, setShouldShow] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
// Start fading out after 2 seconds
|
||||
const fadeTimer = setTimeout(() => {
|
||||
setIsVisible(false);
|
||||
}, 2000);
|
||||
|
||||
// Remove from DOM after fade out completes
|
||||
const removeTimer = setTimeout(() => {
|
||||
setShouldShow(false);
|
||||
}, 3000);
|
||||
|
||||
return () => {
|
||||
clearTimeout(fadeTimer);
|
||||
clearTimeout(removeTimer);
|
||||
};
|
||||
}, []);
|
||||
|
||||
if (!shouldShow) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`fixed inset-0 flex justify-center items-center bg-desert-sand z-50 pointer-events-none transition-opacity duration-1000 ${
|
||||
isVisible ? 'opacity-100' : 'opacity-0'
|
||||
}`}>
|
||||
<img
|
||||
src={`/project_nomad_logo.png`}
|
||||
alt={alt}
|
||||
className={`w-64 h-64 ${className}`}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FadingImage;
|
||||
13
admin/inertia/css/app.css
Normal file
13
admin/inertia/css/app.css
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
@import 'tailwindcss';
|
||||
|
||||
@theme {
|
||||
--color-desert-white: #f6f6f4;
|
||||
--color-desert-green-light: #babaaa;
|
||||
--color-desert-green: #424420;
|
||||
--color-desert-orange: #a84a12;
|
||||
--color-desert-sand: #f7eedc
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: var(--color-desert-sand);
|
||||
}
|
||||
22
admin/inertia/layouts/AppLayout.tsx
Normal file
22
admin/inertia/layouts/AppLayout.tsx
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
export default function AppLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col">
|
||||
<div className="p-2 flex gap-2 flex-col items-center justify-center">
|
||||
<img src="/project_nomad_logo.png" alt="Project Nomad Logo" className="h-40 w-40" />
|
||||
<h1 className="text-5xl font-bold text-desert-green">Command Center</h1>
|
||||
</div>
|
||||
<hr className="text-desert-green font-semibold h-[1.5px] bg-desert-green border-none" />
|
||||
<div className="flex-1 w-full bg-desert">{children}</div>
|
||||
{/* <TanStackRouterDevtools /> */}
|
||||
{/* <hr className="text-desert-green font-semibold h-[1.5px] bg-desert-green border-none" />
|
||||
<div className="p-2 flex flex-col items-center justify-center ">
|
||||
<p className="text-sm text-gray-900 italic">
|
||||
Sapientia ianua vitae | Wisdom is the gateway to life
|
||||
</p>
|
||||
<p
|
||||
className="text-desert-orange font-semibold text-sm italic"
|
||||
>A project by Crosstalk Solutions</p>
|
||||
</div> */}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
4
admin/inertia/lib/classNames.ts
Normal file
4
admin/inertia/lib/classNames.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
|
||||
export default function classNames(...classes: (string | undefined)[]): string {
|
||||
return classes.filter(Boolean).join(' ');
|
||||
}
|
||||
21
admin/inertia/lib/navigation.ts
Normal file
21
admin/inertia/lib/navigation.ts
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
|
||||
|
||||
export function getServiceLink(ui_location: string): string {
|
||||
// Check if the ui location is a valid URL
|
||||
try {
|
||||
const url = new URL(ui_location);
|
||||
// If it is a valid URL, return it as is
|
||||
return url.href;
|
||||
} catch (e) {
|
||||
// If it fails, it means it's not a valid URL
|
||||
}
|
||||
|
||||
// Check if the ui location is a port number
|
||||
const parsedPort = parseInt(ui_location, 10);
|
||||
if (!isNaN(parsedPort)) {
|
||||
// If it's a port number, return a link to the service on that port
|
||||
return `http://localhost:${parsedPort}`;
|
||||
}
|
||||
// Otherwise, treat it as a path
|
||||
return `/${ui_location}`;
|
||||
}
|
||||
5
admin/inertia/lib/transmit.ts
Normal file
5
admin/inertia/lib/transmit.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import { Transmit } from '@adonisjs/transmit-client'
|
||||
|
||||
export const transmit = new Transmit({
|
||||
baseUrl: window.location.origin
|
||||
})
|
||||
9
admin/inertia/pages/about.tsx
Normal file
9
admin/inertia/pages/about.tsx
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import AppLayout from '~/layouts/AppLayout'
|
||||
|
||||
export default function About() {
|
||||
return (
|
||||
<AppLayout>
|
||||
<div className="p-2">Hello from About!</div>
|
||||
</AppLayout>
|
||||
)
|
||||
}
|
||||
11
admin/inertia/pages/errors/not_found.tsx
Normal file
11
admin/inertia/pages/errors/not_found.tsx
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
export default function NotFound() {
|
||||
return (
|
||||
<>
|
||||
<div className="container">
|
||||
<div className="title">Page not found</div>
|
||||
|
||||
<span>This page does not exist.</span>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
11
admin/inertia/pages/errors/server_error.tsx
Normal file
11
admin/inertia/pages/errors/server_error.tsx
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
export default function ServerError(props: { error: any }) {
|
||||
return (
|
||||
<>
|
||||
<div className="container">
|
||||
<div className="title">Server Error</div>
|
||||
|
||||
<span>{props.error.message}</span>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
111
admin/inertia/pages/home.tsx
Normal file
111
admin/inertia/pages/home.tsx
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
import {
|
||||
IconBook,
|
||||
IconBrandWikipedia,
|
||||
IconCalculator,
|
||||
IconHelp,
|
||||
IconMapRoute,
|
||||
IconMessageCircleSearch,
|
||||
IconSettings,
|
||||
IconWifiOff,
|
||||
} from '@tabler/icons-react'
|
||||
import { Head, Link } from '@inertiajs/react'
|
||||
import BouncingLogo from '~/components/BouncingLogo'
|
||||
import AppLayout from '~/layouts/AppLayout'
|
||||
import { getServiceLink } from '~/lib/navigation'
|
||||
|
||||
const NAV_ITEMS = [
|
||||
{
|
||||
label: 'AI Chat',
|
||||
to: '/ai-chat',
|
||||
description: 'Chat with local AI models',
|
||||
icon: <IconMessageCircleSearch size={48} />,
|
||||
},
|
||||
{
|
||||
label: 'Calculators',
|
||||
to: '/calculators',
|
||||
description: 'Perform various calculations',
|
||||
icon: <IconCalculator size={48} />,
|
||||
},
|
||||
{
|
||||
label: 'Ebooks',
|
||||
to: '/ebooks',
|
||||
description: 'Explore our collection of eBooks',
|
||||
icon: <IconBook size={48} />,
|
||||
},
|
||||
{
|
||||
label: 'Kiwix (Offline Browser)',
|
||||
to: '/kiwix',
|
||||
description: 'Access offline content with Kiwix',
|
||||
icon: <IconWifiOff size={48} />,
|
||||
},
|
||||
{
|
||||
label: 'OpenStreetMap',
|
||||
to: '/openstreetmap',
|
||||
description: 'View maps and geospatial data',
|
||||
icon: <IconMapRoute size={48} />,
|
||||
},
|
||||
|
||||
{
|
||||
label: 'Wikipedia',
|
||||
to: '/wikipedia',
|
||||
description: 'Browse an offline Wikipedia snapshot',
|
||||
icon: <IconBrandWikipedia size={48} />,
|
||||
},
|
||||
]
|
||||
|
||||
const STATIC_ITEMS = [
|
||||
{
|
||||
label: 'Help',
|
||||
to: '/help',
|
||||
description: 'Read Project N.O.M.A.D. manuals and guides',
|
||||
icon: <IconHelp size={48} />,
|
||||
installed: true,
|
||||
},
|
||||
{
|
||||
label: 'Settings',
|
||||
to: '/settings',
|
||||
description: 'Configure your N.O.M.A.D. settings',
|
||||
icon: <IconSettings size={48} />,
|
||||
installed: true,
|
||||
},
|
||||
]
|
||||
|
||||
export default function Home(props: {
|
||||
system: {
|
||||
services: { id: number; service_name: string; installed: boolean; ui_location: string }[]
|
||||
}
|
||||
}) {
|
||||
const items = []
|
||||
props.system.services.map((service) => {
|
||||
items.push({
|
||||
label: service.service_name,
|
||||
to: getServiceLink(service.ui_location),
|
||||
description: `Access ${service.service_name} content`,
|
||||
icon: <IconWifiOff size={48} />,
|
||||
installed: service.installed,
|
||||
})
|
||||
})
|
||||
|
||||
items.push(...STATIC_ITEMS)
|
||||
|
||||
return (
|
||||
<AppLayout>
|
||||
<Head title="Project N.O.M.A.D Command Center" />
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 p-4">
|
||||
{items.map((item) => (
|
||||
<a key={item.label} href={item.to} target="_blank">
|
||||
<div
|
||||
key={item.label}
|
||||
className="rounded border-desert-green border-2 bg-desert-green hover:bg-transparent hover:text-black text-white transition-colors shadow-sm h-48 flex flex-col items-center justify-center cursor-pointer"
|
||||
>
|
||||
<div className="flex items-center justify-center mb-2">{item.icon}</div>
|
||||
<h3 className="font-bold text-2xl">{item.label}</h3>
|
||||
<p className="text-lg mt-2">{item.description}</p>
|
||||
</div>
|
||||
</a>
|
||||
))}
|
||||
<BouncingLogo />
|
||||
</div>
|
||||
</AppLayout>
|
||||
)
|
||||
}
|
||||
531
admin/inertia/pages/settings.tsx
Normal file
531
admin/inertia/pages/settings.tsx
Normal file
|
|
@ -0,0 +1,531 @@
|
|||
import { useState } from 'react'
|
||||
import { Dialog, DialogBackdrop, DialogPanel, TransitionChild } from '@headlessui/react'
|
||||
import {
|
||||
ChartBarSquareIcon,
|
||||
Cog6ToothIcon,
|
||||
FolderIcon,
|
||||
GlobeAltIcon,
|
||||
ServerIcon,
|
||||
SignalIcon,
|
||||
XMarkIcon,
|
||||
} from '@heroicons/react/24/outline'
|
||||
import { Bars3Icon, MagnifyingGlassIcon } from '@heroicons/react/20/solid'
|
||||
import { ChevronDownIcon } from '@heroicons/react/16/solid'
|
||||
import classNames from '~/lib/classNames'
|
||||
import { Head } from "@inertiajs/react";
|
||||
|
||||
const navigation = [
|
||||
{ name: 'Projects', href: '#', icon: FolderIcon, current: false },
|
||||
{ name: 'Deployments', href: '#', icon: ServerIcon, current: false },
|
||||
{ name: 'Activity', href: '#', icon: SignalIcon, current: false },
|
||||
{ name: 'Domains', href: '#', icon: GlobeAltIcon, current: false },
|
||||
{ name: 'Usage', href: '#', icon: ChartBarSquareIcon, current: false },
|
||||
{ name: 'Settings', href: '#', icon: Cog6ToothIcon, current: true },
|
||||
]
|
||||
const teams = [
|
||||
{ id: 1, name: 'Planetaria', href: '#', initial: 'P', current: false },
|
||||
{ id: 2, name: 'Protocol', href: '#', initial: 'P', current: false },
|
||||
{ id: 3, name: 'Tailwind Labs', href: '#', initial: 'T', current: false },
|
||||
]
|
||||
const secondaryNavigation = [
|
||||
{ name: 'Account', href: '#', current: true },
|
||||
{ name: 'Notifications', href: '#', current: false },
|
||||
{ name: 'Billing', href: '#', current: false },
|
||||
{ name: 'Teams', href: '#', current: false },
|
||||
{ name: 'Integrations', href: '#', current: false },
|
||||
]
|
||||
|
||||
export default function SettingsPage() {
|
||||
const [sidebarOpen, setSidebarOpen] = useState(false)
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Dialog open={sidebarOpen} onClose={setSidebarOpen} className="relative z-50 xl:hidden">
|
||||
<DialogBackdrop
|
||||
transition
|
||||
className="fixed inset-0 bg-gray-900/80 transition-opacity duration-300 ease-linear data-[closed]:opacity-0"
|
||||
/>
|
||||
|
||||
<div className="fixed inset-0 flex">
|
||||
<DialogPanel
|
||||
transition
|
||||
className="relative mr-16 flex w-full max-w-xs flex-1 transform transition duration-300 ease-in-out data-[closed]:-translate-x-full"
|
||||
>
|
||||
<TransitionChild>
|
||||
<div className="absolute left-full top-0 flex w-16 justify-center pt-5 duration-300 ease-in-out data-[closed]:opacity-0">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setSidebarOpen(false)}
|
||||
className="-m-2.5 p-2.5"
|
||||
>
|
||||
<span className="sr-only">Close sidebar</span>
|
||||
<XMarkIcon aria-hidden="true" className="size-6 text-white" />
|
||||
</button>
|
||||
</div>
|
||||
</TransitionChild>
|
||||
|
||||
{/* Sidebar component, swap this element with another sidebar if you like */}
|
||||
<div className="flex grow flex-col gap-y-5 overflow-y-auto bg-gray-900 px-6 ring-1 ring-white/10">
|
||||
<div className="flex h-16 shrink-0 items-center">
|
||||
<img src="/project_nomad_logo.png" alt="Project Nomad Logo" className="h-12 w-12" />
|
||||
</div>
|
||||
<nav className="flex flex-1 flex-col">
|
||||
<ul role="list" className="flex flex-1 flex-col gap-y-7">
|
||||
<li>
|
||||
<ul role="list" className="-mx-2 space-y-1">
|
||||
{navigation.map((item) => (
|
||||
<li key={item.name}>
|
||||
<a
|
||||
href={item.href}
|
||||
className={classNames(
|
||||
item.current
|
||||
? 'bg-gray-800 text-white'
|
||||
: 'text-gray-400 hover:bg-gray-800 hover:text-white',
|
||||
'group flex gap-x-3 rounded-md p-2 text-sm/6 font-semibold'
|
||||
)}
|
||||
>
|
||||
<item.icon aria-hidden="true" className="size-6 shrink-0" />
|
||||
{item.name}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<div className="text-xs/6 font-semibold text-gray-400">Your teams</div>
|
||||
<ul role="list" className="-mx-2 mt-2 space-y-1">
|
||||
{teams.map((team) => (
|
||||
<li key={team.name}>
|
||||
<a
|
||||
href={team.href}
|
||||
className={classNames(
|
||||
team.current
|
||||
? 'bg-gray-800 text-white'
|
||||
: 'text-gray-400 hover:bg-gray-800 hover:text-white',
|
||||
'group flex gap-x-3 rounded-md p-2 text-sm/6 font-semibold'
|
||||
)}
|
||||
>
|
||||
<span className="flex size-6 shrink-0 items-center justify-center rounded-lg border border-gray-700 bg-gray-800 text-[0.625rem] font-medium text-gray-400 group-hover:text-white">
|
||||
{team.initial}
|
||||
</span>
|
||||
<span className="truncate">{team.name}</span>
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</li>
|
||||
<li className="-mx-6 mt-auto">
|
||||
<a
|
||||
href="#"
|
||||
className="flex items-center gap-x-4 px-6 py-3 text-sm/6 font-semibold text-white hover:bg-gray-800"
|
||||
>
|
||||
<img
|
||||
alt=""
|
||||
src="https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80"
|
||||
className="size-8 rounded-full bg-gray-800"
|
||||
/>
|
||||
<span className="sr-only">Your profile</span>
|
||||
<span aria-hidden="true">Tom Cook</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</DialogPanel>
|
||||
</div>
|
||||
</Dialog>
|
||||
|
||||
{/* Static sidebar for desktop */}
|
||||
<div className="hidden xl:fixed xl:inset-y-0 xl:z-50 xl:flex xl:w-72 xl:flex-col">
|
||||
{/* Sidebar component, swap this element with another sidebar if you like */}
|
||||
<div className="flex grow flex-col gap-y-5 overflow-y-auto bg-black/10 px-6 ring-1 ring-white/5">
|
||||
<div className="flex h-16 shrink-0 items-center">
|
||||
<img
|
||||
alt="Your Company"
|
||||
src="https://tailwindcss.com/plus-assets/img/logos/mark.svg?color=indigo&shade=500"
|
||||
className="h-8 w-auto"
|
||||
/>
|
||||
</div>
|
||||
<nav className="flex flex-1 flex-col">
|
||||
<ul role="list" className="flex flex-1 flex-col gap-y-7">
|
||||
<li>
|
||||
<ul role="list" className="-mx-2 space-y-1">
|
||||
{navigation.map((item) => (
|
||||
<li key={item.name}>
|
||||
<a
|
||||
href={item.href}
|
||||
className={classNames(
|
||||
item.current
|
||||
? 'bg-gray-800 text-white'
|
||||
: 'text-gray-400 hover:bg-gray-800 hover:text-white',
|
||||
'group flex gap-x-3 rounded-md p-2 text-sm/6 font-semibold'
|
||||
)}
|
||||
>
|
||||
<item.icon aria-hidden="true" className="size-6 shrink-0" />
|
||||
{item.name}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<div className="text-xs/6 font-semibold text-gray-400">Your teams</div>
|
||||
<ul role="list" className="-mx-2 mt-2 space-y-1">
|
||||
{teams.map((team) => (
|
||||
<li key={team.name}>
|
||||
<a
|
||||
href={team.href}
|
||||
className={classNames(
|
||||
team.current
|
||||
? 'bg-gray-800 text-white'
|
||||
: 'text-gray-400 hover:bg-gray-800 hover:text-white',
|
||||
'group flex gap-x-3 rounded-md p-2 text-sm/6 font-semibold'
|
||||
)}
|
||||
>
|
||||
<span className="flex size-6 shrink-0 items-center justify-center rounded-lg border border-gray-700 bg-gray-800 text-[0.625rem] font-medium text-gray-400 group-hover:text-white">
|
||||
{team.initial}
|
||||
</span>
|
||||
<span className="truncate">{team.name}</span>
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</li>
|
||||
<li className="-mx-6 mt-auto">
|
||||
<a
|
||||
href="#"
|
||||
className="flex items-center gap-x-4 px-6 py-3 text-sm/6 font-semibold text-white hover:bg-gray-800"
|
||||
>
|
||||
<img
|
||||
alt=""
|
||||
src="https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80"
|
||||
className="size-8 rounded-full bg-gray-800"
|
||||
/>
|
||||
<span className="sr-only">Your profile</span>
|
||||
<span aria-hidden="true">Tom Cook</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="xl:pl-72">
|
||||
{/* Sticky search header */}
|
||||
<div className="sticky top-0 z-40 flex h-16 shrink-0 items-center gap-x-6 border-b border-white/5 bg-gray-900 px-4 shadow-sm sm:px-6 lg:px-8">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setSidebarOpen(true)}
|
||||
className="-m-2.5 p-2.5 text-white xl:hidden"
|
||||
>
|
||||
<span className="sr-only">Open sidebar</span>
|
||||
<Bars3Icon aria-hidden="true" className="size-5" />
|
||||
</button>
|
||||
|
||||
<div className="flex flex-1 gap-x-4 self-stretch lg:gap-x-6">
|
||||
<form action="#" method="GET" className="grid flex-1 grid-cols-1">
|
||||
<input
|
||||
name="search"
|
||||
type="search"
|
||||
placeholder="Search"
|
||||
aria-label="Search"
|
||||
className="col-start-1 row-start-1 block size-full bg-transparent pl-8 text-base text-white outline-none placeholder:text-gray-500 sm:text-sm/6"
|
||||
/>
|
||||
<MagnifyingGlassIcon
|
||||
aria-hidden="true"
|
||||
className="pointer-events-none col-start-1 row-start-1 size-5 self-center text-gray-500"
|
||||
/>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<main>
|
||||
<h1 className="sr-only">Account Settings</h1>
|
||||
|
||||
<header className="border-b border-white/5">
|
||||
{/* Secondary navigation */}
|
||||
<nav className="flex overflow-x-auto py-4">
|
||||
<ul
|
||||
role="list"
|
||||
className="flex min-w-full flex-none gap-x-6 px-4 text-sm/6 font-semibold text-gray-400 sm:px-6 lg:px-8"
|
||||
>
|
||||
{secondaryNavigation.map((item) => (
|
||||
<li key={item.name}>
|
||||
<a href={item.href} className={item.current ? 'text-indigo-400' : ''}>
|
||||
{item.name}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
{/* Settings forms */}
|
||||
<div className="divide-y divide-white/5">
|
||||
<div className="grid max-w-7xl grid-cols-1 gap-x-8 gap-y-10 px-4 py-16 sm:px-6 md:grid-cols-3 lg:px-8">
|
||||
<div>
|
||||
<h2 className="text-base/7 font-semibold text-white">Personal Information</h2>
|
||||
<p className="mt-1 text-sm/6 text-gray-400">
|
||||
Use a permanent address where you can receive mail.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form className="md:col-span-2">
|
||||
<div className="grid grid-cols-1 gap-x-6 gap-y-8 sm:max-w-xl sm:grid-cols-6">
|
||||
<div className="col-span-full flex items-center gap-x-8">
|
||||
<img
|
||||
alt=""
|
||||
src="https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80"
|
||||
className="size-24 flex-none rounded-lg bg-gray-800 object-cover"
|
||||
/>
|
||||
<div>
|
||||
<button
|
||||
type="button"
|
||||
className="rounded-md bg-white/10 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-white/20"
|
||||
>
|
||||
Change avatar
|
||||
</button>
|
||||
<p className="mt-2 text-xs/5 text-gray-400">JPG, GIF or PNG. 1MB max.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="sm:col-span-3">
|
||||
<label htmlFor="first-name" className="block text-sm/6 font-medium text-white">
|
||||
First name
|
||||
</label>
|
||||
<div className="mt-2">
|
||||
<input
|
||||
id="first-name"
|
||||
name="first-name"
|
||||
type="text"
|
||||
autoComplete="given-name"
|
||||
className="block w-full rounded-md bg-white/5 px-3 py-1.5 text-base text-white outline outline-1 -outline-offset-1 outline-white/10 placeholder:text-gray-500 focus:outline focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-500 sm:text-sm/6"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="sm:col-span-3">
|
||||
<label htmlFor="last-name" className="block text-sm/6 font-medium text-white">
|
||||
Last name
|
||||
</label>
|
||||
<div className="mt-2">
|
||||
<input
|
||||
id="last-name"
|
||||
name="last-name"
|
||||
type="text"
|
||||
autoComplete="family-name"
|
||||
className="block w-full rounded-md bg-white/5 px-3 py-1.5 text-base text-white outline outline-1 -outline-offset-1 outline-white/10 placeholder:text-gray-500 focus:outline focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-500 sm:text-sm/6"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="col-span-full">
|
||||
<label htmlFor="email" className="block text-sm/6 font-medium text-white">
|
||||
Email address
|
||||
</label>
|
||||
<div className="mt-2">
|
||||
<input
|
||||
id="email"
|
||||
name="email"
|
||||
type="email"
|
||||
autoComplete="email"
|
||||
className="block w-full rounded-md bg-white/5 px-3 py-1.5 text-base text-white outline outline-1 -outline-offset-1 outline-white/10 placeholder:text-gray-500 focus:outline focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-500 sm:text-sm/6"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="col-span-full">
|
||||
<label htmlFor="username" className="block text-sm/6 font-medium text-white">
|
||||
Username
|
||||
</label>
|
||||
<div className="mt-2">
|
||||
<div className="flex items-center rounded-md bg-white/5 pl-3 outline outline-1 -outline-offset-1 outline-white/10 focus-within:outline focus-within:outline-2 focus-within:-outline-offset-2 focus-within:outline-indigo-500">
|
||||
<div className="shrink-0 select-none text-base text-gray-500 sm:text-sm/6">
|
||||
example.com/
|
||||
</div>
|
||||
<input
|
||||
id="username"
|
||||
name="username"
|
||||
type="text"
|
||||
placeholder="janesmith"
|
||||
className="block min-w-0 grow bg-transparent py-1.5 pl-1 pr-3 text-base text-white placeholder:text-gray-500 focus:outline focus:outline-0 sm:text-sm/6"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="col-span-full">
|
||||
<label htmlFor="timezone" className="block text-sm/6 font-medium text-white">
|
||||
Timezone
|
||||
</label>
|
||||
<div className="mt-2 grid grid-cols-1">
|
||||
<select
|
||||
id="timezone"
|
||||
name="timezone"
|
||||
className="col-start-1 row-start-1 w-full appearance-none rounded-md bg-white/5 py-1.5 pl-3 pr-8 text-base text-white outline outline-1 -outline-offset-1 outline-white/10 *:bg-gray-800 focus:outline focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-500 sm:text-sm/6"
|
||||
>
|
||||
<option>Pacific Standard Time</option>
|
||||
<option>Eastern Standard Time</option>
|
||||
<option>Greenwich Mean Time</option>
|
||||
</select>
|
||||
<ChevronDownIcon
|
||||
aria-hidden="true"
|
||||
className="pointer-events-none col-start-1 row-start-1 mr-2 size-5 self-center justify-self-end text-gray-400 sm:size-4"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-8 flex">
|
||||
<button
|
||||
type="submit"
|
||||
className="rounded-md bg-indigo-500 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-400 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-500"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div className="grid max-w-7xl grid-cols-1 gap-x-8 gap-y-10 px-4 py-16 sm:px-6 md:grid-cols-3 lg:px-8">
|
||||
<div>
|
||||
<h2 className="text-base/7 font-semibold text-white">Change password</h2>
|
||||
<p className="mt-1 text-sm/6 text-gray-400">
|
||||
Update your password associated with your account.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form className="md:col-span-2">
|
||||
<div className="grid grid-cols-1 gap-x-6 gap-y-8 sm:max-w-xl sm:grid-cols-6">
|
||||
<div className="col-span-full">
|
||||
<label
|
||||
htmlFor="current-password"
|
||||
className="block text-sm/6 font-medium text-white"
|
||||
>
|
||||
Current password
|
||||
</label>
|
||||
<div className="mt-2">
|
||||
<input
|
||||
id="current-password"
|
||||
name="current_password"
|
||||
type="password"
|
||||
autoComplete="current-password"
|
||||
className="block w-full rounded-md bg-white/5 px-3 py-1.5 text-base text-white outline outline-1 -outline-offset-1 outline-white/10 placeholder:text-gray-500 focus:outline focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-500 sm:text-sm/6"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="col-span-full">
|
||||
<label
|
||||
htmlFor="new-password"
|
||||
className="block text-sm/6 font-medium text-white"
|
||||
>
|
||||
New password
|
||||
</label>
|
||||
<div className="mt-2">
|
||||
<input
|
||||
id="new-password"
|
||||
name="new_password"
|
||||
type="password"
|
||||
autoComplete="new-password"
|
||||
className="block w-full rounded-md bg-white/5 px-3 py-1.5 text-base text-white outline outline-1 -outline-offset-1 outline-white/10 placeholder:text-gray-500 focus:outline focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-500 sm:text-sm/6"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="col-span-full">
|
||||
<label
|
||||
htmlFor="confirm-password"
|
||||
className="block text-sm/6 font-medium text-white"
|
||||
>
|
||||
Confirm password
|
||||
</label>
|
||||
<div className="mt-2">
|
||||
<input
|
||||
id="confirm-password"
|
||||
name="confirm_password"
|
||||
type="password"
|
||||
autoComplete="new-password"
|
||||
className="block w-full rounded-md bg-white/5 px-3 py-1.5 text-base text-white outline outline-1 -outline-offset-1 outline-white/10 placeholder:text-gray-500 focus:outline focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-500 sm:text-sm/6"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-8 flex">
|
||||
<button
|
||||
type="submit"
|
||||
className="rounded-md bg-indigo-500 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-400 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-500"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div className="grid max-w-7xl grid-cols-1 gap-x-8 gap-y-10 px-4 py-16 sm:px-6 md:grid-cols-3 lg:px-8">
|
||||
<div>
|
||||
<h2 className="text-base/7 font-semibold text-white">Log out other sessions</h2>
|
||||
<p className="mt-1 text-sm/6 text-gray-400">
|
||||
Please enter your password to confirm you would like to log out of your other
|
||||
sessions across all of your devices.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form className="md:col-span-2">
|
||||
<div className="grid grid-cols-1 gap-x-6 gap-y-8 sm:max-w-xl sm:grid-cols-6">
|
||||
<div className="col-span-full">
|
||||
<label
|
||||
htmlFor="logout-password"
|
||||
className="block text-sm/6 font-medium text-white"
|
||||
>
|
||||
Your password
|
||||
</label>
|
||||
<div className="mt-2">
|
||||
<input
|
||||
id="logout-password"
|
||||
name="password"
|
||||
type="password"
|
||||
autoComplete="current-password"
|
||||
className="block w-full rounded-md bg-white/5 px-3 py-1.5 text-base text-white outline outline-1 -outline-offset-1 outline-white/10 placeholder:text-gray-500 focus:outline focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-500 sm:text-sm/6"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-8 flex">
|
||||
<button
|
||||
type="submit"
|
||||
className="rounded-md bg-indigo-500 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-400 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-500"
|
||||
>
|
||||
Log out other sessions
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div className="grid max-w-7xl grid-cols-1 gap-x-8 gap-y-10 px-4 py-16 sm:px-6 md:grid-cols-3 lg:px-8">
|
||||
<div>
|
||||
<h2 className="text-base/7 font-semibold text-white">Delete account</h2>
|
||||
<p className="mt-1 text-sm/6 text-gray-400">
|
||||
No longer want to use our service? You can delete your account here. This action
|
||||
is not reversible. All information related to this account will be deleted
|
||||
permanently.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form className="flex items-start md:col-span-2">
|
||||
<button
|
||||
type="submit"
|
||||
className="rounded-md bg-red-500 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-red-400"
|
||||
>
|
||||
Yes, delete my account
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
12
admin/inertia/tsconfig.json
Normal file
12
admin/inertia/tsconfig.json
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"extends": "@adonisjs/tsconfig/tsconfig.client.json",
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"module": "ESNext",
|
||||
"jsx": "react-jsx",
|
||||
"paths": {
|
||||
"~/*": ["./*"],
|
||||
},
|
||||
},
|
||||
"include": ["./**/*.ts", "./**/*.tsx"],
|
||||
}
|
||||
11191
admin/package-lock.json
generated
Normal file
11191
admin/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
98
admin/package.json
Normal file
98
admin/package.json
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
{
|
||||
"name": "project-nomad-admin",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "ISC",
|
||||
"author": "Crosstalk Solutions, LLC",
|
||||
"scripts": {
|
||||
"start": "node bin/server.js",
|
||||
"build": "node ace build",
|
||||
"dev": "node ace serve --hmr",
|
||||
"test": "node ace test",
|
||||
"lint": "eslint .",
|
||||
"format": "prettier --write .",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"imports": {
|
||||
"#controllers/*": "./app/controllers/*.js",
|
||||
"#exceptions/*": "./app/exceptions/*.js",
|
||||
"#models/*": "./app/models/*.js",
|
||||
"#mails/*": "./app/mails/*.js",
|
||||
"#services/*": "./app/services/*.js",
|
||||
"#listeners/*": "./app/listeners/*.js",
|
||||
"#events/*": "./app/events/*.js",
|
||||
"#middleware/*": "./app/middleware/*.js",
|
||||
"#validators/*": "./app/validators/*.js",
|
||||
"#providers/*": "./providers/*.js",
|
||||
"#policies/*": "./app/policies/*.js",
|
||||
"#abilities/*": "./app/abilities/*.js",
|
||||
"#database/*": "./database/*.js",
|
||||
"#tests/*": "./tests/*.js",
|
||||
"#start/*": "./start/*.js",
|
||||
"#config/*": "./config/*.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@adonisjs/assembler": "^7.8.2",
|
||||
"@adonisjs/eslint-config": "^2.0.0",
|
||||
"@adonisjs/prettier-config": "^1.4.4",
|
||||
"@adonisjs/tsconfig": "^1.4.0",
|
||||
"@japa/assert": "^4.0.1",
|
||||
"@japa/plugin-adonisjs": "^4.0.0",
|
||||
"@japa/runner": "^4.2.0",
|
||||
"@swc/core": "1.11.24",
|
||||
"@types/dockerode": "^3.3.41",
|
||||
"@types/luxon": "^3.6.2",
|
||||
"@types/node": "^22.15.18",
|
||||
"@types/react": "^19.1.8",
|
||||
"@types/react-dom": "^19.1.6",
|
||||
"eslint": "^9.26.0",
|
||||
"hot-hook": "^0.4.0",
|
||||
"prettier": "^3.5.3",
|
||||
"ts-node-maintained": "^10.9.5",
|
||||
"typescript": "~5.8.3",
|
||||
"vite": "^6.3.5"
|
||||
},
|
||||
"dependencies": {
|
||||
"@adonisjs/auth": "^9.4.0",
|
||||
"@adonisjs/core": "^6.18.0",
|
||||
"@adonisjs/cors": "^2.2.1",
|
||||
"@adonisjs/drive": "^3.4.1",
|
||||
"@adonisjs/inertia": "^3.1.1",
|
||||
"@adonisjs/lucid": "^21.6.1",
|
||||
"@adonisjs/session": "^7.5.1",
|
||||
"@adonisjs/shield": "^8.2.0",
|
||||
"@adonisjs/static": "^1.1.1",
|
||||
"@adonisjs/transmit": "^2.0.2",
|
||||
"@adonisjs/transmit-client": "^1.0.0",
|
||||
"@adonisjs/vite": "^4.0.0",
|
||||
"@headlessui/react": "^2.2.4",
|
||||
"@heroicons/react": "^2.2.0",
|
||||
"@inertiajs/react": "^2.0.13",
|
||||
"@tabler/icons-react": "^3.34.0",
|
||||
"@tailwindcss/vite": "^4.1.10",
|
||||
"@vinejs/vine": "^3.0.1",
|
||||
"@vitejs/plugin-react": "^4.6.0",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"axios": "^1.10.0",
|
||||
"better-sqlite3": "^12.1.1",
|
||||
"dockerode": "^4.0.7",
|
||||
"edge.js": "^6.2.1",
|
||||
"luxon": "^3.6.1",
|
||||
"mysql2": "^3.14.1",
|
||||
"pino-pretty": "^13.0.0",
|
||||
"postcss": "^8.5.6",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"tailwindcss": "^4.1.10",
|
||||
"yaml": "^2.8.0"
|
||||
},
|
||||
"hotHook": {
|
||||
"boundaries": [
|
||||
"./app/controllers/**/*.ts",
|
||||
"./app/middleware/*.ts"
|
||||
]
|
||||
},
|
||||
"prettier": "@adonisjs/prettier-config"
|
||||
}
|
||||
BIN
admin/public/powered_by_crosstalk.png
Normal file
BIN
admin/public/powered_by_crosstalk.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
497
admin/public/powered_by_crosstalk.svg
Normal file
497
admin/public/powered_by_crosstalk.svg
Normal file
|
|
@ -0,0 +1,497 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Generator: Adobe Illustrator 27.7.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
|
||||
<svg
|
||||
version="1.1"
|
||||
id="Layer_1"
|
||||
x="0px"
|
||||
y="0px"
|
||||
viewBox="0 0 700 433.7"
|
||||
style="enable-background:new 0 0 700 433.7;"
|
||||
xml:space="preserve"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"><defs
|
||||
id="defs5242" />
|
||||
<style
|
||||
type="text/css"
|
||||
id="style5015">
|
||||
.st0{fill:#231F20;}
|
||||
.st1{fill:#F05A28;}
|
||||
.st2{fill:none;stroke:#009DDC;stroke-width:6.2344;stroke-miterlimit:10;}
|
||||
.st3{fill:none;stroke:#F05A28;stroke-width:6.2344;stroke-miterlimit:10;}
|
||||
.st4{filter:url(#Adobe_OpacityMaskFilter);}
|
||||
.st5{mask:url(#SVGID_1_);}
|
||||
.st6{fill:#009345;stroke:#231F20;stroke-width:2.4284;stroke-miterlimit:22.9256;}
|
||||
.st7{fill:#8A5D3B;stroke:#231F20;stroke-width:2.4284;stroke-miterlimit:10;}
|
||||
.st8{fill:#8A5D3B;}
|
||||
.st9{fill:#FFFFFF;}
|
||||
.st10{fill:#FFFFFF;stroke:#231F20;stroke-width:1.5643;stroke-miterlimit:10;}
|
||||
.st11{fill:#231F20;stroke:#8A5D3B;stroke-width:2.8948;stroke-miterlimit:10;}
|
||||
.st12{opacity:0.3;fill:#231F20;}
|
||||
.st13{fill:#8A5D3B;stroke:#231F20;stroke-width:2.5345;stroke-miterlimit:10;}
|
||||
.st14{fill:#F05A28;stroke:#231F20;stroke-width:1.4437;stroke-miterlimit:10;}
|
||||
.st15{font-family:'AmericanCaptain01';}
|
||||
.st16{font-size:31.6967px;}
|
||||
</style>
|
||||
<g
|
||||
id="g5155">
|
||||
<g
|
||||
id="g5033">
|
||||
<g
|
||||
id="g5031">
|
||||
<path
|
||||
class="st0"
|
||||
d="M315.1,298.5l-3.5-17.9h20.7c1.5,0,2.3-0.7,2.3-2v-17.5c0-1.3-0.3-2.1-0.8-2.4c-0.5-0.3-1.5-0.4-2.8-0.4h-10 c-1.4,0-2.8-0.1-4.1-0.3c-1.3-0.2-2.4-0.7-3.5-1.5c-1-0.8-1.8-1.9-2.4-3.3c-0.6-1.4-0.9-3.3-0.9-5.7v-32.7c0-3.8,1-6.8,3.1-9.1 c2.1-2.3,5.4-3.4,9.9-3.4h26.6l3.3,17.9h-21.8c-1.6,0-2.4,0.7-2.4,2.1v17.5c0,1,0.2,1.6,0.7,2c0.5,0.4,1.2,0.5,2.2,0.5H344 c3.1,0,5.5,0.7,7.1,2.2c1.6,1.5,2.5,4,2.5,7.6v31.8c0,5.3-1.1,9.1-3.3,11.3c-2.2,2.2-6,3.3-11.5,3.3H315.1z"
|
||||
id="path5017" />
|
||||
<path
|
||||
class="st0"
|
||||
d="M376.6,298.5c-5.6,0-9.5-1.1-11.7-3.5c-2.2-2.3-3.3-6.1-3.3-11.4v-81.5h19.7v77.3c0,0.7,0.1,1.2,0.3,1.6 c0.2,0.4,0.7,0.5,1.5,0.5h6.8c0.8,0,1.3-0.2,1.5-0.5c0.2-0.4,0.3-0.9,0.3-1.6v-77.3h19.7v81.5c0,5.3-1.1,9.1-3.3,11.4 c-2.2,2.3-6.1,3.5-11.7,3.5H376.6z"
|
||||
id="path5019" />
|
||||
<path
|
||||
class="st0"
|
||||
d="M457.2,202.2c3.8,0,6.7,1.1,8.6,3.2c1.9,2.1,2.9,5,2.9,8.5V260c0,1.9-0.2,3.6-0.6,5.1 c-0.4,1.6-1.1,2.9-2.2,4.1c-1.1,1.2-2.5,2.1-4.3,2.7c-1.8,0.7-4,1-6.6,1h-16.5v25.6h-19.1v-96.3H457.2z M438.5,219.1v37.9h9.6 c0.8,0,1.3-0.2,1.5-0.5c0.2-0.3,0.3-0.8,0.3-1.5v-34c0-1.2-0.6-1.9-1.7-1.9H438.5z"
|
||||
id="path5021" />
|
||||
<path
|
||||
class="st0"
|
||||
d="M514.1,202.2c3.8,0,6.7,1.1,8.6,3.2c1.9,2.1,2.9,5,2.9,8.5V260c0,1.9-0.2,3.6-0.6,5.1 c-0.4,1.6-1.1,2.9-2.2,4.1c-1.1,1.2-2.5,2.1-4.3,2.7c-1.8,0.7-4,1-6.6,1h-16.5v25.6h-19.1v-96.3H514.1z M495.4,219.1v37.9h9.6 c0.8,0,1.3-0.2,1.5-0.5c0.2-0.3,0.3-0.8,0.3-1.5v-34c0-1.2-0.6-1.9-1.7-1.9H495.4z"
|
||||
id="path5023" />
|
||||
<path
|
||||
class="st0"
|
||||
d="M583.1,284.8c0,1.9-0.2,3.6-0.7,5.2c-0.4,1.6-1.2,3.1-2.3,4.3c-1.1,1.2-2.5,2.2-4.3,3 c-1.8,0.8-4,1.1-6.6,1.1h-22.3c-2.7,0-4.9-0.4-6.6-1.1c-1.8-0.8-3.2-1.7-4.3-3c-1.1-1.2-1.8-2.7-2.3-4.3 c-0.4-1.6-0.7-3.4-0.7-5.2v-70.2c0-3.8,1-6.8,3.1-9.1c2.1-2.3,5.4-3.4,9.9-3.4h23.9c4.5,0,7.8,1.1,9.9,3.4 c2.1,2.3,3.1,5.3,3.1,9.1V284.8z M554.3,219.1c-1.2,0-1.7,0.6-1.7,1.9v58.7c0,0.7,0.1,1.2,0.3,1.5c0.2,0.3,0.7,0.5,1.5,0.5h7.3 c0.8,0,1.3-0.2,1.5-0.5c0.2-0.3,0.3-0.8,0.3-1.5v-58.7c0-1.2-0.6-1.9-1.7-1.9H554.3z"
|
||||
id="path5025" />
|
||||
<path
|
||||
class="st0"
|
||||
d="M640.6,259.3c0,2.1-0.7,4.1-2.2,5.8c-1.5,1.8-3.6,2.7-6.4,2.7l10.1,30.7h-19.9l-9.3-30.7h-2.5v30.7h-19.3 v-96.3h36.5c4.5,0,7.8,1.1,9.9,3.4c2.1,2.3,3.1,5.3,3.1,9.1V259.3z M610.3,219.1v33.2h9.4c1.2,0,1.9-0.7,1.9-2v-29.2 c0-1.3-0.6-2-1.9-2H610.3z"
|
||||
id="path5027" />
|
||||
<path
|
||||
class="st0"
|
||||
d="M686.6,202.2l4.8,17.9h-12.9v78.4h-19.7v-78.4h-12.9l5-17.9H686.6z"
|
||||
id="path5029" />
|
||||
</g>
|
||||
</g>
|
||||
<g
|
||||
id="g5047">
|
||||
<g
|
||||
id="g5045">
|
||||
<path
|
||||
class="st1"
|
||||
d="M381.4,130.1c0,3-1,5.7-3.1,8.2c-2.1,2.5-5.1,3.7-9,3.7l14.2,43.1h-28l-13-43.1h-3.5V185h-27V49.9h51.3 c6.3,0,11,1.6,13.9,4.8c2.9,3.2,4.4,7.4,4.4,12.8V130.1z M338.9,73.6v46.6h13.2c1.7,0,2.6-0.9,2.6-2.8v-41c0-1.9-0.9-2.8-2.6-2.8 H338.9z"
|
||||
id="path5035" />
|
||||
<path
|
||||
class="st1"
|
||||
d="M463,165.8c0,2.6-0.3,5.1-0.9,7.4c-0.6,2.3-1.7,4.3-3.2,6.1c-1.5,1.7-3.5,3.1-6,4.2 c-2.5,1.1-5.6,1.6-9.3,1.6h-31.3c-3.7,0-6.8-0.5-9.3-1.6c-2.5-1.1-4.5-2.5-6-4.2c-1.5-1.7-2.5-3.8-3.2-6.1 c-0.6-2.3-0.9-4.8-0.9-7.4V67.4c0-5.3,1.5-9.6,4.4-12.8c2.9-3.2,7.5-4.8,13.9-4.8h33.6c6.3,0,11,1.6,13.9,4.8 c2.9,3.2,4.4,7.4,4.4,12.8V165.8z M422.6,73.6c-1.6,0-2.4,0.9-2.4,2.6v82.4c0,1,0.2,1.7,0.5,2.1c0.3,0.4,1,0.7,2.1,0.7H433 c1.1,0,1.8-0.2,2.1-0.7c0.3-0.4,0.5-1.1,0.5-2.1V76.2c0-1.7-0.8-2.6-2.4-2.6H422.6z"
|
||||
id="path5037" />
|
||||
<path
|
||||
class="st1"
|
||||
d="M474.2,66.3c0-5,1.3-8.9,4-11.9c2.7-3,6.7-4.5,12-4.5h38.6l4.7,25.2h-29.6c-1.6,0-2.4,0.9-2.4,2.6v80.1 c0,1.7,0.8,2.6,2.4,2.6h12.3v-53.3h26.3V185h-47.2c-7.8,0-13.3-1.6-16.4-4.8c-3.1-3.2-4.7-8.6-4.7-16V66.3z"
|
||||
id="path5039" />
|
||||
<path
|
||||
class="st1"
|
||||
d="M574.1,185c-7.8,0-13.3-1.6-16.4-4.8c-3.1-3.2-4.7-8.6-4.7-16V49.9h27.6v108.5c0,1,0.2,1.7,0.5,2.2 c0.3,0.5,1,0.7,2.1,0.7h9.5c1.1,0,1.8-0.2,2.1-0.7c0.3-0.5,0.5-1.2,0.5-2.2V49.9H623v114.3c0,7.5-1.6,12.8-4.7,16 c-3.1,3.2-8.6,4.8-16.4,4.8H574.1z"
|
||||
id="path5041" />
|
||||
<path
|
||||
class="st1"
|
||||
d="M634.2,49.9h52.6l4.7,25.2h-29.4v32.1h22.4v23.3h-22.4v29.5h28.3l-4.7,25.2h-51.4V49.9z"
|
||||
id="path5043" />
|
||||
</g>
|
||||
</g>
|
||||
<g
|
||||
id="g5141">
|
||||
<g
|
||||
id="g5055">
|
||||
<path
|
||||
class="st0"
|
||||
d="M266.9,152.3c0,50.2-66.6,122.1-102.4,157.1c-12.3,12.1-32,12.1-44.4,0c-35.8-35-102.4-106.9-102.4-157.1 C17.8,83.7,73.5,28,142.3,28S266.9,83.7,266.9,152.3z"
|
||||
id="path5049" />
|
||||
<path
|
||||
class="st2"
|
||||
d="M266.9,152.3c0,50.2-66.6,122.1-102.4,157.1c-12.3,12.1-32,12.1-44.4,0c-35.8-35-102.4-106.9-102.4-157.1 C17.8,83.7,73.5,28,142.3,28S266.9,83.7,266.9,152.3z"
|
||||
id="path5051" />
|
||||
<path
|
||||
class="st3"
|
||||
d="M142.3,324.5c-9.9,0-19.3-3.8-26.4-10.8C77,275.7,11.7,204.1,11.7,152.3c0-71.9,58.6-130.4,130.6-130.4 c72,0,130.6,58.5,130.6,130.4c0,51.8-65.3,123.4-104.2,161.4C161.6,320.7,152.3,324.5,142.3,324.5z"
|
||||
id="path5053" />
|
||||
</g>
|
||||
<defs
|
||||
id="defs5060">
|
||||
<filter
|
||||
id="Adobe_OpacityMaskFilter"
|
||||
filterUnits="userSpaceOnUse"
|
||||
x="33.7"
|
||||
y="10.2"
|
||||
width="223.3"
|
||||
height="316.2">
|
||||
|
||||
<feColorMatrix
|
||||
type="matrix"
|
||||
values="-1 0 0 0 1 0 -1 0 0 1 0 0 -1 0 1 0 0 0 1 0"
|
||||
color-interpolation-filters="sRGB"
|
||||
result="source"
|
||||
id="feColorMatrix5057" />
|
||||
</filter>
|
||||
</defs>
|
||||
<mask
|
||||
maskUnits="userSpaceOnUse"
|
||||
x="33.7"
|
||||
y="10.2"
|
||||
width="223.3"
|
||||
height="316.2"
|
||||
id="SVGID_1_">
|
||||
<g
|
||||
class="st4"
|
||||
id="g5064">
|
||||
<path
|
||||
class="st0"
|
||||
d="M141,9.7C69.9,9.7,13.2,81.1,13.2,152.3c0,51.3,65.1,122.7,103.8,160.7c7,6.8,16.1,10.2,25.3,10.2 c9.1,0,18.3-3.4,25.3-10.2c38.8-38,103.8-109.4,103.8-160.7C271.4,81.1,212.2,9.7,141,9.7z"
|
||||
id="path5062" />
|
||||
</g>
|
||||
</mask>
|
||||
<g
|
||||
class="st5"
|
||||
mask="url(#SVGID_1_)"
|
||||
id="g5139">
|
||||
<path
|
||||
class="st6"
|
||||
d="M223,202.1l15.3,15.3c0,0,0,61-76.3,76.3"
|
||||
id="path5067" />
|
||||
<path
|
||||
class="st6"
|
||||
d="M158.8,325c-4.8,0.4-8.9,0.1-12-0.9c-7.2-2.4,7.6-22.9,7.6-22.9l50-21.9C204.4,279.4,180.3,314.6,158.8,325z "
|
||||
id="path5069" />
|
||||
<path
|
||||
class="st6"
|
||||
d="M154.4,308.9c5.9-2.9,19.7-7,36.7-13.1c20.4-15.3,67.3-82,64.5-83c-7.2-2.4-6.3-0.9-17.3,4.6 C230.6,232.6,162,301.3,154.4,308.9z"
|
||||
id="path5071" />
|
||||
<path
|
||||
class="st6"
|
||||
d="M70.5,209.7L55.3,225c0,0,0,61,76.3,76.3"
|
||||
id="path5073" />
|
||||
<path
|
||||
class="st6"
|
||||
d="M121.8,323.9c7,1.5,13.2,1.9,17.3,0.2c7.1-2.8,0-15.3,0-15.3L75.6,276C75.6,276,108.5,313.3,121.8,323.9z"
|
||||
id="path5075" />
|
||||
<path
|
||||
class="st6"
|
||||
d="M139.1,316.5c-7.3-3.6-25-10.8-45.8-21.4c-19.2-16.5-60.9-72.8-58.4-73.7c10.5-3.8,8-2.6,20.3,3.6 C62.9,240.2,131.5,308.9,139.1,316.5z"
|
||||
id="path5077" />
|
||||
<g
|
||||
id="g5137">
|
||||
<g
|
||||
id="g5085">
|
||||
<path
|
||||
class="st8"
|
||||
d="M168.2,236.7c-23,11.8-71.6,41.5-71.6,41.5l34.9,44.8c0,0,11.1,1.8,15.3,1.1c0,0,1.9-3.7,4.3-7.6 c21.8-35.1,61.1-101.6,61.1-101.6l-143.4-6.2c0,0,22.4,57.7,27.9,69.5"
|
||||
id="path5079" />
|
||||
<g
|
||||
id="g5083">
|
||||
<path
|
||||
class="st0"
|
||||
d="M167.6,235.7c-5.4,2.8-10.6,5.7-15.9,8.6c-6.1,3.5-12.2,7-18.3,10.5c-5.9,3.4-11.8,6.9-17.6,10.4 c-4.5,2.7-9,5.4-13.5,8.1c-2.1,1.2-4.2,2.4-6.2,3.7c0,0-0.1,0-0.1,0.1c-0.7,0.4-0.7,1.3-0.2,1.9c1.9,2.5,3.9,5,5.8,7.5 c4.2,5.4,8.4,10.8,12.6,16.2c4,5.2,8,10.3,12,15.5c1.2,1.5,2.4,3,3.6,4.6c0.3,0.3,0.5,0.7,0.8,1c0.4,0.4,0.9,0.5,1.4,0.5 c3.9,0.6,7.8,1.1,11.7,1.2c0.9,0,1.8,0,2.6-0.1c0.6-0.1,1.2-0.1,1.5-0.7c0.1-0.2,0.2-0.4,0.3-0.6c1.3-2.5,2.7-4.9,4.1-7.3 c4.7-7.6,9.4-15.2,14-22.9c5.2-8.5,10.3-17,15.4-25.5c4.8-8.1,9.7-16.1,14.5-24.2c3.7-6.3,7.5-12.6,11.2-18.9 c1.9-3.1,3.7-6.3,5.6-9.4c0.1-0.1,0.2-0.3,0.2-0.4c0.5-0.8-0.1-1.8-1-1.8c-1.3-0.1-2.6-0.1-3.9-0.2c-3.5-0.2-7-0.3-10.4-0.5 c-5.1-0.2-10.3-0.4-15.4-0.7c-6.3-0.3-12.6-0.5-18.9-0.8c-6.8-0.3-13.7-0.6-20.5-0.9c-7-0.3-13.9-0.6-20.9-0.9 c-6.5-0.3-12.9-0.6-19.4-0.8c-5.5-0.2-10.9-0.5-16.4-0.7c-3.9-0.2-7.9-0.3-11.8-0.5c-1.9-0.1-3.7-0.2-5.6-0.2 c-0.1,0-0.2,0-0.2,0c-0.7,0-1.5,0.8-1.2,1.5c0.8,2,1.5,3.9,2.3,5.9c1.9,4.8,3.8,9.6,5.7,14.4c2.4,6.1,4.8,12.2,7.3,18.4 c2.3,5.9,4.7,11.8,7.1,17.6c1.7,4.2,3.4,8.4,5.3,12.5c0.1,0.3,0.3,0.6,0.4,1c0.3,0.6,1.1,0.7,1.7,0.4c0.6-0.4,0.7-1.1,0.4-1.7 c-0.7-1.5-1.4-3.1-2.1-4.6c-0.5-1.2-0.8-1.9-1.3-3c-0.5-1.2-1-2.4-1.5-3.6c-2.4-5.7-4.7-11.5-7-17.2 c-2.5-6.2-4.9-12.3-7.3-18.5c-2-5-3.9-10-5.9-15.1c-0.6-1.6-1.3-3.2-1.9-4.8c-0.3-0.7-0.5-1.6-0.9-2.3c0,0,0-0.1,0-0.1 c-0.4,0.5-0.8,1-1.2,1.5c1.3,0.1,2.6,0.1,3.9,0.2c3.5,0.2,7,0.3,10.4,0.5c5.1,0.2,10.3,0.4,15.4,0.7 c6.3,0.3,12.6,0.5,18.9,0.8c6.8,0.3,13.7,0.6,20.5,0.9c7,0.3,13.9,0.6,20.9,0.9c6.5,0.3,12.9,0.6,19.4,0.8 c5.5,0.2,10.9,0.5,16.4,0.7c3.9,0.2,7.9,0.3,11.8,0.5c1.8,0.1,3.7,0.2,5.6,0.2c0.1,0,0.2,0,0.2,0c-0.3-0.6-0.7-1.2-1-1.8 c-1.5,2.6-3,5.1-4.5,7.6c-3.9,6.5-7.7,13-11.6,19.5c-5.2,8.7-10.4,17.5-15.6,26.2c-5.6,9.3-11.1,18.5-16.7,27.7 c-2.6,4.3-5.2,8.5-7.8,12.8c-2.8,4.5-5.6,8.9-8.1,13.6c-0.3,0.6-0.6,1.2-0.9,1.8c0.2-0.2,0.5-0.4,0.7-0.6c0.3,0-0.2,0-0.2,0 c-0.2,0-0.3,0-0.5,0c-0.3,0-0.6,0-1,0c-0.7,0-1.5,0-2.2-0.1c-1.7-0.1-3.3-0.2-5-0.4c-2.1-0.2-3.7-0.4-5.8-0.8 c0.2,0.1,0.4,0.2,0.5,0.3c-1.2-1.5-2.3-3-3.5-4.5c-2.8-3.6-5.6-7.1-8.3-10.7c-3.4-4.3-6.7-8.6-10.1-12.9 c-2.9-3.7-5.8-7.4-8.7-11.1c-1-1.2-1.9-2.5-2.9-3.7c-0.4-0.6-0.9-1.2-1.4-1.8c0,0,0-0.1-0.1-0.1c-0.1,0.6-0.2,1.3-0.2,1.9 c1.7-1,3.3-2,5-3c4.2-2.6,8.5-5.1,12.8-7.7c5.8-3.4,11.5-6.9,17.3-10.3c6.1-3.6,12.2-7.1,18.4-10.6c5.4-3.1,10.9-6.1,16.4-9 c0.6-0.3,1.1-0.6,1.7-0.9C170.3,237,169,234.9,167.6,235.7L167.6,235.7z"
|
||||
id="path5081" />
|
||||
</g>
|
||||
</g>
|
||||
<polygon
|
||||
class="st9"
|
||||
points="177.5,201 171.7,220.6 162,238 135.9,253.2 114.9,234.7 98.2,198.3 104.8,203.4 116.9,215.3 131.9,225.9 137.8,228.4 144.6,228 151.6,224.8 162.9,216 "
|
||||
id="polygon5087" />
|
||||
<path
|
||||
class="st10"
|
||||
d="M96.7,193.7c1.3,2.5,29.9,36.5,45.2,34.6c12.7,0.1,39.5-30.1,44.4-39.2l10.6-21.3l2.4-15.5v-13.4l1.5-24.7 l0.5-15.6l-24.1-28.4l-30.5-5.5c0,0-19.2,9.7-20.9,9.7S96.7,89,96.7,89l-15.4,15.4l0.5,7.7l3.2,57.6L96.7,193.7z"
|
||||
id="path5089" />
|
||||
<g
|
||||
id="g5095">
|
||||
<path
|
||||
class="st0"
|
||||
d="M155,81.8c-3.4,0.6-4.9,5.7-7.6,8c-7.9,6.4-26.3,12.3-33.8,9.7c10.2-0.6,9.9-8.2,17.5-11.3 c-6.7,1.4-25.3,13.2-36.2,9.3c3.3-2.1,2.7-15.1,3-16.8c2.7,0.4,45.1-5.9,52.9-5.5C160.5,75.5,155,81.8,155,81.8z"
|
||||
id="path5091" />
|
||||
<path
|
||||
class="st0"
|
||||
d="M153.5,80.5c1.1,0,8.9,14,1,16.5c4.4,2.9,13.4-5.7,14.4-7.8c1-2-13.4-12.8-13.4-12.8L153.5,80.5z"
|
||||
id="path5093" />
|
||||
</g>
|
||||
<path
|
||||
class="st11"
|
||||
d="M197.8,105.2c0,0,29.8,18.8,31.6,20.6c4.5,4.5,53.8,71.7-63.4,115.7c-15,4.3-12.1,2-7.5-1.6 c10-7.9,20.1-24.2,24.2-36.4C207.2,179.3,197.8,105.2,197.8,105.2z"
|
||||
id="path5097" />
|
||||
<path
|
||||
class="st0"
|
||||
d="M196.5,105.2c0,0,29.8,18.8,31.6,20.6c4.8,4.8,54.4,71.4-63.4,115.7c-15,4.3-66.1,36-68,36.7 c0.9-1.7,59.1-39.4,63.8-43c10-7.9,13-23.4,17.2-35.6C202,175.3,196.5,105.2,196.5,105.2z"
|
||||
id="path5099" />
|
||||
<path
|
||||
class="st11"
|
||||
d="M59.8,120.8c-3.2,3.2-73.7,57.8,53.1,122.5c3.6,2.6,23.4,10.7,23.4,10.7c-9.4-6.7-48-58.1-50.4-62.5 c-9.4-14.4-12.1-57.5-12.1-72.7C69.2,121.1,59.8,120.8,59.8,120.8z"
|
||||
id="path5101" />
|
||||
<path
|
||||
class="st0"
|
||||
d="M61.3,120.8c-3.2,3.2-73.7,57.8,53.1,122.5c3.6,2.6,23.4,10.7,23.4,10.7c-9.4-6.7-37.9-56.6-40.4-61.1 C88,178.5,84.3,127.3,84.3,112C79.7,114.4,61.3,120.8,61.3,120.8z"
|
||||
id="path5103" />
|
||||
<path
|
||||
class="st9"
|
||||
d="M200.8,119.2c2.5-0.4,9.4,12.4,6.4,24c-0.6,2.3-4.2,2.6-5.2,0.5L200.8,119.2z"
|
||||
id="path5105" />
|
||||
<path
|
||||
class="st9"
|
||||
d="M81.3,119.2c-2.5-0.4-9.4,12.4-6.4,24c0.6,2.3,4.2,2.6,5.2,0.5L81.3,119.2z"
|
||||
id="path5107" />
|
||||
<g
|
||||
id="g5121">
|
||||
<g
|
||||
id="g5113">
|
||||
<path
|
||||
class="st0"
|
||||
d="M92.5,117.6L92.5,117.6c1.5-0.5,2.8-0.8,3.8-0.8c-2.2,3.6-5.3,11.4-5,16c-2-2.7-4-2.9-7.2-3.2l-2.4-10 c1.3-0.8,4.4,0.7,5.9-0.1C89.4,118.8,91.1,118.1,92.5,117.6"
|
||||
id="path5109" />
|
||||
<path
|
||||
class="st0"
|
||||
d="M188.8,117.1c-1.5-0.5-3-0.9-4.2-0.9c2.5,4.3,5.6,12,5,16c2-2.7,4.9-5.5,8-5.7v-4.8c-1.5-1-3.3-2.1-5.2-3 C191.2,118.1,190,117.5,188.8,117.1"
|
||||
id="path5111" />
|
||||
</g>
|
||||
<g
|
||||
id="g5119">
|
||||
<path
|
||||
class="st0"
|
||||
d="M127.3,121.2H99.6c-0.8,0-1.6,0.3-2.2,0.9l0,0c-0.6,0.5-0.9,1.3-0.9,2.2v12.5c0,2.9,1.2,5.5,3,7.3l0,0 c1.9,1.9,4.5,3,7.3,3h13.7c2.7,0,5.1-1.1,6.9-2.9c1.8-1.8,2.9-4.2,2.9-6.9v-13.1c0-0.8-0.3-1.6-0.9-2.2h0 C128.9,121.6,128.1,121.2,127.3,121.2 M99.6,116.1h27.7c2.3,0,4.3,0.9,5.8,2.4l0,0c1.5,1.5,2.4,3.5,2.4,5.8v3.5h12.9v-3.5 c0-2.3,0.9-4.3,2.4-5.8l0,0l0,0c1.5-1.5,3.5-2.4,5.8-2.4h27.7c2.3,0,4.3,0.9,5.8,2.4l0,0c1.5,1.5,2.4,3.5,2.4,5.8v12.5 c0,4.3-1.7,8.2-4.6,11l0,0c-2.8,2.8-6.7,4.6-11,4.6h-13.7c-4.1,0-7.8-1.7-10.5-4.4c-2.7-2.7-4.4-6.4-4.4-10.6v-4.5h-12.9v4.5 c0,4.1-1.7,7.8-4.4,10.6c-2.7,2.7-6.4,4.4-10.6,4.4h-13.7c-4.3,0-8.1-1.7-11-4.6l0,0l0,0c-2.8-2.8-4.6-6.7-4.6-11v-12.5 c0-2.3,0.9-4.3,2.4-5.8l0,0l0,0C95.2,117,97.3,116.1,99.6,116.1z M184.4,121.2h-27.7c-0.8,0-1.6,0.3-2.2,0.9l0,0 c-0.6,0.5-0.9,1.3-0.9,2.2v13.1c0,2.7,1.1,5.1,2.9,6.9c1.8,1.8,4.2,2.9,6.9,2.9h13.7c2.9,0,5.5-1.2,7.3-3.1h0 c1.9-1.9,3-4.5,3-7.3v-12.5c0-0.8-0.3-1.6-0.9-2.2h0C186,121.6,185.2,121.2,184.4,121.2z"
|
||||
id="path5115" />
|
||||
<path
|
||||
class="st0"
|
||||
d="M188.8,117.1c-1.5-0.5-3-0.9-4.2-0.9c2.5,4.3,5.6,12,5,16c2-2.7,4.9-5.5,8-5.7v-4.8c-1.5-1-3.3-2.1-5.2-3 C191.2,118.1,190,117.5,188.8,117.1"
|
||||
id="path5117" />
|
||||
</g>
|
||||
</g>
|
||||
<g
|
||||
id="g5125">
|
||||
<path
|
||||
class="st0"
|
||||
d="M103.6,177.1c6.4,5.4,13.7,9.5,21.6,12.3c7.2,2.5,14.8,3.9,22.4,4.2c3.9,0.1,7.8,0,11.7-0.5 c0.6-0.1,1.1-0.4,1.1-1.1c0-0.5-0.5-1.2-1.1-1.1c-6.9,0.9-14.1,0.7-20.9-0.4c-7.6-1.2-15.1-3.7-21.9-7.3 c-4.1-2.2-7.9-4.7-11.4-7.7c-0.4-0.4-1.1-0.4-1.5,0C103.2,176,103.2,176.8,103.6,177.1L103.6,177.1z"
|
||||
id="path5123" />
|
||||
</g>
|
||||
<g
|
||||
id="g5131">
|
||||
<g
|
||||
id="g5129">
|
||||
<path
|
||||
class="st0"
|
||||
d="M137,200.4c3.7,1.1,7.7,1.3,11.5,0.9c1.7-0.2,3.3-0.4,4.9-0.9c1.1-0.4,2.3-0.8,3.2-1.7 c0.5-0.5-0.3-1.2-0.7-0.7c-0.8,0.8-1.9,1.2-3,1.5c-1.5,0.5-3.1,0.7-4.7,0.9c-3.6,0.3-7.3,0.1-10.8-0.9 C136.6,199.2,136.3,200.2,137,200.4L137,200.4z"
|
||||
id="path5127" />
|
||||
</g>
|
||||
</g>
|
||||
<path
|
||||
class="st12"
|
||||
d="M82.4,150.6c4.5-4.5,57.3-31.6,64.3-38.6c9.5-9.5,63.8,48.8,68.1,48.8c4.3,0,21.2-23.4,21.2-23.4 L209.9,100l-45.3-25.6l-26.8-9.7L103.3,75L88.5,91.5l-20.2,18.3l-7,11l-5.2,12L82.4,150.6z"
|
||||
id="path5133" />
|
||||
<path
|
||||
class="st13"
|
||||
d="M152.9,76.2c3.9,6.2,23.7,18.6,42.5,29.9c13.5,8.1,33.1,20.4,40.6,31.3C227.9,82.5,156.3,4.6,143,12 c-2.6,22.8-39.6-0.9-86.9,113.7c5.2-4.9,16.7-8.5,25.7-13.7C108.1,96.8,147.7,68,152.9,76.2z"
|
||||
id="path5135" />
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g
|
||||
id="g5153">
|
||||
<g
|
||||
id="g5145">
|
||||
<circle
|
||||
class="st14"
|
||||
cx="103.3"
|
||||
cy="270.6"
|
||||
r="16.6"
|
||||
id="circle5143" />
|
||||
</g>
|
||||
<g
|
||||
id="g5151">
|
||||
<path
|
||||
class="st0"
|
||||
d="M102.8,272.8c0,0.4-0.1,0.8-0.4,1.1c-0.3,0.3-0.7,0.5-1.2,0.5l2,6h-3.9l-1.8-6h-0.5v6h-3.7v-18.7h7.1 c0.9,0,1.5,0.2,1.9,0.7c0.4,0.4,0.6,1,0.6,1.8V272.8z M96.9,265v6.4h1.8c0.2,0,0.4-0.1,0.4-0.4v-5.7c0-0.3-0.1-0.4-0.4-0.4H96.9z "
|
||||
id="path5147" />
|
||||
<path
|
||||
class="st0"
|
||||
d="M105.2,280.4l-0.7-3.5h4c0.3,0,0.4-0.1,0.4-0.4v-3.4c0-0.3-0.1-0.4-0.2-0.5c-0.1-0.1-0.3-0.1-0.5-0.1h-1.9 c-0.3,0-0.5,0-0.8-0.1c-0.2,0-0.5-0.1-0.7-0.3c-0.2-0.2-0.4-0.4-0.5-0.6s-0.2-0.6-0.2-1.1v-6.3c0-0.7,0.2-1.3,0.6-1.8 c0.4-0.4,1-0.7,1.9-0.7h5.2l0.6,3.5h-4.2c-0.3,0-0.5,0.1-0.5,0.4v3.4c0,0.2,0,0.3,0.1,0.4c0.1,0.1,0.2,0.1,0.4,0.1h2.3 c0.6,0,1.1,0.1,1.4,0.4c0.3,0.3,0.5,0.8,0.5,1.5v6.2c0,1-0.2,1.8-0.6,2.2c-0.4,0.4-1.2,0.6-2.2,0.6H105.2z"
|
||||
id="path5149" />
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<rect
|
||||
x="7.4"
|
||||
y="345.7"
|
||||
class="st0"
|
||||
width="685.2"
|
||||
height="76.5"
|
||||
id="rect5157" />
|
||||
<g
|
||||
id="g5237">
|
||||
<g
|
||||
id="g5161">
|
||||
<g
|
||||
aria-label="Powered by"
|
||||
transform="matrix(1 0 0 1 30.9827 395.8971)"
|
||||
id="text5159"
|
||||
class="st9 st15 st16"
|
||||
style="font-size:31.6967px;font-family:AmericanCaptain01;fill:#ffffff"><path
|
||||
d="m 5.0080785,-19.556863 v 10.0161566 h 2.5674326 q 0.5388439,0 0.6656307,-0.1901802 0.1267868,-0.2218769 0.1267868,-0.6656304 v -8.431322 q 0,-0.380361 -0.1584835,-0.538844 -0.1584835,-0.19018 -0.6022373,-0.19018 z m 7.2902405,10.5866971 q 0,1.267868 -0.792417,1.9968921 -0.792418,0.6973274 -2.2821625,0.6973274 H 5.0080785 V 0 H 0.95090097 V -23.0435 H 9.4456163 q 1.4897447,0 2.1553757,0.729024 0.697327,0.729024 0.697327,1.965195 z"
|
||||
id="path9470" /><path
|
||||
d="m 20.34931,-3.3281534 q 0.348664,0 0.475451,-0.09509 0.158483,-0.09509 0.158483,-0.5071472 v -9.2554363 q 0,-0.412057 -0.158483,-0.538844 -0.158484,-0.126787 -0.507147,-0.126787 h -1.933499 q -0.316967,0 -0.507147,0.126787 -0.158484,0.126787 -0.158484,0.538844 v 9.2554363 q 0,0.4120571 0.158484,0.5071472 0.158483,0.09509 0.47545,0.09509 z M 14.105061,-14.834055 q 0,-2.123679 2.091982,-2.123679 h 6.307643 q 2.091982,0 2.091982,2.123679 V -2.1236788 Q 24.596668,0 22.504686,0 h -6.307643 q -2.091982,0 -2.091982,-2.1236788 z"
|
||||
id="path9472" /><path
|
||||
d="M 35.215088,-9.1286494 33.440073,0 h -3.518334 l -4.120571,-16.957734 h 4.120571 L 31.982025,-5.9589794 34.1374,-16.957734 h 2.250466 l 1.996892,10.8719678 2.028589,-10.8719678 h 4.057177 L 40.47674,0 h -3.581727 z"
|
||||
id="path9474" /><path
|
||||
d="m 49.288408,-4.1839643 q 0,0.4437538 0.158483,0.5071472 0.158484,0.063393 0.475451,0.063393 h 5.642012 L 54.803633,0 h -7.19515 Q 46.689278,0 46.182131,-0.47545049 45.674984,-0.98259767 45.674984,-1.9018019 V -14.897449 q 0,-1.045991 0.538844,-1.553138 0.57054,-0.507147 1.489745,-0.507147 h 6.117463 q 0.919204,0 1.458048,0.507147 0.538844,0.475451 0.538844,1.521442 v 8.5898052 h -6.52952 z m 0,-4.7228082 h 3.201366 V -13.05904 q 0,-0.412057 -0.19018,-0.507147 -0.158483,-0.126787 -0.47545,-0.126787 h -1.901802 q -0.316967,0 -0.475451,0.126787 -0.158483,0.09509 -0.158483,0.507147 z"
|
||||
id="path9476" /><path
|
||||
d="m 57.7197,-16.957734 h 3.708514 v 1.775015 l 1.521441,-1.775015 h 3.391547 l -0.982598,3.518334 h -2.187072 l -1.648228,1.965195 V 0 H 57.7197 Z"
|
||||
id="path9478" /><path
|
||||
d="m 70.96895,-4.1839643 q 0,0.4437538 0.158483,0.5071472 0.158484,0.063393 0.475451,0.063393 h 5.642012 L 76.484175,0 h -7.19515 Q 68.36982,0 67.862673,-0.47545049 67.355526,-0.98259767 67.355526,-1.9018019 V -14.897449 q 0,-1.045991 0.538844,-1.553138 0.57054,-0.507147 1.489745,-0.507147 h 6.117463 q 0.919204,0 1.458048,0.507147 0.538844,0.475451 0.538844,1.521442 v 8.5898052 h -6.52952 z m 0,-4.7228082 h 3.201366 V -13.05904 q 0,-0.412057 -0.19018,-0.507147 -0.158483,-0.126787 -0.47545,-0.126787 h -1.901802 q -0.316967,0 -0.475451,0.126787 -0.158483,0.09509 -0.158483,0.507147 z"
|
||||
id="path9480" /><path
|
||||
d="M 86.278426,-16.957734 V -23.0435 h 3.613423 V 0 H 86.278426 V -1.7750152 L 84.788681,0 H 81.365437 Q 80.414536,0 79.907389,-0.50714719 79.400242,-1.0459911 79.400242,-1.996892 v -12.837163 q 0,-2.123679 2.155375,-2.123679 z m -2.630827,3.26476 q -0.316967,0 -0.47545,0.126787 -0.158484,0.09509 -0.158484,0.507147 v 8.9701658 q 0,0.4437538 0.158484,0.5388439 0.158483,0.063393 0.47545,0.063393 h 2.630827 V -13.692974 Z"
|
||||
id="path9482" /><path
|
||||
d="m 104.0603,-16.957734 q 2.18707,0 2.18707,2.091982 v 12.86886 q 0,0.9509009 -0.50715,1.48974481 Q 105.26477,0 104.31387,0 h -3.42324 L 99.369188,-1.7750152 V 0 h -3.613424 v -23.0435 h 3.613424 v 6.085766 z m -4.691112,3.26476 v 10.2063371 h 2.599132 q 0.34866,0 0.50714,-0.09509 0.15849,-0.09509 0.15849,-0.5388439 v -8.9067721 q 0,-0.412057 -0.15849,-0.538844 -0.15848,-0.126787 -0.50714,-0.126787 z"
|
||||
id="path9484" /><path
|
||||
d="m 112.9037,0 h -1.61653 l -3.8353,-16.957734 h 4.12057 l 2.28216,12.5201961 2.66252,-12.5201961 h 4.05718 l -5.7054,23.6140408 h -3.55003 z"
|
||||
id="path9486" /></g>
|
||||
</g>
|
||||
<g
|
||||
id="g5235">
|
||||
<g
|
||||
id="g5203">
|
||||
<g
|
||||
id="g5181">
|
||||
<path
|
||||
class="st9"
|
||||
d="M236.9,387c0-1.7,0.3-3.4,0.8-4.9c0.6-1.5,1.4-2.9,2.4-4c1.1-1.1,2.3-2,3.9-2.7c1.5-0.7,3.2-1,5.2-1 c1.3,0,2.4,0.1,3.5,0.3c1.1,0.2,2.1,0.6,3.1,1l-1.4,5.4c-0.6-0.2-1.3-0.5-2.1-0.6c-0.8-0.2-1.6-0.3-2.5-0.3 c-2,0-3.5,0.6-4.5,1.9c-1,1.2-1.5,2.9-1.5,4.9c0,2.1,0.5,3.8,1.4,5c0.9,1.2,2.5,1.8,4.8,1.8c0.8,0,1.7-0.1,2.6-0.2 c0.9-0.2,1.8-0.4,2.6-0.7l1,5.5c-0.8,0.3-1.8,0.6-2.9,0.9c-1.2,0.2-2.5,0.4-3.9,0.4c-2.2,0-4.1-0.3-5.6-1 c-1.6-0.7-2.9-1.5-3.9-2.7c-1-1.1-1.8-2.4-2.2-4C237.1,390.4,236.9,388.8,236.9,387z"
|
||||
id="path5163" />
|
||||
<path
|
||||
class="st9"
|
||||
d="M275.1,381.1c-0.6-0.2-1.3-0.3-2.1-0.5c-0.8-0.2-1.7-0.2-2.6-0.2c-0.4,0-0.9,0-1.5,0.1 c-0.6,0.1-1,0.2-1.3,0.2v18.2h-6.8v-22.6c1.2-0.4,2.6-0.8,4.3-1.2c1.6-0.4,3.5-0.6,5.5-0.6c0.4,0,0.8,0,1.3,0.1 c0.5,0,1,0.1,1.5,0.2c0.5,0.1,1,0.2,1.5,0.3c0.5,0.1,1,0.2,1.3,0.4L275.1,381.1z"
|
||||
id="path5165" />
|
||||
<path
|
||||
class="st9"
|
||||
d="M301.7,387c0,1.9-0.3,3.6-0.8,5.2c-0.5,1.6-1.3,2.9-2.4,4c-1,1.1-2.3,2-3.7,2.6c-1.4,0.6-3,0.9-4.8,0.9 c-1.8,0-3.4-0.3-4.8-0.9c-1.4-0.6-2.7-1.5-3.7-2.6c-1-1.1-1.8-2.4-2.4-4c-0.6-1.6-0.9-3.3-0.9-5.2c0-1.9,0.3-3.6,0.9-5.1 c0.6-1.5,1.4-2.9,2.5-3.9c1-1.1,2.3-1.9,3.7-2.5c1.4-0.6,3-0.9,4.7-0.9c1.7,0,3.3,0.3,4.7,0.9c1.4,0.6,2.7,1.5,3.7,2.5 c1,1.1,1.8,2.4,2.4,3.9C301.5,383.4,301.7,385.1,301.7,387z M294.8,387c0-2.1-0.4-3.7-1.2-4.9c-0.8-1.2-2-1.8-3.6-1.8 c-1.5,0-2.7,0.6-3.6,1.8c-0.8,1.2-1.3,2.8-1.3,4.9s0.4,3.7,1.3,5c0.8,1.2,2,1.8,3.6,1.8c1.5,0,2.7-0.6,3.6-1.8 C294.4,390.7,294.8,389,294.8,387z"
|
||||
id="path5167" />
|
||||
<path
|
||||
class="st9"
|
||||
d="M313.8,394.1c1.2,0,2.1-0.1,2.6-0.4c0.5-0.2,0.8-0.7,0.8-1.4c0-0.5-0.3-1-1-1.4c-0.7-0.4-1.7-0.9-3-1.4 c-1.1-0.4-2-0.8-2.9-1.2c-0.9-0.4-1.6-0.9-2.2-1.5c-0.6-0.6-1.1-1.3-1.4-2.1c-0.3-0.8-0.5-1.8-0.5-2.9c0-2.2,0.8-4,2.5-5.3 c1.7-1.3,3.9-2,6.9-2c1.5,0,2.8,0.1,4.2,0.4c1.3,0.3,2.4,0.5,3.2,0.8l-1.2,5.3c-0.8-0.3-1.6-0.5-2.6-0.7c-0.9-0.2-2-0.3-3.1-0.3 c-2.1,0-3.2,0.6-3.2,1.8c0,0.3,0,0.5,0.1,0.7c0.1,0.2,0.3,0.4,0.5,0.6c0.3,0.2,0.6,0.4,1.1,0.6c0.5,0.2,1.1,0.5,1.8,0.8 c1.5,0.5,2.7,1.1,3.7,1.6c1,0.5,1.7,1.1,2.3,1.7c0.6,0.6,1,1.3,1.2,2.1c0.2,0.8,0.3,1.6,0.3,2.6c0,2.4-0.9,4.1-2.7,5.3 c-1.8,1.2-4.3,1.8-7.5,1.8c-2.1,0-3.9-0.2-5.3-0.5c-1.4-0.4-2.4-0.7-2.9-0.9l1.1-5.5c1.1,0.5,2.3,0.8,3.5,1.1 C311.4,394,312.6,394.1,313.8,394.1z"
|
||||
id="path5169" />
|
||||
<path
|
||||
class="st9"
|
||||
d="M335.8,394.1c1.2,0,2.1-0.1,2.6-0.4c0.5-0.2,0.8-0.7,0.8-1.4c0-0.5-0.3-1-1-1.4c-0.7-0.4-1.7-0.9-3-1.4 c-1.1-0.4-2-0.8-2.9-1.2c-0.9-0.4-1.6-0.9-2.2-1.5c-0.6-0.6-1.1-1.3-1.4-2.1c-0.3-0.8-0.5-1.8-0.5-2.9c0-2.2,0.8-4,2.5-5.3 c1.7-1.3,3.9-2,6.9-2c1.5,0,2.8,0.1,4.2,0.4c1.3,0.3,2.4,0.5,3.2,0.8l-1.2,5.3c-0.8-0.3-1.6-0.5-2.6-0.7c-0.9-0.2-2-0.3-3.1-0.3 c-2.1,0-3.2,0.6-3.2,1.8c0,0.3,0,0.5,0.1,0.7c0.1,0.2,0.3,0.4,0.5,0.6c0.3,0.2,0.6,0.4,1.1,0.6c0.5,0.2,1.1,0.5,1.8,0.8 c1.5,0.5,2.7,1.1,3.7,1.6c1,0.5,1.7,1.1,2.3,1.7c0.6,0.6,1,1.3,1.2,2.1s0.3,1.6,0.3,2.6c0,2.4-0.9,4.1-2.7,5.3 c-1.8,1.2-4.3,1.8-7.5,1.8c-2.1,0-3.9-0.2-5.3-0.5c-1.4-0.4-2.4-0.7-2.9-0.9l1.1-5.5c1.1,0.5,2.3,0.8,3.5,1.1 C333.5,394,334.6,394.1,335.8,394.1z"
|
||||
id="path5171" />
|
||||
<path
|
||||
class="st9"
|
||||
d="M350.8,369.1l6.8-1.1v7h8.1v5.6h-8.1v8.4c0,1.4,0.2,2.6,0.7,3.4c0.5,0.8,1.5,1.3,3,1.3 c0.7,0,1.5-0.1,2.2-0.2c0.8-0.1,1.5-0.3,2.1-0.6l1,5.3c-0.8,0.3-1.7,0.6-2.7,0.9c-1,0.2-2.2,0.4-3.7,0.4c-1.8,0-3.4-0.2-4.6-0.7 c-1.2-0.5-2.2-1.2-2.9-2.1c-0.7-0.9-1.2-2-1.5-3.2c-0.3-1.3-0.4-2.7-0.4-4.2V369.1z"
|
||||
id="path5173" />
|
||||
<path
|
||||
class="st9"
|
||||
d="M378.8,374.4c2,0,3.7,0.2,5,0.7c1.3,0.5,2.4,1.1,3.2,2c0.8,0.8,1.4,1.9,1.7,3.1c0.3,1.2,0.5,2.6,0.5,4v14.1 c-1,0.2-2.3,0.5-4,0.7c-1.7,0.3-3.8,0.4-6.3,0.4c-1.5,0-2.9-0.1-4.2-0.4c-1.3-0.3-2.3-0.7-3.2-1.3c-0.9-0.6-1.6-1.4-2.1-2.4 c-0.5-1-0.7-2.2-0.7-3.7c0-1.4,0.3-2.6,0.8-3.5c0.6-1,1.3-1.7,2.2-2.3c0.9-0.6,2-1,3.2-1.2c1.2-0.3,2.5-0.4,3.8-0.4 c0.9,0,1.7,0,2.3,0.1c0.7,0.1,1.2,0.2,1.7,0.3v-0.6c0-1.1-0.3-2.1-1-2.8c-0.7-0.7-1.9-1-3.6-1c-1.2,0-2.3,0.1-3.4,0.2 c-1.1,0.2-2.1,0.4-2.9,0.7l-0.9-5.4c0.4-0.1,0.9-0.2,1.5-0.4c0.6-0.1,1.2-0.3,1.9-0.4c0.7-0.1,1.4-0.2,2.2-0.3 C377.2,374.5,378,374.4,378.8,374.4z M379.3,394.2c0.7,0,1.3,0,1.9,0c0.6,0,1.1-0.1,1.5-0.1v-5.1c-0.3-0.1-0.7-0.1-1.2-0.2 c-0.5-0.1-1-0.1-1.5-0.1c-0.6,0-1.2,0-1.8,0.1c-0.6,0.1-1.1,0.2-1.5,0.4c-0.4,0.2-0.8,0.5-1,0.9c-0.2,0.4-0.4,0.8-0.4,1.4 c0,1.1,0.4,1.8,1.1,2.2C377.1,394,378.1,394.2,379.3,394.2z"
|
||||
id="path5175" />
|
||||
<path
|
||||
class="st9"
|
||||
d="M404.8,399.4c-2,0-3.6-0.2-4.8-0.6c-1.2-0.4-2.2-0.9-2.9-1.7c-0.7-0.7-1.2-1.6-1.5-2.6 c-0.3-1-0.4-2.2-0.4-3.4v-26.3l6.8-1.1v26c0,0.6,0,1.2,0.1,1.6c0.1,0.5,0.3,0.9,0.5,1.2c0.3,0.3,0.6,0.6,1.1,0.8 c0.5,0.2,1.1,0.3,2,0.4L404.8,399.4z"
|
||||
id="path5177" />
|
||||
<path
|
||||
class="st9"
|
||||
d="M416.4,383.6c0.7-0.7,1.4-1.5,2.1-2.3c0.7-0.8,1.4-1.6,2.1-2.3c0.7-0.8,1.3-1.5,1.9-2.2 c0.6-0.7,1.1-1.3,1.5-1.8h8c-1.6,1.8-3.2,3.6-4.7,5.3c-1.5,1.7-3.2,3.4-5,5.2c0.9,0.8,1.8,1.8,2.8,2.9c1,1.1,1.9,2.3,2.8,3.5 c0.9,1.2,1.7,2.4,2.5,3.6c0.8,1.2,1.4,2.3,1.9,3.3h-7.8c-0.5-0.8-1-1.7-1.7-2.6c-0.6-1-1.3-1.9-2-2.9c-0.7-1-1.4-1.9-2.2-2.8 c-0.8-0.9-1.5-1.6-2.3-2.2v10.5h-6.8v-34.1l6.8-1.1V383.6z"
|
||||
id="path5179" />
|
||||
</g>
|
||||
<g
|
||||
id="g5201">
|
||||
<path
|
||||
class="st9"
|
||||
d="M448.9,395.4c1.6,0,2.8-0.3,3.6-0.8c0.7-0.6,1.1-1.3,1.1-2.3c0-0.6-0.1-1.1-0.4-1.6 c-0.3-0.4-0.6-0.8-1.1-1.2c-0.5-0.3-1-0.7-1.7-1c-0.7-0.3-1.4-0.6-2.3-0.9c-0.9-0.3-1.7-0.7-2.5-1c-0.8-0.4-1.5-0.8-2.1-1.4 c-0.6-0.6-1.1-1.2-1.5-2c-0.4-0.8-0.6-1.7-0.6-2.8c0-2.3,0.8-4,2.3-5.3c1.6-1.3,3.7-1.9,6.4-1.9c1.6,0,3,0.2,4.2,0.5 c1.2,0.3,2.2,0.7,2.9,1.1l-1.4,3.7c-0.8-0.5-1.7-0.8-2.7-1c-1-0.2-2-0.4-3-0.4c-1.2,0-2.2,0.3-2.9,0.8c-0.7,0.5-1,1.2-1,2.1 c0,0.6,0.1,1,0.3,1.4c0.2,0.4,0.6,0.8,1,1.1c0.4,0.3,0.9,0.6,1.5,0.9c0.6,0.3,1.2,0.5,1.9,0.8c1.2,0.4,2.2,0.9,3.2,1.3 c0.9,0.4,1.7,1,2.3,1.6c0.6,0.6,1.1,1.3,1.5,2.2c0.3,0.8,0.5,1.8,0.5,3c0,2.3-0.8,4-2.4,5.3c-1.6,1.2-3.9,1.9-7,1.9 c-1,0-2-0.1-2.8-0.2c-0.9-0.1-1.6-0.3-2.3-0.5c-0.7-0.2-1.2-0.4-1.7-0.6c-0.5-0.2-0.9-0.4-1.2-0.6l1.3-3.7 c0.6,0.4,1.5,0.7,2.6,1.1C446,395.2,447.4,395.4,448.9,395.4z"
|
||||
id="path5183" />
|
||||
<path
|
||||
class="st9"
|
||||
d="M489.6,386.2c0,2.1-0.3,4-1,5.7c-0.6,1.6-1.5,3-2.6,4.1c-1.1,1.1-2.4,1.9-3.8,2.5c-1.5,0.6-3,0.8-4.7,0.8 s-3.2-0.3-4.7-0.8c-1.5-0.6-2.8-1.4-3.9-2.5c-1.1-1.1-2-2.5-2.6-4.1c-0.6-1.6-1-3.5-1-5.7c0-2.1,0.3-4,1-5.7 c0.6-1.6,1.5-3,2.7-4.1c1.1-1.1,2.4-1.9,3.9-2.5c1.5-0.6,3-0.8,4.7-0.8c1.6,0,3.2,0.3,4.7,0.8c1.5,0.6,2.7,1.4,3.8,2.5 c1.1,1.1,2,2.5,2.6,4.1C489.3,382.2,489.6,384.1,489.6,386.2z M470.1,386.2c0,1.4,0.2,2.6,0.5,3.7c0.3,1.1,0.8,2.1,1.5,2.9 c0.6,0.8,1.4,1.4,2.3,1.8c0.9,0.4,1.9,0.7,3.1,0.7c1.1,0,2.2-0.2,3.1-0.7c0.9-0.4,1.7-1,2.3-1.8c0.6-0.8,1.1-1.7,1.5-2.9 c0.3-1.1,0.5-2.4,0.5-3.7c0-1.4-0.2-2.6-0.5-3.7c-0.3-1.1-0.8-2.1-1.5-2.9c-0.6-0.8-1.4-1.4-2.3-1.8c-0.9-0.4-1.9-0.6-3.1-0.6 c-1.2,0-2.2,0.2-3.1,0.6c-0.9,0.4-1.7,1-2.3,1.8c-0.6,0.8-1.1,1.7-1.5,2.9C470.2,383.6,470.1,384.9,470.1,386.2z"
|
||||
id="path5185" />
|
||||
<path
|
||||
class="st9"
|
||||
d="M514.4,394.8v3.9h-15.8v-25h4.5v21.1H514.4z"
|
||||
id="path5187" />
|
||||
<path
|
||||
class="st9"
|
||||
d="M530.9,399.3c-1.7,0-3.2-0.2-4.4-0.7c-1.2-0.5-2.2-1.2-3.1-2.1c-0.8-0.9-1.4-1.9-1.8-3.1 c-0.4-1.2-0.6-2.5-0.6-4v-15.6h4.6v15.2c0,1.1,0.1,2.1,0.4,2.9c0.3,0.8,0.6,1.5,1.1,2c0.5,0.5,1,0.9,1.7,1.1 c0.6,0.2,1.4,0.4,2.1,0.4c0.8,0,1.5-0.1,2.2-0.4c0.6-0.2,1.2-0.6,1.7-1.1c0.5-0.5,0.8-1.2,1.1-2c0.3-0.8,0.4-1.8,0.4-2.9v-15.2 h4.6v15.6c0,1.4-0.2,2.8-0.6,4c-0.4,1.2-1,2.3-1.8,3.1c-0.8,0.9-1.8,1.6-3.1,2.1C534.1,399.1,532.6,399.3,530.9,399.3z"
|
||||
id="path5189" />
|
||||
<path
|
||||
class="st9"
|
||||
d="M568,373.7v3.9h-7.7v21.1h-4.6v-21.1h-7.7v-3.9H568z"
|
||||
id="path5191" />
|
||||
<path
|
||||
class="st9"
|
||||
d="M575.5,373.7h4.5v25h-4.5V373.7z"
|
||||
id="path5193" />
|
||||
<path
|
||||
class="st9"
|
||||
d="M613.2,386.2c0,2.1-0.3,4-1,5.7c-0.6,1.6-1.5,3-2.6,4.1c-1.1,1.1-2.4,1.9-3.8,2.5c-1.5,0.6-3,0.8-4.7,0.8 c-1.7,0-3.2-0.3-4.7-0.8c-1.5-0.6-2.8-1.4-3.9-2.5c-1.1-1.1-2-2.5-2.6-4.1c-0.7-1.6-1-3.5-1-5.7c0-2.1,0.3-4,1-5.7 c0.6-1.6,1.5-3,2.7-4.1c1.1-1.1,2.4-1.9,3.9-2.5c1.5-0.6,3-0.8,4.7-0.8c1.6,0,3.2,0.3,4.7,0.8c1.5,0.6,2.7,1.4,3.8,2.5 c1.1,1.1,2,2.5,2.6,4.1C612.9,382.2,613.2,384.1,613.2,386.2z M593.7,386.2c0,1.4,0.2,2.6,0.5,3.7c0.3,1.1,0.8,2.1,1.5,2.9 c0.6,0.8,1.4,1.4,2.3,1.8c0.9,0.4,1.9,0.7,3.1,0.7c1.1,0,2.2-0.2,3.1-0.7c0.9-0.4,1.7-1,2.3-1.8c0.6-0.8,1.1-1.7,1.5-2.9 c0.3-1.1,0.5-2.4,0.5-3.7c0-1.4-0.2-2.6-0.5-3.7c-0.3-1.1-0.8-2.1-1.5-2.9c-0.6-0.8-1.4-1.4-2.3-1.8c-0.9-0.4-1.9-0.6-3.1-0.6 c-1.2,0-2.2,0.2-3.1,0.6c-0.9,0.4-1.7,1-2.3,1.8c-0.6,0.8-1.1,1.7-1.5,2.9C593.9,383.6,593.7,384.9,593.7,386.2z"
|
||||
id="path5195" />
|
||||
<path
|
||||
class="st9"
|
||||
d="M639,398.8c-0.8-1.4-1.7-2.8-2.7-4.4c-1-1.6-2.1-3.2-3.1-4.8c-1.1-1.6-2.2-3.2-3.3-4.7 c-1.1-1.5-2.2-2.9-3.2-4.1v17.9h-4.5v-25h3.7c1,1,2,2.2,3.1,3.6c1.1,1.4,2.2,2.8,3.3,4.3c1.1,1.5,2.2,3,3.2,4.5 c1,1.5,2,2.9,2.8,4.2v-16.6h4.5v25H639z"
|
||||
id="path5197" />
|
||||
<path
|
||||
class="st9"
|
||||
d="M658.9,395.4c1.6,0,2.8-0.3,3.6-0.8c0.7-0.6,1.1-1.3,1.1-2.3c0-0.6-0.1-1.1-0.4-1.6 c-0.3-0.4-0.6-0.8-1.1-1.2c-0.5-0.3-1-0.7-1.7-1c-0.7-0.3-1.4-0.6-2.3-0.9c-0.9-0.3-1.7-0.7-2.5-1c-0.8-0.4-1.5-0.8-2.1-1.4 c-0.6-0.6-1.1-1.2-1.5-2c-0.4-0.8-0.6-1.7-0.6-2.8c0-2.3,0.8-4,2.3-5.3c1.6-1.3,3.7-1.9,6.4-1.9c1.6,0,3,0.2,4.2,0.5 c1.2,0.3,2.2,0.7,2.9,1.1l-1.4,3.7c-0.8-0.5-1.7-0.8-2.7-1c-1-0.2-2-0.4-3-0.4c-1.2,0-2.2,0.3-2.9,0.8c-0.7,0.5-1,1.2-1,2.1 c0,0.6,0.1,1,0.3,1.4c0.2,0.4,0.6,0.8,1,1.1c0.4,0.3,0.9,0.6,1.5,0.9c0.6,0.3,1.2,0.5,1.9,0.8c1.2,0.4,2.2,0.9,3.2,1.3 c0.9,0.4,1.7,1,2.3,1.6c0.6,0.6,1.1,1.3,1.5,2.2c0.3,0.8,0.5,1.8,0.5,3c0,2.3-0.8,4-2.4,5.3c-1.6,1.2-3.9,1.9-7,1.9 c-1,0-2-0.1-2.8-0.2c-0.9-0.1-1.6-0.3-2.3-0.5c-0.7-0.2-1.2-0.4-1.7-0.6c-0.5-0.2-0.9-0.4-1.2-0.6l1.3-3.7 c0.6,0.4,1.5,0.7,2.6,1.1C656,395.2,657.4,395.4,658.9,395.4z"
|
||||
id="path5199" />
|
||||
</g>
|
||||
</g>
|
||||
<g
|
||||
id="g5233">
|
||||
<circle
|
||||
class="st9"
|
||||
cx="198.7"
|
||||
cy="366.2"
|
||||
r="4"
|
||||
id="circle5205" />
|
||||
<circle
|
||||
class="st9"
|
||||
cx="211.4"
|
||||
cy="354.8"
|
||||
r="2.6"
|
||||
id="circle5207" />
|
||||
<path
|
||||
class="st9"
|
||||
d="M192,356.3c-0.1,0-0.2,0-0.3,0c-1.7,0-3.1,1.3-3.2,3c-0.2,1.8,1.2,3.4,3,3.6c0.1,0,0.2,0,0.3,0 c1.7,0,3.1-1.3,3.2-3C195.1,358.1,193.8,356.5,192,356.3z"
|
||||
id="path5209" />
|
||||
<circle
|
||||
class="st9"
|
||||
cx="186.1"
|
||||
cy="354.3"
|
||||
r="2.6"
|
||||
id="circle5211" />
|
||||
<path
|
||||
class="st9"
|
||||
d="M206.2,356.8c-0.1,0-0.2,0-0.3,0c-1.7,0-3.1,1.3-3.2,3c-0.2,1.8,1.2,3.4,3,3.6c0.1,0,0.2,0,0.3,0 c1.7,0,3.1-1.3,3.2-3C209.3,358.6,208,357,206.2,356.8z"
|
||||
id="path5213" />
|
||||
<path
|
||||
class="st9"
|
||||
d="M196,359.9c0,0.3-0.1,0.6-0.2,0.9c0.5,0,1.1-0.1,1.6-0.1c1.5,0,3,0.2,4.4,0.4c-0.1-0.5-0.2-1-0.1-1.5 c0.1-0.7,0.3-1.3,0.6-1.8c-1.6-0.3-3.2-0.5-4.8-0.5c-0.6,0-1.3,0-1.9,0.1C195.8,358.2,196,359.1,196,359.9z"
|
||||
id="path5215" />
|
||||
<g
|
||||
id="g5221">
|
||||
<path
|
||||
class="st9"
|
||||
d="M180,396.2c-2.9-3.7-4.6-8.4-4.6-13.4c0,0,0,0,0,0c-1.2,1.5-2.2,3-3,4.5c1,5.4,3.7,10.3,7.5,13.9 C179.5,399.6,179.6,397.9,180,396.2z"
|
||||
id="path5217" />
|
||||
<path
|
||||
class="st9"
|
||||
d="M172.9,384.4c0.7-1.1,1.6-2.3,2.6-3.4c0.7-8.3,5.9-15.3,13.2-18.4c-0.8-0.8-1.3-1.9-1.3-3.2 c-9.1,3.9-15.4,12.9-15.4,23.3c0,1,0.1,1.9,0.2,2.8C172.4,385.2,172.6,384.8,172.9,384.4z"
|
||||
id="path5219" />
|
||||
</g>
|
||||
<path
|
||||
class="st9"
|
||||
d="M191.8,368.9C191.8,368.9,191.8,368.9,191.8,368.9c-32.2,19.8-26.8,42.7,3.4,42.7c1.3,0,2.6,0,3.9-0.1 c-31.5-3.4-19.1-23.3-1.5-38.8c0,0,0,0,0,0C197.4,372.6,191.9,368.9,191.8,368.9z"
|
||||
id="path5223" />
|
||||
<path
|
||||
class="st9"
|
||||
d="M204.9,368.9c-0.3,0.4-5.7,3.7-5.7,3.7c0,0,0,0,0,0c17.6,15.6,30,35.5-1.5,38.8c1.3,0.1,2.6,0.1,3.9,0.1 C231.7,411.6,237.1,388.7,204.9,368.9z"
|
||||
id="path5225" />
|
||||
<g
|
||||
id="g5231">
|
||||
<path
|
||||
class="st9"
|
||||
d="M218.9,378.5c1.4,1.5,2.7,2.9,3.8,4.4c0-0.1,0-0.1,0-0.2c0-9.4-5.1-17.5-12.6-21.9 c-0.2,1.2-0.9,2.2-1.9,2.9C213.7,366.8,217.7,372.2,218.9,378.5z"
|
||||
id="path5227" />
|
||||
<path
|
||||
class="st9"
|
||||
d="M215.9,400.1c0.2-1.4,0-3.1-0.5-4.8c-4,5.7-10.6,9.4-18,9.4c-6.6,0-12.5-2.9-16.5-7.5 c-0.2,1.6-0.2,3,0.2,4.3c0.1,0.4,0.3,0.7,0.4,1c4.3,3.5,9.9,5.6,15.9,5.6C204.7,408.1,211.3,405,215.9,400.1z"
|
||||
id="path5229" />
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 32 KiB |
BIN
admin/public/project_nomad_logo.png
Normal file
BIN
admin/public/project_nomad_logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 952 KiB |
20
admin/resources/views/inertia_layout.edge
Normal file
20
admin/resources/views/inertia_layout.edge
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
|
||||
<title inertia>Project N.O.M.A.D</title>
|
||||
|
||||
@stack('dumper')
|
||||
@viteReactRefresh()
|
||||
@inertiaHead()
|
||||
@vite(['inertia/app/app.tsx', `inertia/pages/${page.component}.tsx`])
|
||||
</head>
|
||||
|
||||
<body class="min-h-screen w-screen font-sans">
|
||||
@inertia()
|
||||
</body>
|
||||
|
||||
</html>
|
||||
47
admin/start/env.ts
Normal file
47
admin/start/env.ts
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Environment variables service
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The `Env.create` method creates an instance of the Env service. The
|
||||
| service validates the environment variables and also cast values
|
||||
| to JavaScript data types.
|
||||
|
|
||||
*/
|
||||
|
||||
import { Env } from '@adonisjs/core/env'
|
||||
|
||||
export default await Env.create(new URL('../', import.meta.url), {
|
||||
NODE_ENV: Env.schema.enum(['development', 'production', 'test'] as const),
|
||||
PORT: Env.schema.number(),
|
||||
APP_KEY: Env.schema.string(),
|
||||
HOST: Env.schema.string({ format: 'host' }),
|
||||
LOG_LEVEL: Env.schema.string(),
|
||||
|
||||
/*
|
||||
|----------------------------------------------------------
|
||||
| Variables for configuring session package
|
||||
|----------------------------------------------------------
|
||||
*/
|
||||
//SESSION_DRIVER: Env.schema.enum(['cookie', 'memory'] as const),
|
||||
|
||||
/*
|
||||
|----------------------------------------------------------
|
||||
| Variables for configuring the drive package
|
||||
|----------------------------------------------------------
|
||||
*/
|
||||
DRIVE_DISK: Env.schema.enum(['fs'] as const),
|
||||
|
||||
|
||||
/*
|
||||
|----------------------------------------------------------
|
||||
| Variables for configuring the database package
|
||||
|----------------------------------------------------------
|
||||
*/
|
||||
DB_HOST: Env.schema.string({ format: 'host' }),
|
||||
DB_PORT: Env.schema.number(),
|
||||
DB_USER: Env.schema.string(),
|
||||
DB_PASSWORD: Env.schema.string.optional(),
|
||||
DB_DATABASE: Env.schema.string(),
|
||||
DB_SSL: Env.schema.boolean.optional(),
|
||||
})
|
||||
47
admin/start/kernel.ts
Normal file
47
admin/start/kernel.ts
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| HTTP kernel file
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The HTTP kernel file is used to register the middleware with the server
|
||||
| or the router.
|
||||
|
|
||||
*/
|
||||
|
||||
import router from '@adonisjs/core/services/router'
|
||||
import server from '@adonisjs/core/services/server'
|
||||
|
||||
/**
|
||||
* The error handler is used to convert an exception
|
||||
* to an HTTP response.
|
||||
*/
|
||||
server.errorHandler(() => import('#exceptions/handler'))
|
||||
|
||||
/**
|
||||
* The server middleware stack runs middleware on all the HTTP
|
||||
* requests, even if there is no route registered for
|
||||
* the request URL.
|
||||
*/
|
||||
server.use([
|
||||
() => import('#middleware/container_bindings_middleware'),
|
||||
() => import('@adonisjs/static/static_middleware'),
|
||||
() => import('@adonisjs/cors/cors_middleware'),
|
||||
() => import('@adonisjs/vite/vite_middleware'),
|
||||
() => import('@adonisjs/inertia/inertia_middleware')
|
||||
])
|
||||
|
||||
/**
|
||||
* The router middleware stack runs middleware on all the HTTP
|
||||
* requests with a registered route.
|
||||
*/
|
||||
router.use([
|
||||
() => import('@adonisjs/core/bodyparser_middleware'),
|
||||
// () => import('@adonisjs/session/session_middleware'),
|
||||
() => import('@adonisjs/shield/shield_middleware'),
|
||||
])
|
||||
|
||||
/**
|
||||
* Named middleware collection must be explicitly assigned to
|
||||
* the routes or the routes group.
|
||||
*/
|
||||
export const middleware = router.named({})
|
||||
24
admin/start/routes.ts
Normal file
24
admin/start/routes.ts
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Routes file
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The routes file is used for defining the HTTP routes.
|
||||
|
|
||||
*/
|
||||
import HomeController from '#controllers/home_controller'
|
||||
import SystemController from '#controllers/system_controller'
|
||||
import router from '@adonisjs/core/services/router'
|
||||
import transmit from '@adonisjs/transmit/services/main'
|
||||
|
||||
transmit.registerRoutes()
|
||||
|
||||
router.get('/home', [HomeController, 'index']);
|
||||
router.on('/about').renderInertia('about')
|
||||
router.on('/settings').renderInertia('settings')
|
||||
|
||||
|
||||
router.group(() => {
|
||||
router.get('/services', [SystemController, 'getServices'])
|
||||
router.post('/install-service', [SystemController, 'installService'])
|
||||
}).prefix('/api/system')
|
||||
13
admin/tailwind.config.ts
Normal file
13
admin/tailwind.config.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
/** @type {import('tailwindcss').Config} */
|
||||
|
||||
export default {
|
||||
content: ["./src/**/*.{js,jsx,ts,tsx}", "./index.html"],
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
desert: "#EADAB9",
|
||||
"desert-green-light": "#BABAAA",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
37
admin/tests/bootstrap.ts
Normal file
37
admin/tests/bootstrap.ts
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
import { assert } from '@japa/assert'
|
||||
import app from '@adonisjs/core/services/app'
|
||||
import type { Config } from '@japa/runner/types'
|
||||
import { pluginAdonisJS } from '@japa/plugin-adonisjs'
|
||||
import testUtils from '@adonisjs/core/services/test_utils'
|
||||
|
||||
/**
|
||||
* This file is imported by the "bin/test.ts" entrypoint file
|
||||
*/
|
||||
|
||||
/**
|
||||
* Configure Japa plugins in the plugins array.
|
||||
* Learn more - https://japa.dev/docs/runner-config#plugins-optional
|
||||
*/
|
||||
export const plugins: Config['plugins'] = [assert(), pluginAdonisJS(app)]
|
||||
|
||||
/**
|
||||
* Configure lifecycle function to run before and after all the
|
||||
* tests.
|
||||
*
|
||||
* The setup functions are executed before all the tests
|
||||
* The teardown functions are executed after all the tests
|
||||
*/
|
||||
export const runnerHooks: Required<Pick<Config, 'setup' | 'teardown'>> = {
|
||||
setup: [],
|
||||
teardown: [],
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure suites by tapping into the test suite instance.
|
||||
* Learn more - https://japa.dev/docs/test-suites#lifecycle-hooks
|
||||
*/
|
||||
export const configureSuite: Config['configureSuite'] = (suite) => {
|
||||
if (['browser', 'functional', 'e2e'].includes(suite.name)) {
|
||||
return suite.setup(() => testUtils.httpServer().start())
|
||||
}
|
||||
}
|
||||
8
admin/tsconfig.json
Normal file
8
admin/tsconfig.json
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"extends": "@adonisjs/tsconfig/tsconfig.app.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "./",
|
||||
"outDir": "./build"
|
||||
},
|
||||
"exclude": ["./inertia/**/*", "node_modules", "build"]
|
||||
}
|
||||
10
admin/types/docker.ts
Normal file
10
admin/types/docker.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
|
||||
export type DockerComposeServiceConfig = {
|
||||
image: string;
|
||||
container_name: string;
|
||||
restart: string;
|
||||
ports: string[];
|
||||
environment?: Record<string, string>;
|
||||
volumes?: string[];
|
||||
networks?: string[];
|
||||
}
|
||||
2
admin/types/util.ts
Normal file
2
admin/types/util.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
|
||||
|
||||
73
admin/views/inertia_layout.edge
Normal file
73
admin/views/inertia_layout.edge
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
|
||||
<title inertia>Project N.O.M.A.D</title>
|
||||
|
||||
<link rel="preconnect" href="https://fonts.bunny.net">
|
||||
<link href="https://fonts.bunny.net/css?family=instrument-sans:400,400i,500,500i,600,600i,700,700i" rel="stylesheet" />
|
||||
|
||||
<style>
|
||||
:root {
|
||||
--sand-1: #fdfdfc;
|
||||
--sand-2: #f9f9f8;
|
||||
--sand-3: #f1f0ef;
|
||||
--sand-4: #e9e8e6;
|
||||
--sand-5: #e2e1de;
|
||||
--sand-6: #dad9d6;
|
||||
--sand-7: #cfceca;
|
||||
--sand-8: #bcbbb5;
|
||||
--sand-9: #8d8d86;
|
||||
--sand-10: #82827c;
|
||||
--sand-11: #63635e;
|
||||
--sand-12: #21201c;
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- <script src="https://cdn.tailwindcss.com"></script> -->
|
||||
|
||||
<!-- <script>
|
||||
tailwind.config = {
|
||||
theme: {
|
||||
extend: {
|
||||
fontFamily: {
|
||||
sans: ['Instrument Sans', 'sans-serif'],
|
||||
},
|
||||
colors: {
|
||||
primary: {
|
||||
DEFAULT: '#5A45FF',
|
||||
},
|
||||
sand: {
|
||||
1: 'var(--sand-1)',
|
||||
2: 'var(--sand-2)',
|
||||
3: 'var(--sand-3)',
|
||||
4: 'var(--sand-4)',
|
||||
5: 'var(--sand-5)',
|
||||
6: 'var(--sand-6)',
|
||||
7: 'var(--sand-7)',
|
||||
8: 'var(--sand-8)',
|
||||
9: 'v and import it herear(--sand-9)',
|
||||
10: 'var(--sand-10)',
|
||||
11: 'var(--sand-11)',
|
||||
12: 'var(--sand-12)',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
</script> -->
|
||||
|
||||
@stack('dumper')
|
||||
@viteReactRefresh()
|
||||
@inertiaHead()
|
||||
@vite(['inertia/app/app.tsx', `inertia/pages/${page.component}.tsx`])
|
||||
</head>
|
||||
|
||||
<body class="min-h-screen w-screen font-sans">
|
||||
@inertia()
|
||||
</body>
|
||||
|
||||
</html>
|
||||
21
admin/vite.config.ts
Normal file
21
admin/vite.config.ts
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import { defineConfig } from 'vite'
|
||||
import { getDirname } from '@adonisjs/core/helpers'
|
||||
import inertia from '@adonisjs/inertia/client'
|
||||
import react from '@vitejs/plugin-react'
|
||||
import adonisjs from '@adonisjs/vite/client'
|
||||
import tailwindcss from '@tailwindcss/vite'
|
||||
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [inertia({ ssr: { enabled: false } }), react(), tailwindcss(), adonisjs({ entrypoints: ['inertia/app/app.tsx'], reload: ['resources/views/**/*.edge'] })],
|
||||
|
||||
/**
|
||||
* Define aliases for importing modules from
|
||||
* your frontend code
|
||||
*/
|
||||
resolve: {
|
||||
alias: {
|
||||
'~/': `${getDirname(import.meta.url)}/inertia/`,
|
||||
},
|
||||
},
|
||||
})
|
||||
22
install/entrypoint.sh
Normal file
22
install/entrypoint.sh
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
#!/bin/sh
|
||||
|
||||
set -e
|
||||
|
||||
echo "Starting entrypoint script..."
|
||||
echo "Running wait-for-it.sh to ensure MySQL is ready..."
|
||||
|
||||
# Use wait-for-it.sh to wait for MySQL to be available
|
||||
# wait-for-it.sh <host>:<port> [-t timeout] [-- command args]
|
||||
/usr/local/bin/wait-for-it.sh ${DB_HOST}:${DB_PORT} -t 60 -- echo "MySQL is up and running!"
|
||||
|
||||
# Run AdonisJS migrations
|
||||
echo "Running AdonisJS migrations..."
|
||||
node ace migration:run --force
|
||||
|
||||
# Seed the database if needed
|
||||
echo "Seeding the database..."
|
||||
node ace db:seed
|
||||
|
||||
# Start the AdonisJS application
|
||||
echo "Starting AdonisJS application..."
|
||||
exec node bin/server.js
|
||||
304
install/install_nomad.sh
Normal file
304
install/install_nomad.sh
Normal file
|
|
@ -0,0 +1,304 @@
|
|||
#!/bin/bash
|
||||
|
||||
# Project N.O.M.A.D. Installation Script
|
||||
|
||||
###################################################################################################################################################################################################
|
||||
|
||||
# Script | Project N.O.M.A.D. Installation Script
|
||||
# Version | 1.0.0
|
||||
# Author | Crosstalk Solutions, LLC
|
||||
# Website | https://crosstalksolutions.com
|
||||
|
||||
###################################################################################################################################################################################################
|
||||
# #
|
||||
# Color Codes #
|
||||
# #
|
||||
###################################################################################################################################################################################################
|
||||
|
||||
RESET='\033[0m'
|
||||
YELLOW='\033[1;33m'
|
||||
WHITE_R='\033[39m' # Same as GRAY_R for terminals with white background.
|
||||
GRAY_R='\033[39m'
|
||||
RED='\033[1;31m' # Light Red.
|
||||
GREEN='\033[1;32m' # Light Green.
|
||||
|
||||
###################################################################################################################################################################################################
|
||||
# #
|
||||
# Constants & Variables #
|
||||
# #
|
||||
###################################################################################################################################################################################################
|
||||
|
||||
WHIPTAIL_TITLE="Project N.O.M.A.D Installation"
|
||||
MANAGEMENT_COMPOSE_FILE_URL="http://192.168.1.53:8000/management_compose.yaml"
|
||||
ENTRYPOINT_SCRIPT_URL="http://192.168.1.53:8000/entrypoint.sh"
|
||||
WAIT_FOR_IT_SCRIPT_URL="https://raw.githubusercontent.com/vishnubob/wait-for-it/master/wait-for-it.sh"
|
||||
|
||||
script_option_debug='true'
|
||||
install_actions=''
|
||||
nomad_dir="/opt/project-nomad"
|
||||
|
||||
|
||||
###################################################################################################################################################################################################
|
||||
# #
|
||||
# Functions #
|
||||
# #
|
||||
###################################################################################################################################################################################################
|
||||
|
||||
header() {
|
||||
if [[ "${script_option_debug}" != 'true' ]]; then clear; clear; fi
|
||||
echo -e "${GREEN}#########################################################################${RESET}\\n"
|
||||
}
|
||||
|
||||
header_red() {
|
||||
if [[ "${script_option_debug}" != 'true' ]]; then clear; clear; fi
|
||||
echo -e "${RED}#########################################################################${RESET}\\n"
|
||||
}
|
||||
|
||||
check_has_sudo() {
|
||||
if sudo -n true 2>/dev/null; then
|
||||
echo -e "${GREEN}#${RESET} User has sudo permissions.\\n"
|
||||
else
|
||||
echo "User does not have sudo permissions"
|
||||
header_red
|
||||
echo -e "${RED}#${RESET} This script requires sudo permissions to run. Please run the script with sudo.\\n"
|
||||
echo -e "${RED}#${RESET} For example: sudo bash $(basename "$0")"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
check_is_bash() {
|
||||
if [[ -z "$BASH_VERSION" ]]; then
|
||||
header_red
|
||||
echo -e "${RED}#${RESET} This script requires bash to run. Please run the script using bash.\\n"
|
||||
echo -e "${RED}#${RESET} For example: bash $(basename "$0")"
|
||||
exit 1
|
||||
fi
|
||||
echo -e "${GREEN}#${RESET} This script is running in bash.\\n"
|
||||
}
|
||||
|
||||
check_is_debian_based() {
|
||||
if [[ ! -f /etc/debian_version ]]; then
|
||||
header_red
|
||||
echo -e "${RED}#${RESET} This script is designed to run on Debian-based systems only.\\n"
|
||||
echo -e "${RED}#${RESET} Please run this script on a Debian-based system and try again."
|
||||
exit 1
|
||||
fi
|
||||
echo -e "${GREEN}#${RESET} This script is running on a Debian-based system.\\n"
|
||||
}
|
||||
|
||||
check_is_debug_mode(){
|
||||
# Check if the script is being run in debug mode
|
||||
if [[ "${script_option_debug}" == 'true' ]]; then
|
||||
echo -e "${YELLOW}#${RESET} Debug mode is enabled, the script will not clear the screen...\\n"
|
||||
else
|
||||
clear; clear
|
||||
fi
|
||||
}
|
||||
|
||||
ensure_docker_installed() {
|
||||
if ! command -v docker &> /dev/null; then
|
||||
echo -e "${YELLOW}#${RESET} Docker not found. Installing Docker...\\n"
|
||||
|
||||
# Update package database
|
||||
sudo apt-get update
|
||||
|
||||
# Install prerequisites
|
||||
sudo apt-get install -y ca-certificates curl
|
||||
|
||||
# Create directory for keyrings
|
||||
# sudo install -m 0755 -d /etc/apt/keyrings
|
||||
|
||||
# # Download Docker's official GPG key
|
||||
# sudo curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc
|
||||
# sudo chmod a+r /etc/apt/keyrings/docker.asc
|
||||
|
||||
# # Add the repository to Apt sources
|
||||
# echo \
|
||||
# "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/debian \
|
||||
# $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
|
||||
# sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
|
||||
|
||||
# # Update the package database with the Docker packages from the newly added repo
|
||||
# sudo apt-get update
|
||||
|
||||
# # Install Docker packages
|
||||
# sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
|
||||
|
||||
# Download the Docker convenience script
|
||||
curl -fsSL https://get.docker.com -o get-docker.sh
|
||||
|
||||
# Run the Docker installation script
|
||||
sudo sh get-docker.sh
|
||||
|
||||
# Check if Docker was installed successfully
|
||||
if ! command -v docker &> /dev/null; then
|
||||
echo -e "${RED}#${RESET} Docker installation failed. Please check the logs and try again."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo -e "${GREEN}#${RESET} Docker installation completed.\\n"
|
||||
else
|
||||
echo -e "${GREEN}#${RESET} Docker is already installed.\\n"
|
||||
|
||||
# Check if Docker service is running
|
||||
if ! systemctl is-active --quiet docker; then
|
||||
echo -e "${YELLOW}#${RESET} Docker is installed but not running. Attempting to start Docker...\\n"
|
||||
sudo systemctl start docker
|
||||
if ! systemctl is-active --quiet docker; then
|
||||
echo -e "${RED}#${RESET} Failed to start Docker. Please check the Docker service status and try again."
|
||||
exit 1
|
||||
else
|
||||
echo -e "${GREEN}#${RESET} Docker service started successfully.\\n"
|
||||
fi
|
||||
else
|
||||
echo -e "${GREEN}#${RESET} Docker service is already running.\\n"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
ensure_whiptail_installed() {
|
||||
if ! command -v whiptail &> /dev/null; then
|
||||
header_red
|
||||
echo -e "${GRAY_R}#${RESET} whiptail is not installed, attempting to install it...\\n"
|
||||
if command -v apt &> /dev/null; then
|
||||
apt update && apt install -y whiptail
|
||||
else
|
||||
echo -e "${RED}#${RESET} Unsupported package manager. Please install whiptail manually."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
get_install_confirmation(){
|
||||
if whiptail --title "$WHIPTAIL_TITLE" --yesno "This script will install/update Project N.O.M.A.D. and its dependencies on your machine.\\n\\n Are you sure you want to continue?\\n\\nInfo:\\nVersion 1.0.0\\nAuthor: Crosstalk Solutions, LLC\\nWebsite: https://crosstalksolutions.com" 15 70; then
|
||||
echo -e "${GREEN}#${RESET} User chose to continue with the installation."
|
||||
else
|
||||
echo -e "${RED}#${RESET} User chose not to continue with the installation."
|
||||
exit 0
|
||||
fi
|
||||
}
|
||||
|
||||
get_install_directory() {
|
||||
# Prompt user for installation directory
|
||||
nomad_dir=$(whiptail --title "$WHIPTAIL_TITLE" --inputbox "Enter the installation directory for Project N.O.M.A.D. (default: /opt/project-nomad):" 10 60 "$nomad_dir" 3>&1 1>&2 2>&3)
|
||||
|
||||
if [[ $? -ne 0 ]]; then
|
||||
echo -e "${RED}#${RESET} Installation cancelled by user."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Validate the directory
|
||||
if [[ ! -d "$nomad_dir" ]]; then
|
||||
echo -e "${YELLOW}#${RESET} Directory $nomad_dir does not exist. It will be attempted to be created during the installation process.\\n"
|
||||
fi
|
||||
}
|
||||
|
||||
create_nomad_directory(){
|
||||
if [[ ! -d "$nomad_dir" ]]; then
|
||||
echo -e "${YELLOW}#${RESET} Creating directory for Project N.O.M.A.D at $nomad_dir...\\n"
|
||||
sudo mkdir -p "$nomad_dir"
|
||||
sudo chown "$(whoami):$(whoami)" "$nomad_dir"
|
||||
echo -e "${GREEN}#${RESET} Directory created successfully.\\n"
|
||||
else
|
||||
echo -e "${GREEN}#${RESET} Directory $nomad_dir already exists.\\n"
|
||||
fi
|
||||
}
|
||||
|
||||
download_management_compose_file() {
|
||||
local compose_file_path="${nomad_dir}/docker-compose-management.yml"
|
||||
|
||||
echo -e "${YELLOW}#${RESET} Downloading docker-compose file for management...\\n"
|
||||
if ! curl -fsSL "$MANAGEMENT_COMPOSE_FILE_URL" -o "$compose_file_path"; then
|
||||
echo -e "${RED}#${RESET} Failed to download the docker compose file. Please check the URL and try again."
|
||||
exit 1
|
||||
fi
|
||||
echo -e "${GREEN}#${RESET} Docker compose file downloaded successfully to $compose_file_path.\\n"
|
||||
}
|
||||
|
||||
download_wait_for_it_script() {
|
||||
local wait_for_it_script_path="${nomad_dir}/wait-for-it.sh"
|
||||
|
||||
echo -e "${YELLOW}#${RESET} Downloading wait-for-it script...\\n"
|
||||
if ! curl -fsSL "$WAIT_FOR_IT_SCRIPT_URL" -o "$wait_for_it_script_path"; then
|
||||
echo -e "${RED}#${RESET} Failed to download the wait-for-it script. Please check the URL and try again."
|
||||
exit 1
|
||||
fi
|
||||
chmod +x "$wait_for_it_script_path"
|
||||
echo -e "${GREEN}#${RESET} wait-for-it script downloaded successfully to $wait_for_it_script_path.\\n"
|
||||
}
|
||||
|
||||
download_entrypoint_script() {
|
||||
local entrypoint_script_path="${nomad_dir}/entrypoint.sh"
|
||||
|
||||
echo -e "${YELLOW}#${RESET} Downloading entrypoint script...\\n"
|
||||
if ! curl -fsSL "$ENTRYPOINT_SCRIPT_URL" -o "$entrypoint_script_path"; then
|
||||
echo -e "${RED}#${RESET} Failed to download the entrypoint script. Please check the URL and try again."
|
||||
exit 1
|
||||
fi
|
||||
chmod +x "$entrypoint_script_path"
|
||||
echo -e "${GREEN}#${RESET} entrypoint script downloaded successfully to $entrypoint_script_path.\\n"
|
||||
}
|
||||
|
||||
start_management_containers() {
|
||||
echo -e "${YELLOW}#${RESET} Starting management containers using docker compose...\\n"
|
||||
if ! sudo docker compose -f "${nomad_dir}/docker-compose-management.yml" up -d; then
|
||||
echo -e "${RED}#${RESET} Failed to start management containers. Please check the logs and try again."
|
||||
exit 1
|
||||
fi
|
||||
echo -e "${GREEN}#${RESET} Management containers started successfully.\\n"
|
||||
}
|
||||
|
||||
###################################################################################################################################################################################################
|
||||
# #
|
||||
# Main Script #
|
||||
# #
|
||||
###################################################################################################################################################################################################
|
||||
|
||||
# Pre-flight checks
|
||||
check_is_debian_based
|
||||
check_is_bash
|
||||
check_has_sudo
|
||||
check_is_debug_mode
|
||||
ensure_whiptail_installed
|
||||
|
||||
# Main install
|
||||
get_install_confirmation
|
||||
ensure_docker_installed
|
||||
#get_install_directory
|
||||
create_nomad_directory
|
||||
download_wait_for_it_script
|
||||
download_entrypoint_script
|
||||
download_management_compose_file
|
||||
start_management_containers
|
||||
|
||||
# free_space_check() {
|
||||
# if [[ "$(df -B1 / | awk 'NR==2{print $4}')" -le '5368709120' ]]; then
|
||||
# header_red
|
||||
# echo -e "${YELLOW}#${RESET} You only have $(df -B1 / | awk 'NR==2{print $4}' | awk '{ split( "B KB MB GB TB PB EB ZB YB" , v ); s=1; while( $1>1024 && s<9 ){ $1/=1024; s++ } printf "%.1f %s", $1, v[s] }') of disk space available on \"/\"... \\n"
|
||||
# while true; do
|
||||
# read -rp $'\033[39m#\033[0m Do you want to proceed with running the script? (y/N) ' yes_no
|
||||
# case "$yes_no" in
|
||||
# [Nn]*|"")
|
||||
# free_space_check_response="Cancel script"
|
||||
# free_space_check_date="$(date +%s)"
|
||||
# echo -e "${YELLOW}#${RESET} OK... Please free up disk space before running the script again..."
|
||||
# cancel_script
|
||||
# break;;
|
||||
# [Yy]*)
|
||||
# free_space_check_response="Proceed at own risk"
|
||||
# free_space_check_date="$(date +%s)"
|
||||
# echo -e "${YELLOW}#${RESET} OK... Proceeding with the script.. please note that failures may occur due to not enough disk space... \\n"; sleep 10
|
||||
# break;;
|
||||
# *) echo -e "\\n${RED}#${RESET} Invalid input, please answer Yes or No (y/n)...\\n"; sleep 3;;
|
||||
# esac
|
||||
# done
|
||||
# if [[ -n "$(command -v jq)" ]]; then
|
||||
# if [[ "$(dpkg-query --showformat='${version}' --show jq 2> /dev/null | sed -e 's/.*://' -e 's/-.*//g' -e 's/[^0-9.]//g' -e 's/\.//g' | sort -V | tail -n1)" -ge "16" && -e "${eus_dir}/db/db.json" ]]; then
|
||||
# jq '.scripts."'"${script_name}"'" += {"warnings": {"low-free-disk-space": {"response": "'"${free_space_check_response}"'", "detected-date": "'"${free_space_check_date}"'"}}}' "${eus_dir}/db/db.json" > "${eus_dir}/db/db.json.tmp" 2>> "${eus_dir}/logs/eus-database-management.log"
|
||||
# else
|
||||
# jq '.scripts."'"${script_name}"'" = (.scripts."'"${script_name}"'" | . + {"warnings": {"low-free-disk-space": {"response": "'"${free_space_check_response}"'", "detected-date": "'"${free_space_check_date}"'"}}})' "${eus_dir}/db/db.json" > "${eus_dir}/db/db.json.tmp" 2>> "${eus_dir}/logs/eus-database-management.log"
|
||||
# fi
|
||||
# eus_database_move
|
||||
# fi
|
||||
# fi
|
||||
# }
|
||||
55
install/management_compose.yaml
Normal file
55
install/management_compose.yaml
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
services:
|
||||
admin:
|
||||
image: jturnercosmistack/projectnomad:admin-latest
|
||||
pull_policy: always
|
||||
container_name: nomad_admin
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "8080:8080"
|
||||
volumes:
|
||||
- /opt/project-nomad/data:/data
|
||||
- /opt/project-nomad/storage:/app/storage
|
||||
- /var/run/docker.sock:/var/run/docker.sock # Allows the admin service to communicate with the Host's Docker daemon
|
||||
- ./entrypoint.sh:/usr/local/bin/entrypoint.sh
|
||||
- ./wait-for-it.sh:/usr/local/bin/wait-for-it.sh
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
- PORT=8080
|
||||
- LOG_LEVEL=debug
|
||||
- DRIVE_DISK=fs
|
||||
- APP_KEY=secretlongpasswordsecret
|
||||
- HOST=0.0.0.0
|
||||
- DB_HOST=mysql
|
||||
- DB_PORT=3306
|
||||
- DB_DATABASE=nomad
|
||||
- DB_USER=nomad_user
|
||||
- DB_PASSWORD=nomad_password
|
||||
- DB_NAME=nomad
|
||||
- DB_SSL=false
|
||||
depends_on:
|
||||
mysql:
|
||||
condition: service_healthy
|
||||
entrypoint: ["/usr/local/bin/entrypoint.sh"]
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:8080/api/v1/health"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
mysql:
|
||||
image: mysql:8.0
|
||||
container_name: nomad_mysql
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "3306:3306"
|
||||
environment:
|
||||
- MYSQL_ROOT_PASSWORD=secretpassword
|
||||
- MYSQL_DATABASE=nomad
|
||||
- MYSQL_USER=nomad_user
|
||||
- MYSQL_PASSWORD=nomad_password
|
||||
volumes:
|
||||
- /opt/project-nomad/mysql:/var/lib/mysql
|
||||
healthcheck:
|
||||
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
11
package.json
Normal file
11
package.json
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"name": "project-nomad",
|
||||
"version": "1.0.0",
|
||||
"description": "\"",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"author": "Crosstalk Solutions, LLC",
|
||||
"license": "ISC"
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user