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