Initial commit
This commit is contained in:
commit
85c4bb9a26
|
|
@ -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
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
TZ=UTC
|
||||||
|
PORT=3333
|
||||||
|
HOST=localhost
|
||||||
|
LOG_LEVEL=info
|
||||||
|
APP_KEY=55xt0BdfVHygJlHTvqJB3iCD8Z7PUPtb
|
||||||
|
NODE_ENV=development
|
||||||
|
DB_HOST=127.0.0.1
|
||||||
|
DB_PORT=3306
|
||||||
|
DB_USER=root
|
||||||
|
DB_PASSWORD=
|
||||||
|
DB_DATABASE=
|
||||||
|
DOMAIN_NAME=http://localhost
|
||||||
|
SOCKET_PORT=8989
|
||||||
|
FRONTEND_URL=http://localhost:5173
|
||||||
|
BACKEND_URL=http://localhost:3333
|
||||||
|
GOOGLE_CLIENT_ID=532287737140-2e3kb67raaac56u2uohnqveg6gt7vga9.apps.googleusercontent.com
|
||||||
|
GOOGLE_CLIENT_SECRET=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
|
||||||
|
SMTP_HOST=smtp.gmail.com
|
||||||
|
SMTP_PORT=587
|
||||||
|
SMTP_USERNAME=
|
||||||
|
SMTP_PASSWORD=
|
||||||
|
PAYPAL_MODE=sandbox
|
||||||
|
PAYPAL_BASE_URL=https://api-m.sandbox.paypal.com
|
||||||
|
PAYPAL_CLIENT_ID=ASV19JCbp2rUaaFLtfJ7TQtLFvaIFKMrze6kiK4LJYjcCRPjFWFjcAzFueofJKKzQ0iJTGle4qPUGwex
|
||||||
|
PAYPAL_CLIENT_SECRET=EDprE7q7sdsY2Lk869AnRI_mIc5VPtDLMfK4bsVlGd6Qswe4T2_By9anIi9mEKe-bNHosW9J2N_urTaH
|
||||||
|
PAYPAL_CURRENCY=USD
|
||||||
|
|
||||||
|
SEND_ZULIP=1
|
||||||
|
ZULIP_REALM="https://zulip.ipsupply.com.au"
|
||||||
|
ZULIP_USERNAME="networktool-bot@zulip.ipsupply.com.au"
|
||||||
|
ZULIP_API_KEY="0jMAmOuhfLvBqKJikv5oAkyNM4RIEoAM"
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
# Dependencies and AdonisJS build
|
||||||
|
node_modules
|
||||||
|
build
|
||||||
|
tmp
|
||||||
|
|
||||||
|
# Secrets
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
.env.production.local
|
||||||
|
.env.development.local
|
||||||
|
|
||||||
|
# Frontend assets compiled code
|
||||||
|
public/assets
|
||||||
|
|
||||||
|
# Build tools specific
|
||||||
|
npm-debug.log
|
||||||
|
yarn-error.log
|
||||||
|
|
||||||
|
# Editors specific
|
||||||
|
.fleet
|
||||||
|
.idea
|
||||||
|
.vscode
|
||||||
|
|
||||||
|
# Platform specific
|
||||||
|
.DS_Store
|
||||||
|
|
@ -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
|
||||||
|
*/
|
||||||
|
import 'ts-node-maintained/register/esm'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Import ace console entrypoint
|
||||||
|
*/
|
||||||
|
await import('./bin/console.js')
|
||||||
|
|
@ -0,0 +1,87 @@
|
||||||
|
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/cors/cors_provider'),
|
||||||
|
() => import('@adonisjs/lucid/database_provider'),
|
||||||
|
() => import('@adonisjs/auth/auth_provider'),
|
||||||
|
() => import('#providers/socket_io_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,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
@ -0,0 +1,67 @@
|
||||||
|
import Line from '#models/line'
|
||||||
|
import type { HttpContext } from '@adonisjs/core/http'
|
||||||
|
import Log from '#models/log'
|
||||||
|
|
||||||
|
export default class LinesController {
|
||||||
|
/**
|
||||||
|
* Display a list of resource
|
||||||
|
*/
|
||||||
|
async get({}: HttpContext) {
|
||||||
|
const lines = await Line.all()
|
||||||
|
return { status: true, data: lines }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display form to create a new record
|
||||||
|
*/
|
||||||
|
async create({ auth, request, response }: HttpContext) {
|
||||||
|
let payload = request.only([...Array.from(Line.$columnsDefinitions.keys()), 'station_id'])
|
||||||
|
try {
|
||||||
|
const line = await Line.create(payload)
|
||||||
|
return response.created({ status: true, message: 'Line created successfully', data: line })
|
||||||
|
} catch (error) {
|
||||||
|
return response.badRequest({ error: error, message: 'Line create failed', status: false })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async update({ request, response, auth }: HttpContext) {
|
||||||
|
let payload = request.only(
|
||||||
|
Array.from(Line.$columnsDefinitions.keys()).filter(
|
||||||
|
(f) => f !== 'created_at' && f !== 'updated_at'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
try {
|
||||||
|
const line = await Line.find(request.body().id)
|
||||||
|
if (!line) {
|
||||||
|
return response.status(404).json({ message: 'Line not found' })
|
||||||
|
}
|
||||||
|
Object.assign(line, payload)
|
||||||
|
await line.save()
|
||||||
|
return response.ok({ status: true, message: 'Line update successfully', data: line })
|
||||||
|
} catch (error) {
|
||||||
|
return response.badRequest({ error: error, message: 'Line update failed', status: false })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete record
|
||||||
|
*/
|
||||||
|
async delete({ auth, request, response }: HttpContext) {
|
||||||
|
try {
|
||||||
|
const line = await Line.find(request.body().id)
|
||||||
|
if (!line) {
|
||||||
|
return response.status(404).json({ message: 'line not found' })
|
||||||
|
}
|
||||||
|
const logs = await Log.query().where('line_id', line.id)
|
||||||
|
for (const log of logs) {
|
||||||
|
await log.delete()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete the line
|
||||||
|
await line.delete()
|
||||||
|
return response.ok({ status: true, message: 'Line delete successfully', data: line })
|
||||||
|
} catch (error) {
|
||||||
|
return response.badRequest({ error: error, message: 'Line delete failed', status: false })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,107 @@
|
||||||
|
import Log from '#models/log'
|
||||||
|
import type { HttpContext } from '@adonisjs/core/http'
|
||||||
|
import fs from 'node:fs'
|
||||||
|
import path, { dirname } from 'node:path'
|
||||||
|
import { fileURLToPath } from 'node:url'
|
||||||
|
|
||||||
|
const FILENAME = fileURLToPath(import.meta.url)
|
||||||
|
const DIRNAME = dirname(FILENAME)
|
||||||
|
|
||||||
|
export default class LogsController {
|
||||||
|
async list({ request, response, auth }: HttpContext) {
|
||||||
|
const search = request.input('search', '')
|
||||||
|
const perPage = request.input('per_page', 10)
|
||||||
|
const page = request.input('page', 1)
|
||||||
|
|
||||||
|
// Fetch logs that are associated with the user's stations with pagination
|
||||||
|
const query = Log.query()
|
||||||
|
.select('logs.*')
|
||||||
|
.preload('line', (lineQuery) => {
|
||||||
|
lineQuery.preload('station')
|
||||||
|
})
|
||||||
|
|
||||||
|
const logs = await query.paginate(page, perPage)
|
||||||
|
|
||||||
|
return response.ok({
|
||||||
|
status: true,
|
||||||
|
data: logs,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async viewLog({ request, response }: HttpContext) {
|
||||||
|
const logFilePath = request.input('path')
|
||||||
|
|
||||||
|
try {
|
||||||
|
const normalizedPath = path.normalize(logFilePath)
|
||||||
|
const fullPath = path.join(DIRNAME, '..', '..', normalizedPath)
|
||||||
|
|
||||||
|
if (!fs.existsSync(fullPath)) {
|
||||||
|
return response.notFound({
|
||||||
|
status: false,
|
||||||
|
message: 'Log file not found',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Đọc file với encoding buffer trước
|
||||||
|
const buffer = fs.readFileSync(fullPath)
|
||||||
|
|
||||||
|
// Thử các encoding khác nhau
|
||||||
|
let logContent
|
||||||
|
try {
|
||||||
|
// Thử với utf-8 trước
|
||||||
|
logContent = buffer.toString('utf-8')
|
||||||
|
if (logContent.includes('\u0000')) {
|
||||||
|
// Nếu có null bytes, thử với encoding khác
|
||||||
|
logContent = buffer.toString('ascii')
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Fallback to ascii if utf-8 fails
|
||||||
|
logContent = buffer.toString('ascii')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Loại bỏ các null bytes
|
||||||
|
logContent = logContent.replace(/\u0000/g, '')
|
||||||
|
|
||||||
|
return response.ok({
|
||||||
|
status: true,
|
||||||
|
data: logContent,
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
return response.internalServerError({
|
||||||
|
status: false,
|
||||||
|
message: 'Failed to read log file',
|
||||||
|
error: error.message,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async downloadLog({ request, response }: HttpContext) {
|
||||||
|
try {
|
||||||
|
const logPath = request.input('path')
|
||||||
|
const fullPath = path.join(DIRNAME, '..', '..', logPath)
|
||||||
|
|
||||||
|
if (!fs.existsSync(fullPath)) {
|
||||||
|
return response.notFound({
|
||||||
|
status: false,
|
||||||
|
message: 'Log file not found',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lấy tên file từ đường dẫn
|
||||||
|
const fileName = path.basename(logPath)
|
||||||
|
|
||||||
|
// Set headers cho download
|
||||||
|
response.header('Content-Type', 'application/octet-stream')
|
||||||
|
response.header('Content-Disposition', `attachment; filename="${fileName}"`)
|
||||||
|
|
||||||
|
// Stream file về client
|
||||||
|
return response.stream(fs.createReadStream(fullPath))
|
||||||
|
} catch (error) {
|
||||||
|
return response.internalServerError({
|
||||||
|
status: false,
|
||||||
|
message: 'Failed to download log file',
|
||||||
|
error: error.message,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
import type { HttpContext } from '@adonisjs/core/http'
|
||||||
|
import Model from '#models/model'
|
||||||
|
|
||||||
|
export default class ModelsController {
|
||||||
|
// GET /models
|
||||||
|
async index({}: HttpContext) {
|
||||||
|
return await Model.all()
|
||||||
|
}
|
||||||
|
|
||||||
|
// POST /models
|
||||||
|
async store({ request }: HttpContext) {
|
||||||
|
const data = request.only(['name'])
|
||||||
|
const model = await Model.create(data)
|
||||||
|
return model
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET /models/:id
|
||||||
|
async show({ params }: HttpContext) {
|
||||||
|
const model = await Model.findOrFail(params.id)
|
||||||
|
return model
|
||||||
|
}
|
||||||
|
|
||||||
|
// PUT /models/:id
|
||||||
|
async update({ params, request }: HttpContext) {
|
||||||
|
const model = await Model.findOrFail(params.id)
|
||||||
|
const data = request.only(['name'])
|
||||||
|
model.merge(data)
|
||||||
|
await model.save()
|
||||||
|
return model
|
||||||
|
}
|
||||||
|
|
||||||
|
// DELETE /models/:id
|
||||||
|
async destroy({ params }: HttpContext) {
|
||||||
|
const model = await Model.findOrFail(params.id)
|
||||||
|
await model.delete()
|
||||||
|
return { success: true }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,134 @@
|
||||||
|
import Scenario from '#models/scenario'
|
||||||
|
import type { HttpContext } from '@adonisjs/core/http'
|
||||||
|
import { searchRequest } from '../utils/hasPaginationRequest.js'
|
||||||
|
import db from '@adonisjs/lucid/services/db'
|
||||||
|
import UserScenarios from '#models/user_scenario'
|
||||||
|
|
||||||
|
export default class ScenariosController {
|
||||||
|
/**
|
||||||
|
* List all scenarios
|
||||||
|
*/
|
||||||
|
async get({ request, response, auth }: HttpContext) {
|
||||||
|
try {
|
||||||
|
const search = request.input('search', '')
|
||||||
|
const perPage = request.input('per_page', 10)
|
||||||
|
const page = request.input('page', 1)
|
||||||
|
|
||||||
|
const query = Scenario.query()
|
||||||
|
|
||||||
|
const scenarios = await query.orderBy('scenarios.created_at', 'asc').paginate(page, perPage)
|
||||||
|
return response.ok({
|
||||||
|
status: true,
|
||||||
|
data: scenarios,
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
return response.internalServerError({
|
||||||
|
status: false,
|
||||||
|
message: 'Failed to fetch scenarios',
|
||||||
|
error,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new scenario
|
||||||
|
*/
|
||||||
|
async create({ request, response, auth }: HttpContext) {
|
||||||
|
try {
|
||||||
|
const payload = await request.all()
|
||||||
|
|
||||||
|
const trx = await db.transaction()
|
||||||
|
try {
|
||||||
|
const scenario = await Scenario.create(
|
||||||
|
{
|
||||||
|
title: payload.title.trim(),
|
||||||
|
body: JSON.stringify(payload.body),
|
||||||
|
timeout: payload.timeout,
|
||||||
|
isReboot: payload.is_reboot,
|
||||||
|
},
|
||||||
|
{ client: trx }
|
||||||
|
)
|
||||||
|
|
||||||
|
await trx.commit()
|
||||||
|
|
||||||
|
return response.ok({
|
||||||
|
status: true,
|
||||||
|
message: 'Scenario created successfully',
|
||||||
|
data: scenario,
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
await trx.rollback()
|
||||||
|
|
||||||
|
return response.internalServerError({
|
||||||
|
status: false,
|
||||||
|
message: 'Failed to create scenario, please try again!',
|
||||||
|
error,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
return response.internalServerError({
|
||||||
|
status: false,
|
||||||
|
message: 'Failed to create scenario',
|
||||||
|
error,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a single scenario by ID
|
||||||
|
*/
|
||||||
|
async show({ params, response }: HttpContext) {
|
||||||
|
try {
|
||||||
|
const scenario = await Scenario.findOrFail(params.id)
|
||||||
|
return response.ok({ status: true, data: scenario })
|
||||||
|
} catch (error) {
|
||||||
|
return response.notFound({ status: false, message: 'Scenario not found' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update a scenario
|
||||||
|
*/
|
||||||
|
async update({ request, response, auth }: HttpContext) {
|
||||||
|
try {
|
||||||
|
const scenarioId = request.param('id')
|
||||||
|
const payload = await request.all()
|
||||||
|
const scenario = await Scenario.findOrFail(scenarioId)
|
||||||
|
payload.body = JSON.stringify(payload.body)
|
||||||
|
scenario.merge(payload)
|
||||||
|
await scenario.save()
|
||||||
|
|
||||||
|
return response.ok({ status: true, message: 'Scenario updated successfully', data: scenario })
|
||||||
|
} catch (error) {
|
||||||
|
return response.internalServerError({
|
||||||
|
status: false,
|
||||||
|
message: 'Failed to update scenario',
|
||||||
|
error,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a scenario
|
||||||
|
*/
|
||||||
|
async delete({ request, response }: HttpContext) {
|
||||||
|
try {
|
||||||
|
const scenarioId = request.param('id')
|
||||||
|
const scenario = await Scenario.findOrFail(scenarioId)
|
||||||
|
|
||||||
|
if (!scenario) {
|
||||||
|
return response.notFound({ message: 'Scenario not found' })
|
||||||
|
}
|
||||||
|
|
||||||
|
await scenario.delete()
|
||||||
|
|
||||||
|
return response.ok({ status: true, message: 'Scenario deleted successfully' })
|
||||||
|
} catch (error) {
|
||||||
|
return response.internalServerError({
|
||||||
|
status: false,
|
||||||
|
message: 'Failed to delete scenario',
|
||||||
|
error,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,76 @@
|
||||||
|
import type { HttpContext } from '@adonisjs/core/http'
|
||||||
|
import Station from '#models/station'
|
||||||
|
|
||||||
|
export default class StationsController {
|
||||||
|
public async index({}: HttpContext) {
|
||||||
|
return await Station.query().preload('lines')
|
||||||
|
}
|
||||||
|
|
||||||
|
public async store({ request, response }: HttpContext) {
|
||||||
|
let payload = request.only(Array.from(Station.$columnsDefinitions.keys()))
|
||||||
|
try {
|
||||||
|
const stationName = await Station.findBy('name', payload.name)
|
||||||
|
if (stationName) return response.status(400).json({ message: 'Station name exist!' })
|
||||||
|
|
||||||
|
const stationIP = await Station.findBy('ip', payload.ip)
|
||||||
|
if (stationIP) return response.status(400).json({ message: 'Ip of station is exist!' })
|
||||||
|
|
||||||
|
const station = await Station.create(payload)
|
||||||
|
return response.created({
|
||||||
|
status: true,
|
||||||
|
message: 'Station created successfully',
|
||||||
|
data: station,
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
return response.badRequest({ error: error, message: 'Station create failed', status: false })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async show({ params }: HttpContext) {
|
||||||
|
return await Station.findOrFail(params.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
public async update({ request, response }: HttpContext) {
|
||||||
|
let payload = request.only(
|
||||||
|
Array.from(Station.$columnsDefinitions.keys()).filter(
|
||||||
|
(f) => f !== 'created_at' && f !== 'updated_at'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
try {
|
||||||
|
const station = await Station.find(request.body().id)
|
||||||
|
|
||||||
|
// If the station does not exist, return a 404 response
|
||||||
|
if (!station) {
|
||||||
|
return response.status(404).json({ message: 'Station not found' })
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.assign(station, payload)
|
||||||
|
|
||||||
|
await station.save()
|
||||||
|
|
||||||
|
return response.ok({ status: true, message: 'Station update successfully', data: station })
|
||||||
|
} catch (error) {
|
||||||
|
return response.badRequest({ error: error, message: 'Station update failed', status: false })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async destroy({ request, response }: HttpContext) {
|
||||||
|
try {
|
||||||
|
const station = await Station.find(request.body().id)
|
||||||
|
|
||||||
|
// If the station does not exist, return a 404 response
|
||||||
|
if (!station) {
|
||||||
|
return response.status(404).json({ message: 'Station not found' })
|
||||||
|
}
|
||||||
|
|
||||||
|
// Optionally, delete associated lines first
|
||||||
|
await station.related('lines').query().delete()
|
||||||
|
|
||||||
|
// Delete the station
|
||||||
|
await station.delete()
|
||||||
|
return response.ok({ status: true, message: 'Station delete successfully', data: station })
|
||||||
|
} catch (error) {
|
||||||
|
return response.badRequest({ error: error, message: 'Station delete failed', status: false })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,100 @@
|
||||||
|
import User from '#models/user'
|
||||||
|
import type { HttpContext } from '@adonisjs/core/http'
|
||||||
|
import hash from '@adonisjs/core/services/hash'
|
||||||
|
|
||||||
|
export default class UsersController {
|
||||||
|
async index({ request, response }: HttpContext) {
|
||||||
|
const search = request.input('search', '')
|
||||||
|
const perPage = request.input('per_page', 10)
|
||||||
|
const page = request.input('page', 1)
|
||||||
|
|
||||||
|
const users = await User.query().orderBy('created_at', 'desc').paginate(page, perPage)
|
||||||
|
|
||||||
|
return response.ok({ status: true, data: users })
|
||||||
|
}
|
||||||
|
|
||||||
|
async get({ params, response }: HttpContext) {
|
||||||
|
const user = await User.query().where('id', params.id).firstOrFail() // Preload package data
|
||||||
|
return response.ok({ status: true, data: user })
|
||||||
|
}
|
||||||
|
|
||||||
|
async getByEmail({ request, response }: HttpContext) {
|
||||||
|
const data = request.only(['email'])
|
||||||
|
const user = await User.query().where('email', data.email).firstOrFail() // Preload package data
|
||||||
|
return response.ok({ status: true, data: user })
|
||||||
|
}
|
||||||
|
|
||||||
|
async store({ request, response }: HttpContext) {
|
||||||
|
try {
|
||||||
|
const data = request.only(['full_name', 'email', 'password'])
|
||||||
|
|
||||||
|
// Check if email already exists
|
||||||
|
const existingUser = await User.findBy('email', data.email)
|
||||||
|
if (existingUser) {
|
||||||
|
return response.conflict({
|
||||||
|
status: false,
|
||||||
|
message: 'Email already exists',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hash the password before saving
|
||||||
|
const hashedPassword = await hash.make(data.password)
|
||||||
|
|
||||||
|
const user = await User.create({
|
||||||
|
fullName: data.full_name,
|
||||||
|
email: data.email,
|
||||||
|
password: hashedPassword,
|
||||||
|
})
|
||||||
|
|
||||||
|
return response.created({
|
||||||
|
status: true,
|
||||||
|
message: 'User created successfully',
|
||||||
|
data: user,
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
return response.badRequest({ error: error.message || 'User create failed', status: false })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async update({ params, request, response }: HttpContext) {
|
||||||
|
try {
|
||||||
|
const user = await User.findOrFail(params.id)
|
||||||
|
const data = request.only(['full_name', 'email', 'password']) // Include password
|
||||||
|
|
||||||
|
// Check if email already exists for another user
|
||||||
|
if (data.email)
|
||||||
|
if (data.email !== user.email) {
|
||||||
|
const existingUser = await User.findBy('email', data.email)
|
||||||
|
if (existingUser) {
|
||||||
|
return response.conflict({
|
||||||
|
status: false,
|
||||||
|
message: 'Email already exists',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hash the password if it is provided
|
||||||
|
if (data.password) {
|
||||||
|
data.password = await hash.make(data.password)
|
||||||
|
}
|
||||||
|
|
||||||
|
user.merge(data)
|
||||||
|
await user.save()
|
||||||
|
|
||||||
|
return response.ok({ status: true, message: 'User updated successfully', data: user })
|
||||||
|
} catch (error) {
|
||||||
|
return response.badRequest({ error: error.message || 'User update failed', status: false })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async destroy({ params, response }: HttpContext) {
|
||||||
|
try {
|
||||||
|
const user = await User.findOrFail(params.id)
|
||||||
|
|
||||||
|
await user.delete()
|
||||||
|
return response.ok({ status: true, message: 'User deleted successfully', data: user })
|
||||||
|
} catch (error) {
|
||||||
|
return response.badRequest({ error: error.message || 'User delete failed', status: false })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
import app from '@adonisjs/core/services/app'
|
||||||
|
import { HttpContext, ExceptionHandler } from '@adonisjs/core/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
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
import type { HttpContext } from '@adonisjs/core/http'
|
||||||
|
import type { NextFn } from '@adonisjs/core/types/http'
|
||||||
|
import type { Authenticators } from '@adonisjs/auth/types'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auth middleware is used authenticate HTTP requests and deny
|
||||||
|
* access to unauthenticated users.
|
||||||
|
*/
|
||||||
|
export default class AuthMiddleware {
|
||||||
|
/**
|
||||||
|
* The URL to redirect to, when authentication fails
|
||||||
|
*/
|
||||||
|
redirectTo = '/login'
|
||||||
|
|
||||||
|
async handle(
|
||||||
|
ctx: HttpContext,
|
||||||
|
next: NextFn,
|
||||||
|
options: {
|
||||||
|
guards?: (keyof Authenticators)[]
|
||||||
|
} = {}
|
||||||
|
) {
|
||||||
|
await ctx.auth.authenticateUsing(options.guards, { loginRoute: this.redirectTo })
|
||||||
|
return next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
import { Logger } from '@adonisjs/core/logger'
|
||||||
|
import { HttpContext } from '@adonisjs/core/http'
|
||||||
|
import type { 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,42 @@
|
||||||
|
import { DateTime } from 'luxon'
|
||||||
|
import { BaseModel, belongsTo, column, hasMany } from '@adonisjs/lucid/orm'
|
||||||
|
import type { BelongsTo, HasMany } from '@adonisjs/lucid/types/relations'
|
||||||
|
import Station from './station.js'
|
||||||
|
import Log from './log.js'
|
||||||
|
|
||||||
|
export default class Line extends BaseModel {
|
||||||
|
@column({ isPrimary: true })
|
||||||
|
declare id: number
|
||||||
|
|
||||||
|
@column()
|
||||||
|
declare port: number
|
||||||
|
|
||||||
|
@column()
|
||||||
|
declare line_number: number
|
||||||
|
|
||||||
|
@column()
|
||||||
|
declare line_clear: number
|
||||||
|
|
||||||
|
@column()
|
||||||
|
declare stationId: number
|
||||||
|
|
||||||
|
@column()
|
||||||
|
declare apc_name: number
|
||||||
|
|
||||||
|
@column()
|
||||||
|
declare outlet: number
|
||||||
|
|
||||||
|
@belongsTo(() => Station)
|
||||||
|
declare station: BelongsTo<typeof Station>
|
||||||
|
|
||||||
|
@hasMany(() => Log, {
|
||||||
|
foreignKey: 'lineId',
|
||||||
|
})
|
||||||
|
declare logs: HasMany<typeof Log>
|
||||||
|
|
||||||
|
@column.dateTime({ autoCreate: true })
|
||||||
|
declare createdAt: DateTime
|
||||||
|
|
||||||
|
@column.dateTime({ autoCreate: true, autoUpdate: true })
|
||||||
|
declare updatedAt: DateTime
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
import { DateTime } from 'luxon'
|
||||||
|
import { BaseModel, belongsTo, column } from '@adonisjs/lucid/orm'
|
||||||
|
import Line from './line.js'
|
||||||
|
import type { BelongsTo } from '@adonisjs/lucid/types/relations'
|
||||||
|
|
||||||
|
export default class Log extends BaseModel {
|
||||||
|
@column({ isPrimary: true })
|
||||||
|
declare id: number
|
||||||
|
|
||||||
|
@column()
|
||||||
|
declare path: string
|
||||||
|
|
||||||
|
@column()
|
||||||
|
declare lineId: number
|
||||||
|
|
||||||
|
@belongsTo(() => Line)
|
||||||
|
declare line: BelongsTo<typeof Line>
|
||||||
|
|
||||||
|
@column({ columnName: 'PID' })
|
||||||
|
declare PID: string
|
||||||
|
|
||||||
|
@column({ columnName: 'SN' })
|
||||||
|
declare SN: string
|
||||||
|
|
||||||
|
@column()
|
||||||
|
declare description: string
|
||||||
|
|
||||||
|
@column.dateTime({ autoCreate: true })
|
||||||
|
declare createdAt: DateTime
|
||||||
|
|
||||||
|
@column.dateTime({ autoCreate: true, autoUpdate: true })
|
||||||
|
declare updatedAt: DateTime
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
import { DateTime } from 'luxon'
|
||||||
|
import { BaseModel, column } from '@adonisjs/lucid/orm'
|
||||||
|
|
||||||
|
export default class Model extends BaseModel {
|
||||||
|
@column({ isPrimary: true })
|
||||||
|
declare id: number
|
||||||
|
|
||||||
|
@column()
|
||||||
|
declare name: string
|
||||||
|
|
||||||
|
@column.dateTime({ autoCreate: true })
|
||||||
|
declare createdAt: DateTime
|
||||||
|
|
||||||
|
@column.dateTime({ autoCreate: true, autoUpdate: true })
|
||||||
|
declare updatedAt: DateTime
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
import { BaseModel, column } from '@adonisjs/lucid/orm'
|
||||||
|
import { DateTime } from 'luxon'
|
||||||
|
|
||||||
|
export default class Scenario extends BaseModel {
|
||||||
|
@column({ isPrimary: true })
|
||||||
|
declare id: number
|
||||||
|
|
||||||
|
@column()
|
||||||
|
declare title: string
|
||||||
|
|
||||||
|
@column()
|
||||||
|
declare body: string
|
||||||
|
|
||||||
|
@column()
|
||||||
|
declare timeout: number
|
||||||
|
|
||||||
|
@column()
|
||||||
|
declare isReboot: boolean
|
||||||
|
|
||||||
|
@column.dateTime({ autoCreate: true })
|
||||||
|
declare createdAt: DateTime
|
||||||
|
|
||||||
|
@column.dateTime({ autoCreate: true, autoUpdate: true })
|
||||||
|
declare updatedAt: DateTime
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,82 @@
|
||||||
|
import { DateTime } from 'luxon'
|
||||||
|
import { BaseModel, column, hasMany, SnakeCaseNamingStrategy } from '@adonisjs/lucid/orm'
|
||||||
|
import type { HasMany } from '@adonisjs/lucid/types/relations'
|
||||||
|
import Line from './line.js'
|
||||||
|
|
||||||
|
BaseModel.namingStrategy = new SnakeCaseNamingStrategy()
|
||||||
|
|
||||||
|
export default class Station extends BaseModel {
|
||||||
|
@column({ isPrimary: true })
|
||||||
|
declare id: number
|
||||||
|
|
||||||
|
@column()
|
||||||
|
declare name: string
|
||||||
|
|
||||||
|
@column()
|
||||||
|
declare ip: string
|
||||||
|
|
||||||
|
@column()
|
||||||
|
declare netmask?: string
|
||||||
|
|
||||||
|
@column()
|
||||||
|
declare network?: string
|
||||||
|
|
||||||
|
@column()
|
||||||
|
declare gateway?: string
|
||||||
|
|
||||||
|
@column()
|
||||||
|
declare tftp_ip?: string
|
||||||
|
|
||||||
|
@column()
|
||||||
|
declare apc_1_ip?: string
|
||||||
|
|
||||||
|
@column()
|
||||||
|
declare apc_2_ip?: string
|
||||||
|
|
||||||
|
@column()
|
||||||
|
declare port: number
|
||||||
|
|
||||||
|
@column()
|
||||||
|
declare apc_1_port?: number
|
||||||
|
|
||||||
|
@column()
|
||||||
|
declare apc_2_port?: number
|
||||||
|
|
||||||
|
@column()
|
||||||
|
declare apc_1_username?: string
|
||||||
|
|
||||||
|
@column()
|
||||||
|
declare apc_1_password?: string
|
||||||
|
|
||||||
|
@column()
|
||||||
|
declare apc_2_username?: string
|
||||||
|
|
||||||
|
@column()
|
||||||
|
declare apc_2_password?: string
|
||||||
|
|
||||||
|
@column()
|
||||||
|
declare master_control: boolean
|
||||||
|
|
||||||
|
@column()
|
||||||
|
declare switch_control_ip?: string
|
||||||
|
|
||||||
|
@column()
|
||||||
|
declare switch_control_port?: number
|
||||||
|
|
||||||
|
@column()
|
||||||
|
declare switch_control_username?: string
|
||||||
|
|
||||||
|
@column()
|
||||||
|
declare switch_control_password?: string
|
||||||
|
|
||||||
|
@hasMany(() => Line, {
|
||||||
|
foreignKey: 'stationId',
|
||||||
|
})
|
||||||
|
declare lines: HasMany<typeof Line>
|
||||||
|
|
||||||
|
@column.dateTime({ autoCreate: true })
|
||||||
|
declare createdAt: DateTime
|
||||||
|
|
||||||
|
@column.dateTime({ autoCreate: true, autoUpdate: true })
|
||||||
|
declare updatedAt: DateTime
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
import { DateTime } from 'luxon'
|
||||||
|
import hash from '@adonisjs/core/services/hash'
|
||||||
|
import { compose } from '@adonisjs/core/helpers'
|
||||||
|
import { BaseModel, column } from '@adonisjs/lucid/orm'
|
||||||
|
import { withAuthFinder } from '@adonisjs/auth/mixins/lucid'
|
||||||
|
|
||||||
|
const AuthFinder = withAuthFinder(() => hash.use('scrypt'), {
|
||||||
|
uids: ['email'],
|
||||||
|
passwordColumnName: 'password',
|
||||||
|
})
|
||||||
|
|
||||||
|
export default class User extends compose(BaseModel, AuthFinder) {
|
||||||
|
@column({ isPrimary: true })
|
||||||
|
declare id: number
|
||||||
|
|
||||||
|
@column()
|
||||||
|
declare fullName: string | null
|
||||||
|
|
||||||
|
@column()
|
||||||
|
declare email: string
|
||||||
|
|
||||||
|
@column({ serializeAs: null })
|
||||||
|
declare password: string
|
||||||
|
|
||||||
|
@column.dateTime({ autoCreate: true })
|
||||||
|
declare createdAt: DateTime
|
||||||
|
|
||||||
|
@column.dateTime({ autoCreate: true, autoUpdate: true })
|
||||||
|
declare updatedAt: DateTime | null
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,86 @@
|
||||||
|
import net from 'node:net'
|
||||||
|
|
||||||
|
interface LineConfig {
|
||||||
|
id: number
|
||||||
|
port: number
|
||||||
|
lineNumber: number
|
||||||
|
ip: string
|
||||||
|
stationId: number
|
||||||
|
apcName?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class LineConnection {
|
||||||
|
public client: net.Socket
|
||||||
|
public readonly config: LineConfig
|
||||||
|
public readonly socketIO: any
|
||||||
|
|
||||||
|
constructor(config: LineConfig, socketIO: any) {
|
||||||
|
this.config = config
|
||||||
|
this.socketIO = socketIO
|
||||||
|
this.client = new net.Socket()
|
||||||
|
}
|
||||||
|
|
||||||
|
connect() {
|
||||||
|
return new Promise<void>((resolve, reject) => {
|
||||||
|
const { ip, port, lineNumber, id, stationId } = this.config
|
||||||
|
|
||||||
|
this.client.connect(port, ip, () => {
|
||||||
|
console.log(`✅ Connected to line ${lineNumber} (${ip}:${port})`)
|
||||||
|
this.socketIO.emit('line_connected', {
|
||||||
|
stationId,
|
||||||
|
lineId: id,
|
||||||
|
lineNumber,
|
||||||
|
status: 'connected',
|
||||||
|
})
|
||||||
|
resolve()
|
||||||
|
})
|
||||||
|
|
||||||
|
this.client.on('data', (data) => {
|
||||||
|
const message = data.toString().trim()
|
||||||
|
console.log(`📨 [${this.config.apcName}] ${message}`)
|
||||||
|
this.socketIO.emit('line_output', {
|
||||||
|
stationId,
|
||||||
|
lineId: id,
|
||||||
|
data: message,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
this.client.on('error', (err) => {
|
||||||
|
console.error(`❌ Error line ${lineNumber}:`, err.message)
|
||||||
|
this.socketIO.emit('line_error', {
|
||||||
|
stationId,
|
||||||
|
lineId: id,
|
||||||
|
error: err.message,
|
||||||
|
})
|
||||||
|
reject(err)
|
||||||
|
})
|
||||||
|
|
||||||
|
this.client.on('close', () => {
|
||||||
|
console.log(`🔌 Line ${lineNumber} disconnected`)
|
||||||
|
this.socketIO.emit('line_disconnected', {
|
||||||
|
stationId,
|
||||||
|
lineId: id,
|
||||||
|
lineNumber,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
sendCommand(cmd: string) {
|
||||||
|
if (this.client.destroyed) {
|
||||||
|
console.log(`⚠️ Cannot send, line ${this.config.lineNumber} is closed`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
console.log(`➡️ [${this.config.apcName}] SEND:`, cmd)
|
||||||
|
this.client.write(`${cmd}\r\n`)
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnect() {
|
||||||
|
try {
|
||||||
|
this.client.destroy()
|
||||||
|
console.log(`🔻 Closed connection to line ${this.config.lineNumber}`)
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Error closing line:', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
import { Server, Socket } from 'socket.io'
|
||||||
|
|
||||||
|
export interface CustomSocket extends Socket {
|
||||||
|
connectionTime?: Date
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CustomServer extends Server {
|
||||||
|
userKeys?: string[]
|
||||||
|
}
|
||||||
|
|
@ -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)
|
||||||
|
})
|
||||||
|
|
@ -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((appBoot) => {
|
||||||
|
appBoot.booting(async () => {
|
||||||
|
await import('#start/env')
|
||||||
|
})
|
||||||
|
appBoot.listen('SIGTERM', () => appBoot.terminate())
|
||||||
|
appBoot.listenIf(appBoot.managedByPm2, 'SIGINT', () => appBoot.terminate())
|
||||||
|
})
|
||||||
|
.httpServer()
|
||||||
|
.start()
|
||||||
|
.catch((error) => {
|
||||||
|
process.exitCode = 1
|
||||||
|
prettyPrintError(error)
|
||||||
|
})
|
||||||
|
|
@ -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)
|
||||||
|
})
|
||||||
|
|
@ -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',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
import { defineConfig } from '@adonisjs/auth'
|
||||||
|
import { basicAuthGuard, basicAuthUserProvider } from '@adonisjs/auth/basic_auth'
|
||||||
|
import type { InferAuthenticators, InferAuthEvents, Authenticators } from '@adonisjs/auth/types'
|
||||||
|
|
||||||
|
const authConfig = defineConfig({
|
||||||
|
default: 'basicAuth',
|
||||||
|
guards: {
|
||||||
|
basicAuth: basicAuthGuard({
|
||||||
|
provider: basicAuthUserProvider({
|
||||||
|
model: () => import('#models/user')
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export default authConfig
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inferring types from the configured auth
|
||||||
|
* guards.
|
||||||
|
*/
|
||||||
|
declare module '@adonisjs/auth/types' {
|
||||||
|
export interface Authenticators extends InferAuthenticators<typeof authConfig> {}
|
||||||
|
}
|
||||||
|
declare module '@adonisjs/core/types' {
|
||||||
|
interface EventsList extends InferAuthEvents<Authenticators> {}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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: true,
|
||||||
|
methods: ['GET', 'HEAD', 'POST', 'PUT', 'DELETE'],
|
||||||
|
headers: true,
|
||||||
|
exposeHeaders: [],
|
||||||
|
credentials: true,
|
||||||
|
maxAge: 90,
|
||||||
|
})
|
||||||
|
|
||||||
|
export default corsConfig
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
import env from '#start/env'
|
||||||
|
import { defineConfig } from '@adonisjs/lucid'
|
||||||
|
|
||||||
|
const dbConfig = defineConfig({
|
||||||
|
connection: 'mysql',
|
||||||
|
connections: {
|
||||||
|
mysql: {
|
||||||
|
client: 'mysql2',
|
||||||
|
connection: {
|
||||||
|
host: env.get('DB_HOST'),
|
||||||
|
port: env.get('DB_PORT'),
|
||||||
|
user: env.get('DB_USER'),
|
||||||
|
password: env.get('DB_PASSWORD'),
|
||||||
|
database: env.get('DB_DATABASE'),
|
||||||
|
},
|
||||||
|
migrations: {
|
||||||
|
naturalSort: true,
|
||||||
|
paths: ['database/migrations'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export default dbConfig
|
||||||
|
|
@ -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> {}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
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()
|
||||||
|
.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> {}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
import { BaseSchema } from '@adonisjs/lucid/schema'
|
||||||
|
|
||||||
|
export default class extends BaseSchema {
|
||||||
|
protected tableName = 'users'
|
||||||
|
|
||||||
|
async up() {
|
||||||
|
this.schema.createTable(this.tableName, (table) => {
|
||||||
|
table.increments('id').notNullable()
|
||||||
|
table.string('full_name').nullable()
|
||||||
|
table.string('email', 254).notNullable().unique()
|
||||||
|
table.string('password').notNullable()
|
||||||
|
|
||||||
|
table.timestamps()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async down() {
|
||||||
|
this.schema.dropTable(this.tableName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
import { BaseSchema } from '@adonisjs/lucid/schema'
|
||||||
|
|
||||||
|
export default class extends BaseSchema {
|
||||||
|
protected tableName = 'stations'
|
||||||
|
|
||||||
|
async up() {
|
||||||
|
this.schema.createTable(this.tableName, (table) => {
|
||||||
|
table.increments('id')
|
||||||
|
table.string('name').notNullable()
|
||||||
|
table.string('ip').notNullable()
|
||||||
|
table.integer('port').notNullable()
|
||||||
|
table.string('netmask')
|
||||||
|
table.string('network')
|
||||||
|
table.string('gateway')
|
||||||
|
table.string('tftp_ip')
|
||||||
|
table.string('apc_1_ip')
|
||||||
|
table.integer('apc_1_port')
|
||||||
|
table.string('apc_1_username')
|
||||||
|
table.string('apc_1_password')
|
||||||
|
table.string('apc_2_ip')
|
||||||
|
table.integer('apc_2_port')
|
||||||
|
table.string('apc_2_username')
|
||||||
|
table.string('apc_2_password')
|
||||||
|
table.string('switch_control_ip')
|
||||||
|
table.integer('switch_control_port')
|
||||||
|
table.string('switch_control_username')
|
||||||
|
table.string('switch_control_password')
|
||||||
|
table.timestamps()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async down() {
|
||||||
|
this.schema.dropTable(this.tableName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
import { BaseSchema } from '@adonisjs/lucid/schema'
|
||||||
|
|
||||||
|
export default class extends BaseSchema {
|
||||||
|
protected tableName = 'lines'
|
||||||
|
|
||||||
|
async up() {
|
||||||
|
this.schema.createTable(this.tableName, (table) => {
|
||||||
|
table.increments('id')
|
||||||
|
table.integer('port').notNullable()
|
||||||
|
table.integer('line_number').notNullable()
|
||||||
|
table.integer('line_clear')
|
||||||
|
table.integer('outlet')
|
||||||
|
table.integer('station_id').unsigned().references('id').inTable('stations')
|
||||||
|
table.string('apc_name').notNullable()
|
||||||
|
table.timestamps()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async down() {
|
||||||
|
this.schema.dropTable(this.tableName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
import { BaseSchema } from '@adonisjs/lucid/schema'
|
||||||
|
|
||||||
|
export default class extends BaseSchema {
|
||||||
|
protected tableName = 'scenarios'
|
||||||
|
|
||||||
|
async up() {
|
||||||
|
this.schema.createTable(this.tableName, (table) => {
|
||||||
|
table.increments('id')
|
||||||
|
table.string('title').notNullable()
|
||||||
|
table.string('name').notNullable()
|
||||||
|
table.text('body').notNullable()
|
||||||
|
table.integer('timeout').notNullable()
|
||||||
|
table.boolean('isReboot').defaultTo(false)
|
||||||
|
table.timestamps()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async down() {
|
||||||
|
this.schema.dropTable(this.tableName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
import { BaseSchema } from '@adonisjs/lucid/schema'
|
||||||
|
|
||||||
|
export default class extends BaseSchema {
|
||||||
|
protected tableName = 'logs'
|
||||||
|
|
||||||
|
async up() {
|
||||||
|
this.schema.createTable(this.tableName, (table) => {
|
||||||
|
table.increments('id')
|
||||||
|
table.string('path').notNullable()
|
||||||
|
table.integer('line_id').unsigned().references('id').inTable('lines')
|
||||||
|
table.string('PID')
|
||||||
|
table.string('SN')
|
||||||
|
table.string('description')
|
||||||
|
table.timestamps()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async down() {
|
||||||
|
this.schema.dropTable(this.tableName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
import { BaseSchema } from '@adonisjs/lucid/schema'
|
||||||
|
|
||||||
|
export default class extends BaseSchema {
|
||||||
|
protected tableName = 'models'
|
||||||
|
|
||||||
|
async up() {
|
||||||
|
this.schema.createTable(this.tableName, (table) => {
|
||||||
|
table.increments('id')
|
||||||
|
table.string('name').notNullable()
|
||||||
|
table.timestamps()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async down() {
|
||||||
|
this.schema.dropTable(this.tableName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
import { configApp } from '@adonisjs/eslint-config'
|
||||||
|
export default configApp()
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,72 @@
|
||||||
|
{
|
||||||
|
"name": "BACKEND",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"license": "UNLICENSED",
|
||||||
|
"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",
|
||||||
|
"#start/*": "./start/*.js",
|
||||||
|
"#tests/*": "./tests/*.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/api-client": "^3.1.0",
|
||||||
|
"@japa/assert": "^4.0.1",
|
||||||
|
"@japa/plugin-adonisjs": "^4.0.0",
|
||||||
|
"@japa/runner": "^4.2.0",
|
||||||
|
"@swc/core": "1.11.24",
|
||||||
|
"@types/luxon": "^3.7.1",
|
||||||
|
"@types/node": "^22.15.18",
|
||||||
|
"eslint": "^9.26.0",
|
||||||
|
"hot-hook": "^0.4.0",
|
||||||
|
"pino-pretty": "^13.0.0",
|
||||||
|
"prettier": "^3.5.3",
|
||||||
|
"ts-node-maintained": "^10.9.5",
|
||||||
|
"typescript": "~5.8"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@adonisjs/auth": "^9.4.0",
|
||||||
|
"@adonisjs/core": "^6.18.0",
|
||||||
|
"@adonisjs/cors": "^2.2.1",
|
||||||
|
"@adonisjs/lucid": "^21.6.1",
|
||||||
|
"@vinejs/vine": "^3.0.1",
|
||||||
|
"luxon": "^3.7.2",
|
||||||
|
"mysql2": "^3.15.3",
|
||||||
|
"net": "^1.0.2",
|
||||||
|
"reflect-metadata": "^0.2.2",
|
||||||
|
"socket.io": "^4.8.1"
|
||||||
|
},
|
||||||
|
"hotHook": {
|
||||||
|
"boundaries": [
|
||||||
|
"./app/controllers/**/*.ts",
|
||||||
|
"./app/middleware/*.ts"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"prettier": "@adonisjs/prettier-config"
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,148 @@
|
||||||
|
import { Server as SocketIOServer } from 'socket.io'
|
||||||
|
import http from 'node:http'
|
||||||
|
import LineConnection from '../app/services/line_connection.js'
|
||||||
|
import { ApplicationService } from '@adonisjs/core/types'
|
||||||
|
import env from '#start/env'
|
||||||
|
import { CustomServer, CustomSocket } from '../app/ultils/types.js'
|
||||||
|
|
||||||
|
interface Station {
|
||||||
|
id: number
|
||||||
|
name: string
|
||||||
|
ip: string
|
||||||
|
lines: any[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class SocketIoProvider {
|
||||||
|
private static _io: CustomServer
|
||||||
|
constructor(protected app: ApplicationService) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register bindings to the container
|
||||||
|
*/
|
||||||
|
register() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The container bindings have booted
|
||||||
|
*/
|
||||||
|
async boot() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The application has been booted
|
||||||
|
*/
|
||||||
|
async start() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The process has been started
|
||||||
|
*/
|
||||||
|
async ready() {
|
||||||
|
if (process.argv[1].includes('server.js')) {
|
||||||
|
const webSocket = new WebSocketIo(this.app)
|
||||||
|
SocketIoProvider._io = await webSocket.boot()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Preparing to shutdown the app
|
||||||
|
*/
|
||||||
|
async shutdown() {}
|
||||||
|
|
||||||
|
public static get io() {
|
||||||
|
return this._io
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class WebSocketIo {
|
||||||
|
intervalMap: { [key: string]: NodeJS.Timeout } = {}
|
||||||
|
stationMap: Map<number, Station> = new Map()
|
||||||
|
lineMap: Map<number, LineConnection> = new Map() // key = lineId
|
||||||
|
|
||||||
|
constructor(protected app: ApplicationService) {}
|
||||||
|
|
||||||
|
async boot() {
|
||||||
|
const SOCKET_IO_PORT = env.get('SOCKET_PORT') || 8989
|
||||||
|
const FRONTEND_URL = env.get('FRONTEND_URL') || 'http://localhost:5173'
|
||||||
|
|
||||||
|
const socketServer = http.createServer()
|
||||||
|
const io = new SocketIOServer(socketServer, {
|
||||||
|
pingInterval: 25000, // 25s server gửi ping
|
||||||
|
pingTimeout: 20000, // chờ 20s không có pong thì disconnect
|
||||||
|
cors: {
|
||||||
|
origin: [FRONTEND_URL],
|
||||||
|
methods: ['GET', 'POST'],
|
||||||
|
credentials: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
io.on('connection', (socket: CustomSocket) => {
|
||||||
|
console.log('Socket connected:', socket.id)
|
||||||
|
socket.connectionTime = new Date()
|
||||||
|
|
||||||
|
socket.on('disconnect', () => {
|
||||||
|
console.log(`🔴 FE disconnected: ${socket.id}`)
|
||||||
|
})
|
||||||
|
|
||||||
|
// FE gửi yêu cầu connect lines
|
||||||
|
socket.on('connect_lines', async (stationData: Station) => {
|
||||||
|
console.log('📡 Yêu cầu connect station:', stationData.name)
|
||||||
|
await this.connectStation(socket, stationData)
|
||||||
|
})
|
||||||
|
|
||||||
|
// FE gửi command đến line cụ thể
|
||||||
|
socket.on('send_command', (data) => {
|
||||||
|
const { lineId, command } = data
|
||||||
|
const line = this.lineMap.get(lineId)
|
||||||
|
if (line) {
|
||||||
|
line.sendCommand(command)
|
||||||
|
} else {
|
||||||
|
socket.emit('line_error', { lineId, error: 'Line not connected' })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// FE yêu cầu ngắt kết nối 1 station
|
||||||
|
socket.on('disconnect_station', (stationId) => {
|
||||||
|
this.disconnectStation(stationId)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
socketServer.listen(SOCKET_IO_PORT, () => {
|
||||||
|
console.log(`Socket server is running on port ${SOCKET_IO_PORT}`)
|
||||||
|
})
|
||||||
|
|
||||||
|
return io
|
||||||
|
}
|
||||||
|
|
||||||
|
private async connectStation(socket, station: Station) {
|
||||||
|
this.stationMap.set(station.id, station)
|
||||||
|
for (const line of station.lines) {
|
||||||
|
const lineConn = new LineConnection(
|
||||||
|
{
|
||||||
|
id: line.id,
|
||||||
|
port: line.port,
|
||||||
|
ip: station.ip,
|
||||||
|
lineNumber: line.lineNumber,
|
||||||
|
stationId: station.id,
|
||||||
|
apcName: line.apcName,
|
||||||
|
},
|
||||||
|
this.io
|
||||||
|
)
|
||||||
|
await lineConn.connect()
|
||||||
|
this.lineMap.set(line.id, lineConn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private disconnectStation(stationId: number) {
|
||||||
|
const station = this.stationMap.get(stationId)
|
||||||
|
if (!station) return
|
||||||
|
|
||||||
|
for (const line of station.lines) {
|
||||||
|
const conn = this.lineMap.get(line.id)
|
||||||
|
if (conn) {
|
||||||
|
conn.disconnect()
|
||||||
|
this.lineMap.delete(line.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.stationMap.delete(stationId)
|
||||||
|
console.log(`🔻 Station ${station.name} disconnected`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| 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.enum(['fatal', 'error', 'warn', 'info', 'debug', 'trace', 'silent']),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|----------------------------------------------------------
|
||||||
|
| Variables for configuring database connection
|
||||||
|
|----------------------------------------------------------
|
||||||
|
*/
|
||||||
|
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()
|
||||||
|
})
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| 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('#middleware/force_json_response_middleware'),
|
||||||
|
() => import('@adonisjs/cors/cors_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/auth/initialize_auth_middleware')])
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Named middleware collection must be explicitly assigned to
|
||||||
|
* the routes or the routes group.
|
||||||
|
*/
|
||||||
|
export const middleware = router.named({
|
||||||
|
auth: () => import('#middleware/auth_middleware')
|
||||||
|
})
|
||||||
|
|
@ -0,0 +1,65 @@
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Routes file
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| The routes file is used for defining the HTTP routes.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
import router from '@adonisjs/core/services/router'
|
||||||
|
|
||||||
|
router
|
||||||
|
.group(() => {
|
||||||
|
router.get('/', '#controllers/stations_controller.index')
|
||||||
|
router.post('create', '#controllers/stations_controller.store')
|
||||||
|
router.post('update', '#controllers/stations_controller.update')
|
||||||
|
router.post('delete', '#controllers/stations_controller.destroy')
|
||||||
|
})
|
||||||
|
.prefix('api/stations')
|
||||||
|
|
||||||
|
router
|
||||||
|
.group(() => {
|
||||||
|
router.get('/', '#controllers/lines_controller.get')
|
||||||
|
router.post('create', '#controllers/lines_controller.create')
|
||||||
|
router.post('update', '#controllers/lines_controller.update')
|
||||||
|
router.post('delete', '#controllers/lines_controller.delete')
|
||||||
|
})
|
||||||
|
.prefix('api/lines')
|
||||||
|
|
||||||
|
router
|
||||||
|
.group(() => {
|
||||||
|
router.get('list', '#controllers/logs_controller.list')
|
||||||
|
router.post('viewLog', '#controllers/logs_controller.viewLog')
|
||||||
|
router.post('downloadLog', '#controllers/logs_controller.downloadLog')
|
||||||
|
})
|
||||||
|
.prefix('api/logs')
|
||||||
|
|
||||||
|
router
|
||||||
|
.group(() => {
|
||||||
|
router.get('/', '#controllers/users_controller.index')
|
||||||
|
router.get('/:id', '#controllers/users_controller.get')
|
||||||
|
router.post('create', '#controllers/users_controller.store')
|
||||||
|
router.post('update', '#controllers/users_controller.update')
|
||||||
|
router.post('delete', '#controllers/users_controller.destroy')
|
||||||
|
router.post('getByEmail', '#controllers/users_controller.getByEmail')
|
||||||
|
})
|
||||||
|
.prefix('api/users')
|
||||||
|
|
||||||
|
router
|
||||||
|
.group(() => {
|
||||||
|
router.get('/', '#controllers/models_controller.index')
|
||||||
|
router.post('create', '#controllers/models_controller.store')
|
||||||
|
router.post('update', '#controllers/models_controller.update')
|
||||||
|
router.post('delete', '#controllers/models_controller.destroy')
|
||||||
|
})
|
||||||
|
.prefix('api/models')
|
||||||
|
|
||||||
|
router
|
||||||
|
.group(() => {
|
||||||
|
router.get('/', '#controllers/scenarios_controller.get')
|
||||||
|
router.post('create', '#controllers/scenarios_controller.create')
|
||||||
|
router.post('update', '#controllers/scenarios_controller.update')
|
||||||
|
router.post('delete', '#controllers/scenarios_controller.delete')
|
||||||
|
})
|
||||||
|
.prefix('api/scenarios')
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
import { assert } from '@japa/assert'
|
||||||
|
import { apiClient } from '@japa/api-client'
|
||||||
|
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(), apiClient(), 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())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"extends": "@adonisjs/tsconfig/tsconfig.app.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"rootDir": "./",
|
||||||
|
"outDir": "./build"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
VITE_BACKEND_URL=http://localhost:3333/
|
||||||
|
VITE_SOCKET_SERVER=http://localhost:8989/
|
||||||
|
VITE_GOOGLE_CLIEND_ID=532287737140-2e3kb67raaac56u2uohnqveg6gt7vga9.apps.googleusercontent.com
|
||||||
|
VITE_LOCALSTORAGE_VARIABLE=au_ma_te_da
|
||||||
|
VITE_DOMAIN=http://localhost:5173/
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
|
|
@ -0,0 +1,73 @@
|
||||||
|
# React + TypeScript + Vite
|
||||||
|
|
||||||
|
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
|
||||||
|
|
||||||
|
Currently, two official plugins are available:
|
||||||
|
|
||||||
|
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh
|
||||||
|
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
|
||||||
|
|
||||||
|
## React Compiler
|
||||||
|
|
||||||
|
The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
|
||||||
|
|
||||||
|
## Expanding the ESLint configuration
|
||||||
|
|
||||||
|
If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
|
||||||
|
|
||||||
|
```js
|
||||||
|
export default defineConfig([
|
||||||
|
globalIgnores(['dist']),
|
||||||
|
{
|
||||||
|
files: ['**/*.{ts,tsx}'],
|
||||||
|
extends: [
|
||||||
|
// Other configs...
|
||||||
|
|
||||||
|
// Remove tseslint.configs.recommended and replace with this
|
||||||
|
tseslint.configs.recommendedTypeChecked,
|
||||||
|
// Alternatively, use this for stricter rules
|
||||||
|
tseslint.configs.strictTypeChecked,
|
||||||
|
// Optionally, add this for stylistic rules
|
||||||
|
tseslint.configs.stylisticTypeChecked,
|
||||||
|
|
||||||
|
// Other configs...
|
||||||
|
],
|
||||||
|
languageOptions: {
|
||||||
|
parserOptions: {
|
||||||
|
project: ['./tsconfig.node.json', './tsconfig.app.json'],
|
||||||
|
tsconfigRootDir: import.meta.dirname,
|
||||||
|
},
|
||||||
|
// other options...
|
||||||
|
},
|
||||||
|
},
|
||||||
|
])
|
||||||
|
```
|
||||||
|
|
||||||
|
You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
|
||||||
|
|
||||||
|
```js
|
||||||
|
// eslint.config.js
|
||||||
|
import reactX from 'eslint-plugin-react-x'
|
||||||
|
import reactDom from 'eslint-plugin-react-dom'
|
||||||
|
|
||||||
|
export default defineConfig([
|
||||||
|
globalIgnores(['dist']),
|
||||||
|
{
|
||||||
|
files: ['**/*.{ts,tsx}'],
|
||||||
|
extends: [
|
||||||
|
// Other configs...
|
||||||
|
// Enable lint rules for React
|
||||||
|
reactX.configs['recommended-typescript'],
|
||||||
|
// Enable lint rules for React DOM
|
||||||
|
reactDom.configs.recommended,
|
||||||
|
],
|
||||||
|
languageOptions: {
|
||||||
|
parserOptions: {
|
||||||
|
project: ['./tsconfig.node.json', './tsconfig.app.json'],
|
||||||
|
tsconfigRootDir: import.meta.dirname,
|
||||||
|
},
|
||||||
|
// other options...
|
||||||
|
},
|
||||||
|
},
|
||||||
|
])
|
||||||
|
```
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
import js from '@eslint/js'
|
||||||
|
import globals from 'globals'
|
||||||
|
import reactHooks from 'eslint-plugin-react-hooks'
|
||||||
|
import reactRefresh from 'eslint-plugin-react-refresh'
|
||||||
|
import tseslint from 'typescript-eslint'
|
||||||
|
import { defineConfig, globalIgnores } from 'eslint/config'
|
||||||
|
|
||||||
|
export default defineConfig([
|
||||||
|
globalIgnores(['dist']),
|
||||||
|
{
|
||||||
|
files: ['**/*.{ts,tsx}'],
|
||||||
|
extends: [
|
||||||
|
js.configs.recommended,
|
||||||
|
tseslint.configs.recommended,
|
||||||
|
reactHooks.configs['recommended-latest'],
|
||||||
|
reactRefresh.configs.vite,
|
||||||
|
],
|
||||||
|
languageOptions: {
|
||||||
|
ecmaVersion: 2020,
|
||||||
|
globals: globals.browser,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>frontend</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,36 @@
|
||||||
|
{
|
||||||
|
"name": "ATC",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "tsc -b && vite build",
|
||||||
|
"lint": "eslint .",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@mantine/core": "^8.3.5",
|
||||||
|
"@mantine/dates": "^8.3.5",
|
||||||
|
"@mantine/notifications": "^8.3.5",
|
||||||
|
"@tabler/icons-react": "^3.35.0",
|
||||||
|
"axios": "^1.12.2",
|
||||||
|
"react": "^19.1.1",
|
||||||
|
"react-dom": "^19.1.1",
|
||||||
|
"socket.io-client": "^4.8.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@eslint/js": "^9.36.0",
|
||||||
|
"@types/node": "^24.9.1",
|
||||||
|
"@types/react": "^19.1.16",
|
||||||
|
"@types/react-dom": "^19.1.9",
|
||||||
|
"@vitejs/plugin-react": "^5.0.4",
|
||||||
|
"eslint": "^9.36.0",
|
||||||
|
"eslint-plugin-react-hooks": "^5.2.0",
|
||||||
|
"eslint-plugin-react-refresh": "^0.4.22",
|
||||||
|
"globals": "^16.4.0",
|
||||||
|
"typescript": "~5.9.3",
|
||||||
|
"typescript-eslint": "^8.45.0",
|
||||||
|
"vite": "^7.1.7"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||||
|
After Width: | Height: | Size: 1.5 KiB |
|
|
@ -0,0 +1,9 @@
|
||||||
|
#root {
|
||||||
|
width: 100%;
|
||||||
|
height: 100vh;
|
||||||
|
/* background-color: aliceblue; */
|
||||||
|
}
|
||||||
|
|
||||||
|
button:focus{
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,54 @@
|
||||||
|
.test {
|
||||||
|
/* background-color: red; */
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: 'Mulish', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list {
|
||||||
|
position: relative;
|
||||||
|
margin-bottom: var(--mantine-spacing-md);
|
||||||
|
gap: 10px;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.indicator {
|
||||||
|
background-color: var(--mantine-color-white);
|
||||||
|
border-radius: var(--mantine-radius-md);
|
||||||
|
border: 1px solid var(--mantine-color-gray-2);
|
||||||
|
box-shadow: var(--mantine-shadow-sm);
|
||||||
|
|
||||||
|
@mixin dark {
|
||||||
|
background-color: var(--mantine-color-dark-6);
|
||||||
|
border-color: var(--mantine-color-dark-4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab {
|
||||||
|
z-index: 1;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: color 100ms ease;
|
||||||
|
color: var(--mantine-color-gray-7);
|
||||||
|
background-color: var(--mantine-color-white);
|
||||||
|
outline: none;
|
||||||
|
font-size: 16px;
|
||||||
|
|
||||||
|
&[data-active] {
|
||||||
|
color: var(--mantine-color-black);
|
||||||
|
background-color: #a6ffe1 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
@mixin dark {
|
||||||
|
color: var(--mantine-color-dark-1);
|
||||||
|
|
||||||
|
&[data-active] {
|
||||||
|
color: var(--mantine-color-white);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.content{
|
||||||
|
width: 100%;
|
||||||
|
border-top: 1px #ccc solid;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,176 @@
|
||||||
|
import "@mantine/core/styles.css";
|
||||||
|
import "@mantine/dates/styles.css";
|
||||||
|
import "@mantine/notifications/styles.css";
|
||||||
|
import "./App.css";
|
||||||
|
import classes from "./App.module.css";
|
||||||
|
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import {
|
||||||
|
Tabs,
|
||||||
|
Text,
|
||||||
|
Container,
|
||||||
|
Flex,
|
||||||
|
MantineProvider,
|
||||||
|
FloatingIndicator,
|
||||||
|
Grid,
|
||||||
|
ScrollArea,
|
||||||
|
Button,
|
||||||
|
ActionIcon,
|
||||||
|
} from "@mantine/core";
|
||||||
|
import type { TLine, TStation } from "./untils/types";
|
||||||
|
import axios from "axios";
|
||||||
|
import CardLine from "./components/CardLine";
|
||||||
|
import { IconEdit, IconSettingsPlus } from "@tabler/icons-react";
|
||||||
|
|
||||||
|
const apiUrl = import.meta.env.VITE_BACKEND_URL;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main Component
|
||||||
|
*/
|
||||||
|
export default function App() {
|
||||||
|
document.title = "Automation Test";
|
||||||
|
const [stations, setStations] = useState<TStation[]>([]);
|
||||||
|
const [selectedLines, setSelectedLines] = useState<TLine[]>([]);
|
||||||
|
const [activeTab, setActiveTab] = useState("0");
|
||||||
|
const [controlsRefs, setControlsRefs] = useState<
|
||||||
|
Record<string, HTMLButtonElement | null>
|
||||||
|
>({});
|
||||||
|
const [rootRef, setRootRef] = useState<HTMLDivElement | null>(null);
|
||||||
|
const setControlRef = (val: string) => (node: HTMLButtonElement) => {
|
||||||
|
controlsRefs[val] = node;
|
||||||
|
setControlsRefs(controlsRefs);
|
||||||
|
};
|
||||||
|
const [showBottomShadow, setShowBottomShadow] = useState(false);
|
||||||
|
|
||||||
|
// function get list station
|
||||||
|
const getStation = async () => {
|
||||||
|
try {
|
||||||
|
const response = await axios.get(apiUrl + "api/stations");
|
||||||
|
if (response.status) {
|
||||||
|
if (Array.isArray(response.data)) {
|
||||||
|
setStations(response.data);
|
||||||
|
if (response.data?.length > 0)
|
||||||
|
setActiveTab(response.data[0]?.id.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Error get station", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getStation();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MantineProvider>
|
||||||
|
<Container py="xl" w={"100%"} style={{ maxWidth: "100%" }}>
|
||||||
|
{/* Tabs (Top Bar) */}
|
||||||
|
<Tabs
|
||||||
|
value={activeTab}
|
||||||
|
onChange={(id) => setActiveTab(id?.toString() || "0")}
|
||||||
|
variant="none"
|
||||||
|
keepMounted={false}
|
||||||
|
>
|
||||||
|
<Tabs.List ref={setRootRef} className={classes.list}>
|
||||||
|
{stations.map((station) => (
|
||||||
|
<Tabs.Tab
|
||||||
|
ref={setControlRef(station.id.toString())}
|
||||||
|
className={classes.tab}
|
||||||
|
key={station.id}
|
||||||
|
value={station.id.toString()}
|
||||||
|
>
|
||||||
|
{station.name}
|
||||||
|
</Tabs.Tab>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<FloatingIndicator
|
||||||
|
target={activeTab ? controlsRefs[activeTab] : null}
|
||||||
|
parent={rootRef}
|
||||||
|
className={classes.indicator}
|
||||||
|
/>
|
||||||
|
<Flex gap={"sm"}>
|
||||||
|
<ActionIcon title="Add Station" variant="outline" color="green">
|
||||||
|
<IconSettingsPlus />
|
||||||
|
</ActionIcon>
|
||||||
|
<ActionIcon title="Edit Station" variant="outline">
|
||||||
|
<IconEdit />
|
||||||
|
</ActionIcon>
|
||||||
|
</Flex>
|
||||||
|
</Tabs.List>
|
||||||
|
|
||||||
|
{stations.map((station) => (
|
||||||
|
<Tabs.Panel
|
||||||
|
className={classes.content}
|
||||||
|
key={station.id}
|
||||||
|
value={station.id.toString()}
|
||||||
|
pt="md"
|
||||||
|
>
|
||||||
|
<Grid>
|
||||||
|
<Grid.Col
|
||||||
|
span={10}
|
||||||
|
style={{
|
||||||
|
boxShadow: showBottomShadow
|
||||||
|
? "inset 0 -12px 10px -10px rgba(0, 0, 0, 0.2)"
|
||||||
|
: "none",
|
||||||
|
borderRadius: 8,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ScrollArea
|
||||||
|
h={"84vh"}
|
||||||
|
onScrollPositionChange={({ y }) => {
|
||||||
|
const el = document.querySelector(
|
||||||
|
".mantine-ScrollArea-viewport"
|
||||||
|
);
|
||||||
|
if (!el) return;
|
||||||
|
const maxScroll = el.scrollHeight - el.clientHeight;
|
||||||
|
setShowBottomShadow(y < maxScroll - 2);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{station.lines.length > 0 ? (
|
||||||
|
<Flex wrap="wrap" gap="sm" justify={"center"}>
|
||||||
|
{[
|
||||||
|
...station.lines,
|
||||||
|
...station.lines,
|
||||||
|
...station.lines,
|
||||||
|
].map((line) => (
|
||||||
|
<CardLine
|
||||||
|
line={line}
|
||||||
|
selectedLines={selectedLines}
|
||||||
|
setSelectedLines={setSelectedLines}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Flex>
|
||||||
|
) : (
|
||||||
|
<Text ta="center" c="dimmed" mt="lg">
|
||||||
|
No lines configured
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</ScrollArea>
|
||||||
|
</Grid.Col>
|
||||||
|
<Grid.Col
|
||||||
|
span={2}
|
||||||
|
style={{ backgroundColor: "#f1f1f1", borderRadius: 8 }}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="filled"
|
||||||
|
style={{ height: "30px", width: "120px" }}
|
||||||
|
onClick={() => {
|
||||||
|
if (selectedLines.length !== station.lines.length)
|
||||||
|
setSelectedLines(station.lines);
|
||||||
|
else setSelectedLines([]);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{selectedLines.length !== station.lines.length
|
||||||
|
? "Select All"
|
||||||
|
: "Deselect All"}
|
||||||
|
</Button>
|
||||||
|
</Grid.Col>
|
||||||
|
</Grid>
|
||||||
|
</Tabs.Panel>
|
||||||
|
))}
|
||||||
|
</Tabs>
|
||||||
|
</Container>
|
||||||
|
</MantineProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>
|
||||||
|
After Width: | Height: | Size: 4.0 KiB |
|
|
@ -0,0 +1,62 @@
|
||||||
|
import { Card, Text, Box, Flex } from "@mantine/core";
|
||||||
|
import type { TLine } from "../untils/types";
|
||||||
|
import classes from "./Component.module.css";
|
||||||
|
|
||||||
|
const CardLine = ({
|
||||||
|
line,
|
||||||
|
selectedLines,
|
||||||
|
setSelectedLines,
|
||||||
|
}: {
|
||||||
|
line: TLine;
|
||||||
|
selectedLines: TLine[];
|
||||||
|
setSelectedLines: (lines: React.SetStateAction<TLine[]>) => void;
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
key={line.id}
|
||||||
|
shadow="sm"
|
||||||
|
radius="md"
|
||||||
|
withBorder
|
||||||
|
className={classes.card_line}
|
||||||
|
style={
|
||||||
|
selectedLines.find((val) => val.id === line.id)
|
||||||
|
? { backgroundColor: "#8bf55940" }
|
||||||
|
: {}
|
||||||
|
}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
if (selectedLines.find((val) => val.id === line.id))
|
||||||
|
setSelectedLines(selectedLines.filter((val) => val.id !== line.id));
|
||||||
|
else setSelectedLines((pre) => [...pre, line]);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Flex justify={"space-between"}>
|
||||||
|
<Box>
|
||||||
|
<div>
|
||||||
|
<Text fw={600}>
|
||||||
|
Line {line.lineNumber} - {line.port}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
<Text className={classes.info_line}>PID: WS-C3560CG-8PC-S</Text>
|
||||||
|
<div className={classes.info_line}>SN: FGL2240307M</div>
|
||||||
|
<div className={classes.info_line}>VID: V01</div>
|
||||||
|
</Box>
|
||||||
|
<Box
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
}}
|
||||||
|
style={{ backgroundColor: "black", height: "130px", width: "220px" }}
|
||||||
|
></Box>
|
||||||
|
</Flex>
|
||||||
|
{/* <Flex justify={"flex-end"}>
|
||||||
|
<Button variant="filled" style={{ height: "30px", width: "70px" }}>
|
||||||
|
Take
|
||||||
|
</Button>
|
||||||
|
</Flex> */}
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CardLine;
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
.card_line {
|
||||||
|
width: 400px;
|
||||||
|
height: 150px;
|
||||||
|
padding: 8px;
|
||||||
|
gap: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info_line {
|
||||||
|
color: dimgrey;
|
||||||
|
font-size: 12px;
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
margin-top: 4px;
|
||||||
|
height: 20px;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,64 @@
|
||||||
|
import React, { createContext, useContext, useEffect, useState } from "react";
|
||||||
|
import { io, Socket } from "socket.io-client";
|
||||||
|
import { SOCKET_EVENTS } from "../untils/constanst";
|
||||||
|
import { notifications } from "@mantine/notifications";
|
||||||
|
|
||||||
|
interface ISocketContext {
|
||||||
|
socket: Socket | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SocketContext = createContext<ISocketContext | undefined>(undefined);
|
||||||
|
const SOCKET_URL =
|
||||||
|
import.meta.env.VITE_SOCKET_SERVER || "http://localhost:8989/";
|
||||||
|
|
||||||
|
export const SocketProvider: React.FC<{ children: React.ReactNode }> = ({
|
||||||
|
children,
|
||||||
|
}) => {
|
||||||
|
const [socket, setSocket] = useState<Socket | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const newSocket = io(SOCKET_URL);
|
||||||
|
|
||||||
|
setSocket(newSocket);
|
||||||
|
|
||||||
|
newSocket.on("connect", () => {
|
||||||
|
console.log("Connected to Socket Server");
|
||||||
|
|
||||||
|
newSocket.emit(SOCKET_EVENTS.ROOM.JOINED);
|
||||||
|
console.log(`Joined room`);
|
||||||
|
});
|
||||||
|
|
||||||
|
newSocket.on("disconnect", () => {
|
||||||
|
console.log("Disconnected from Socket Server");
|
||||||
|
});
|
||||||
|
|
||||||
|
newSocket.on("error", (error) => {
|
||||||
|
console.error("Connection failed:", error);
|
||||||
|
notifications.show({
|
||||||
|
title: "Error",
|
||||||
|
message: error.message,
|
||||||
|
color: "red",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
newSocket.emit(SOCKET_EVENTS.ROOM.LEFT);
|
||||||
|
|
||||||
|
newSocket.disconnect();
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SocketContext.Provider value={{ socket }}>
|
||||||
|
{children}
|
||||||
|
</SocketContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useSocket = () => {
|
||||||
|
const context = useContext(SocketContext);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error("useSocket must be used within a SocketProvider");
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,68 @@
|
||||||
|
:root {
|
||||||
|
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||||
|
line-height: 1.5;
|
||||||
|
font-weight: 400;
|
||||||
|
|
||||||
|
color-scheme: light dark;
|
||||||
|
color: rgba(255, 255, 255, 0.87);
|
||||||
|
background-color: #242424;
|
||||||
|
|
||||||
|
font-synthesis: none;
|
||||||
|
text-rendering: optimizeLegibility;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
font-weight: 500;
|
||||||
|
color: #646cff;
|
||||||
|
text-decoration: inherit;
|
||||||
|
}
|
||||||
|
a:hover {
|
||||||
|
color: #535bf2;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
display: flex;
|
||||||
|
place-items: center;
|
||||||
|
min-width: 320px;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 3.2em;
|
||||||
|
line-height: 1.1;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
padding: 0.6em 1.2em;
|
||||||
|
font-size: 1em;
|
||||||
|
font-weight: 500;
|
||||||
|
font-family: inherit;
|
||||||
|
background-color: #1a1a1a;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: border-color 0.25s;
|
||||||
|
}
|
||||||
|
button:hover {
|
||||||
|
border-color: #646cff;
|
||||||
|
}
|
||||||
|
button:focus,
|
||||||
|
button:focus-visible {
|
||||||
|
outline: 4px auto -webkit-focus-ring-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: light) {
|
||||||
|
:root {
|
||||||
|
color: #213547;
|
||||||
|
background-color: #ffffff;
|
||||||
|
}
|
||||||
|
a:hover {
|
||||||
|
color: #747bff;
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
background-color: #f9f9f9;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
import { StrictMode } from 'react'
|
||||||
|
import { createRoot } from 'react-dom/client'
|
||||||
|
import './index.css'
|
||||||
|
import App from './App.tsx'
|
||||||
|
|
||||||
|
createRoot(document.getElementById('root')!).render(
|
||||||
|
<StrictMode>
|
||||||
|
<App />
|
||||||
|
</StrictMode>,
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,246 @@
|
||||||
|
import {
|
||||||
|
IconBan,
|
||||||
|
IconCrown,
|
||||||
|
IconDeviceDesktop,
|
||||||
|
IconFile,
|
||||||
|
IconHome,
|
||||||
|
IconRouter,
|
||||||
|
IconServer,
|
||||||
|
IconWebhook,
|
||||||
|
IconSettingsAutomation,
|
||||||
|
IconKey,
|
||||||
|
IconClipboardList,
|
||||||
|
} from '@tabler/icons-react'
|
||||||
|
|
||||||
|
export const SOCKET_EVENTS = {
|
||||||
|
ROOM: {
|
||||||
|
JOINED: 'room_joined',
|
||||||
|
LEFT: 'room_left',
|
||||||
|
},
|
||||||
|
|
||||||
|
APP_STATUS: { RECEIVED: 'app_status_received' },
|
||||||
|
|
||||||
|
APP_DATA: {
|
||||||
|
SENT: 'app_data_sent',
|
||||||
|
RECEIVED: 'app_data_received',
|
||||||
|
},
|
||||||
|
|
||||||
|
SCRIPT_TEST: {
|
||||||
|
SENT: 'script_test_sent',
|
||||||
|
TIME_RECEIVED: 'script_test_time_received',
|
||||||
|
},
|
||||||
|
|
||||||
|
DATA_OUTPUT: { RECEIVED: 'data_output_received' },
|
||||||
|
NOTIFICATION: {
|
||||||
|
FROM_APP: 'notification_send_from_app',
|
||||||
|
SEND_ALL: 'notification_send_to_all',
|
||||||
|
},
|
||||||
|
APC_CONTROL: {
|
||||||
|
FROM_WEB: 'apc_control_request_from_web',
|
||||||
|
TO_APP: 'apc_control_request_to_app',
|
||||||
|
FROM_WEB_ALL_APC: 'all_apc_control_request_from_web',
|
||||||
|
},
|
||||||
|
SYSTEM_LOG: {
|
||||||
|
FROM_APP: 'system_log_send_from_app',
|
||||||
|
GET_SYSTEM_LOG_FROM_WEB: 'get_system_log_from_web',
|
||||||
|
REQUEST_LIST_SYSTEM_LOG_FROM_WEB: 'request_list_system_log_from_web',
|
||||||
|
RESPONSE_LIST_SYSTEM_LOG_FROM_APP: 'response_list_system_log_from_app',
|
||||||
|
RESPONSE_SYSTEM_LOG_FROM_APP: 'response_system_log_from_app',
|
||||||
|
RESPONSE_SYSTEM_LOG_TO_WEB: 'response_system_log_to_web',
|
||||||
|
},
|
||||||
|
CLI: {
|
||||||
|
OPEN_CLI_LINE_FROM_WEB: 'open_cli_line_from_web',
|
||||||
|
CLOSE_CLI_LINE_FROM_WEB: 'close_cli_line_from_web',
|
||||||
|
OPEN_CLI_MULTI_LINE_FROM_WEB: 'open_cli_multi_line_from_web',
|
||||||
|
CLOSE_CLI_MULTI_LINE_FROM_WEB: 'close_cli_multi_line_from_web',
|
||||||
|
WRITE_COMMAND_FROM_WEB: 'write_command_line_from_web',
|
||||||
|
WRITE_COMMAND_TO_APP: 'write_command_line_to_app',
|
||||||
|
RECEIVE_COMMAND_DATA_FROM_APP: 'receive_command_data_from_app',
|
||||||
|
RECEIVE_COMMAND_DATA_TO_WEB: 'receive_command_data_to_web',
|
||||||
|
},
|
||||||
|
RESCAN: {
|
||||||
|
SEND_LIST_RESCAN_FROM_WEB: 'send_list_rescan_from_web',
|
||||||
|
SEND_LIST_RESCAN_TO_APP: 'send_list_rescan_to_app',
|
||||||
|
},
|
||||||
|
LOCK: {
|
||||||
|
SEND_LIST_LOCK_FROM_WEB: 'send_list_lock_from_web',
|
||||||
|
SEND_LIST_LOCK_TO_APP: 'send_list_lock_to_app',
|
||||||
|
},
|
||||||
|
CHANGE_STAGE: {
|
||||||
|
SEND_STAGE_FROM_WEB: 'send_stage_from_web',
|
||||||
|
SEND_STAGE_TO_APP: 'send_stage_to_app',
|
||||||
|
},
|
||||||
|
UPDATE_PROPERTY: {
|
||||||
|
UPDATE_PROPERTY_FROM_WEB: 'update_property_from_web',
|
||||||
|
UPDATE_PROPERTY_TO_APP: 'update_property_to_app',
|
||||||
|
},
|
||||||
|
RUN_SCENARIOS: {
|
||||||
|
RUN_SCENARIOS_FROM_WEB: 'run_scenarios_from_web',
|
||||||
|
RUN_SCENARIOS_TO_APP: 'run_scenarios_to_app',
|
||||||
|
},
|
||||||
|
SEND_BREAK: {
|
||||||
|
SEND_BREAK_FROM_WEB: 'send_break_from_web',
|
||||||
|
SEND_BREAK_TO_APP: 'send_break_to_app',
|
||||||
|
},
|
||||||
|
JOIN_MULTI_ROOM: {
|
||||||
|
JOIN_MULTI_ROOM_FROM_WEB: 'join_multi_room_from_web',
|
||||||
|
},
|
||||||
|
LEAVE_MULTI_ROOM: {
|
||||||
|
LEAVE_MULTI_ROOM_FROM_WEB: 'leave_multi_room_from_web',
|
||||||
|
},
|
||||||
|
TAKE_OVER: {
|
||||||
|
TAKE_OVER_FROM_WEB: 'take_over_from_web',
|
||||||
|
TAKE_OVER_TO_WEB: 'take_over_to_web',
|
||||||
|
},
|
||||||
|
CONNECT_APC: {
|
||||||
|
CONNECT_APC_FROM_WEB: 'connect_apc_from_web',
|
||||||
|
CONNECT_APC_TO_APP: 'connect_apc_to_app',
|
||||||
|
},
|
||||||
|
DATA_APC_RECEIVED: {
|
||||||
|
DATA_APC_RECEIVED_FROM_APP: 'data_apc_received_from_app',
|
||||||
|
DATA_APC_RECEIVED_TO_WEB: 'data_apc_received_to_web',
|
||||||
|
},
|
||||||
|
SEND_COMMAND_TO_APC: {
|
||||||
|
SEND_COMMAND_TO_APC_FROM_WEB: 'send_command_to_apc_from_web',
|
||||||
|
SEND_COMMAND_TO_APC_TO_APP: 'send_command_to_apc_to_app',
|
||||||
|
},
|
||||||
|
SEND_CLEAR_LINE: {
|
||||||
|
SEND_CLEAR_LINE_FROM_WEB: 'send_clear_line_from_web',
|
||||||
|
SEND_CLEAR_LINE_TO_APP: 'send_clear_line_to_app',
|
||||||
|
},
|
||||||
|
SEND_CLOSE_LINE: {
|
||||||
|
SEND_CLOSE_LINE_FROM_WEB: 'send_close_line_from_web',
|
||||||
|
SEND_CLOSE_LINE_TO_APP: 'send_close_line_to_app',
|
||||||
|
},
|
||||||
|
SEND_OPEN_LINE: {
|
||||||
|
SEND_OPEN_LINE_FROM_WEB: 'send_open_line_from_web',
|
||||||
|
SEND_OPEN_LINE_TO_APP: 'send_open_line_to_app',
|
||||||
|
},
|
||||||
|
CONTROL_APP: {
|
||||||
|
SEND_PAUSE_APP_FROM_WEB: 'send_pause_app_from_web',
|
||||||
|
SEND_RESUME_APP_FROM_WEB: 'send_resume_app_from_web',
|
||||||
|
SEND_RESTART_APP_FROM_WEB: 'send_restart_app_from_web',
|
||||||
|
SEND_QUIT_APP_FROM_WEB: 'send_quit_app_from_web',
|
||||||
|
},
|
||||||
|
CONNECT_SWITCH: {
|
||||||
|
CONNECT_SWITCH_FROM_WEB: 'connect_switch_from_web',
|
||||||
|
CONNECT_SWITCH_TO_APP: 'connect_switch_to_app',
|
||||||
|
},
|
||||||
|
DATA_SWITCH_RECEIVED: {
|
||||||
|
DATA_SWITCH_RECEIVED_FROM_APP: 'data_switch_received_from_app',
|
||||||
|
DATA_SWITCH_RECEIVED_TO_WEB: 'data_switch_received_to_web',
|
||||||
|
},
|
||||||
|
SEND_COMMAND_TO_SWITCH: {
|
||||||
|
SEND_COMMAND_TO_SWITCH_FROM_WEB: 'send_command_to_switch_from_web',
|
||||||
|
SEND_COMMAND_TO_SWITCH_TO_APP: 'send_command_to_switch_to_app',
|
||||||
|
SEND_COMMAND_TO_SWITCH_FROM_APP: 'send_command_to_switch_from_app',
|
||||||
|
},
|
||||||
|
RELOAD_TICKET: {
|
||||||
|
RELOAD_TICKET_FROM_WEB: 'reload_ticket_from_web',
|
||||||
|
RELOAD_TICKET_TO_WEB: 'reload_ticket_to_web',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const LINE_STATUS = {
|
||||||
|
CHECK_INVENTORY: 'CHECK_INVENTORY',
|
||||||
|
STATUS_TEST: 'TESTING',
|
||||||
|
CONNECT_FAIL: 'CONNECT_FAIL',
|
||||||
|
CONNECTED: 'CONNECTED',
|
||||||
|
STATUS_READY: 'READY',
|
||||||
|
STATUS_DONE: 'DONE',
|
||||||
|
STATUS_CHECKING: 'CHECKING',
|
||||||
|
STATUS_LOCKED: 'LOCKED',
|
||||||
|
STATUS_CLOSED: 'CLOSED',
|
||||||
|
STATUS_TIMEOUT: 'TIMEOUT',
|
||||||
|
STATUS_PHYSICAL_TEST: 'STATUS_PHYSICAL_TEST',
|
||||||
|
STATUS_PHYSICAL_TEST_DONE: 'STATUS_PHYSICAL_TEST_DONE',
|
||||||
|
STATUS_UNDIFINED_INVEN: 'INVENTORY_UNIDENTIFIED',
|
||||||
|
STATUS_RUNNING_SCENARIOS: 'RUNNING_SCENARIOS',
|
||||||
|
APC_CONTROL: 'APC_CONTROL',
|
||||||
|
STATUS_STARTING: 'STARTING',
|
||||||
|
STATUS_TURN_OFF: 'TURN_OFF',
|
||||||
|
STATUS_RESTARTING: 'RESTARTING',
|
||||||
|
}
|
||||||
|
|
||||||
|
export const LIST_FAVORITE_COMMANDS = [
|
||||||
|
'sh inv',
|
||||||
|
'sh ver',
|
||||||
|
// 'sh diag',
|
||||||
|
// 'sh post',
|
||||||
|
// 'sh env',
|
||||||
|
// 'sh log',
|
||||||
|
// 'sh platform',
|
||||||
|
]
|
||||||
|
|
||||||
|
export const dataPermission = [
|
||||||
|
{
|
||||||
|
link: '/dashboard',
|
||||||
|
label: 'Dashboard',
|
||||||
|
icon: IconHome,
|
||||||
|
requiredPermissions: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
link: '/station-setting',
|
||||||
|
label: 'Station Setting',
|
||||||
|
icon: IconServer,
|
||||||
|
requiredPermissions: ['station_activity'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
link: '/monitor',
|
||||||
|
label: 'Monitoring',
|
||||||
|
icon: IconDeviceDesktop,
|
||||||
|
requiredPermissions: [
|
||||||
|
'monitor_power',
|
||||||
|
'monitor_cli',
|
||||||
|
'monitor_other_items',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
link: '/control-apc',
|
||||||
|
label: 'Control APC',
|
||||||
|
icon: IconSettingsAutomation,
|
||||||
|
requiredPermissions: ['control_apc_activity'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
link: '/group-model',
|
||||||
|
label: 'Group - Model',
|
||||||
|
icon: IconRouter,
|
||||||
|
requiredPermissions: ['group_model_activity'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
link: '/keyword',
|
||||||
|
label: 'Keyword',
|
||||||
|
icon: IconKey,
|
||||||
|
requiredPermissions: ['keyword_activity', 'keyword_limit'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
link: '/exclude-error',
|
||||||
|
label: 'Exclude Errors',
|
||||||
|
icon: IconBan,
|
||||||
|
requiredPermissions: ['exclude_error_activity', 'exclude_error_limit'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
link: '/list-logs',
|
||||||
|
label: 'List Logs',
|
||||||
|
icon: IconFile,
|
||||||
|
requiredPermissions: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
link: '/webhooks',
|
||||||
|
label: 'Webhooks',
|
||||||
|
icon: IconWebhook,
|
||||||
|
requiredPermissions: ['webhook_activity', 'webhook_add_limit'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
link: '/scenario',
|
||||||
|
label: 'Scenario',
|
||||||
|
icon: IconClipboardList,
|
||||||
|
requiredPermissions: ['scenario_activity', 'scenario_add_limit'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
link: '/upgrade',
|
||||||
|
label: 'Upgrade now!',
|
||||||
|
icon: IconCrown,
|
||||||
|
requiredPermissions: [],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
@ -0,0 +1,128 @@
|
||||||
|
export type TGeneralSettingValues = {
|
||||||
|
page_title: string;
|
||||||
|
email: string;
|
||||||
|
phone: string;
|
||||||
|
meta_title: string;
|
||||||
|
meta_description: string;
|
||||||
|
meta_keyword: string;
|
||||||
|
facebook: string;
|
||||||
|
twitter: string;
|
||||||
|
linkedin: string;
|
||||||
|
favicon: File | null | string;
|
||||||
|
logo: File | null | string;
|
||||||
|
address: string;
|
||||||
|
license: string;
|
||||||
|
};
|
||||||
|
export type TStation = {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
ip: string;
|
||||||
|
port: number;
|
||||||
|
netmask?: string; // Optional
|
||||||
|
network?: string; // Optional
|
||||||
|
gateway?: string; // Optional
|
||||||
|
tftp_ip?: string; // Optional
|
||||||
|
apc_1_ip?: string; // Optional
|
||||||
|
apc_1_port?: number; // Optional
|
||||||
|
apc_2_ip?: string; // Optional
|
||||||
|
apc_2_port?: number; // Optional
|
||||||
|
apc_1_username?: string; // Optional
|
||||||
|
apc_1_password?: string; // Optional
|
||||||
|
apc_2_username?: string; // Optional
|
||||||
|
apc_2_password?: string; // Optional
|
||||||
|
inventory?: {
|
||||||
|
pid: string;
|
||||||
|
vid: string;
|
||||||
|
sn: string;
|
||||||
|
};
|
||||||
|
lines: TLine[];
|
||||||
|
is_active: boolean;
|
||||||
|
data?: string | any;
|
||||||
|
type?: string;
|
||||||
|
status?: boolean;
|
||||||
|
created_at?: string; // or use Date if you're working with Date objects
|
||||||
|
updated_at?: string; // or use Date if you're working with Date objects
|
||||||
|
owner_id: number;
|
||||||
|
owner: TUser;
|
||||||
|
users: TUser[];
|
||||||
|
apcs?: APCProps[];
|
||||||
|
master_control?: boolean;
|
||||||
|
switch_control_ip?: string; // Optional
|
||||||
|
switch_control_port?: number; // Optional
|
||||||
|
switch_control_username?: string; // Optional
|
||||||
|
switch_control_password?: string; // Optional
|
||||||
|
switches?: SwitchesProps[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TLine = {
|
||||||
|
id?: number;
|
||||||
|
port: number;
|
||||||
|
lineNumber: number;
|
||||||
|
lineClear: number;
|
||||||
|
station_id: number;
|
||||||
|
is_active: string | boolean;
|
||||||
|
data?: string | any;
|
||||||
|
type?: string;
|
||||||
|
inventory?: any;
|
||||||
|
status?: string;
|
||||||
|
netOutput?: string;
|
||||||
|
outlet?: number;
|
||||||
|
cliOpened?: boolean;
|
||||||
|
systemLogUrl?: string;
|
||||||
|
start_round_at: number;
|
||||||
|
apc_name: string;
|
||||||
|
created_at?: string; // or use Date if you're working with Date objects
|
||||||
|
updated_at?: string; // or use Date if you're working with Date objects
|
||||||
|
notes?: string;
|
||||||
|
noteScenario?: string;
|
||||||
|
physicalLog?: string;
|
||||||
|
checkPower?: boolean;
|
||||||
|
checkLed?: boolean;
|
||||||
|
userOpenCLI?: string;
|
||||||
|
userEmailOpenCLI?: string;
|
||||||
|
statusTicket?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TUser = {
|
||||||
|
id: number;
|
||||||
|
email: string;
|
||||||
|
email_cc: string;
|
||||||
|
full_name: string;
|
||||||
|
package_id: string;
|
||||||
|
zulip: string;
|
||||||
|
token?: string;
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type APCProps = {
|
||||||
|
apc_number?: number;
|
||||||
|
apc_ip?: string;
|
||||||
|
apc_port?: number;
|
||||||
|
apc_username?: string;
|
||||||
|
apc_password?: string;
|
||||||
|
outlets?: OutletProps[];
|
||||||
|
status: string;
|
||||||
|
output?: string;
|
||||||
|
keep_connect?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type OutletProps = {
|
||||||
|
name: string;
|
||||||
|
status: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SwitchesProps = {
|
||||||
|
host?: string;
|
||||||
|
port?: number;
|
||||||
|
username?: string;
|
||||||
|
password?: string;
|
||||||
|
status: string;
|
||||||
|
ports?: SwitchPortsProps[];
|
||||||
|
portGroups?: SwitchPortsProps[][];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SwitchPortsProps = {
|
||||||
|
name: string;
|
||||||
|
status: string;
|
||||||
|
poe: string;
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||||
|
"target": "ES2022",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"types": ["vite/client"],
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"verbatimModuleSyntax": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"erasableSyntaxOnly": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUncheckedSideEffectImports": true
|
||||||
|
},
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"files": [],
|
||||||
|
"references": [
|
||||||
|
{ "path": "./tsconfig.app.json" },
|
||||||
|
{ "path": "./tsconfig.node.json" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||||
|
"target": "ES2023",
|
||||||
|
"lib": ["ES2023"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"types": ["node"],
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"verbatimModuleSyntax": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"noEmit": true,
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"erasableSyntaxOnly": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUncheckedSideEffectImports": true
|
||||||
|
},
|
||||||
|
"include": ["vite.config.ts"]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import react from '@vitejs/plugin-react'
|
||||||
|
|
||||||
|
// https://vite.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
})
|
||||||
Loading…
Reference in New Issue