diff --git a/LICENSE b/LICENSE index 8942413..4e735f8 100644 --- a/LICENSE +++ b/LICENSE @@ -1,2 +1,2 @@ -DTP Time Tracker Copyright (C) 2024 DTP Technologies, LLC -All Rights Reserved +DTP Base Copyright (C) 2024 DTP Technologies, LLC +All Rights Reserved \ No newline at end of file diff --git a/README.md b/README.md index 5d9870e..849dba2 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -# DTP Chat +# DTP Base -A no-nonsense/no-frills communications platform. +A production-ready Node.js application development framework for use in-house at DTP Technologies, LLC. ## System Requirements @@ -82,4 +82,4 @@ Edit `.bashrc` and set `NODE_ENV=production` ## Emoji Picker -Chat currently uses [emoji-picker-element](https://www.npmjs.com/package/emoji-picker-element) as the renderer of an emoji picker, and then it manages the presentation of the picker itself. \ No newline at end of file +Base currently uses [emoji-picker-element](https://www.npmjs.com/package/emoji-picker-element) as the renderer of an emoji picker, and then it manages the presentation of the picker itself. \ No newline at end of file diff --git a/app/controllers/client.js b/app/controllers/client.js deleted file mode 100644 index a24b33b..0000000 --- a/app/controllers/client.js +++ /dev/null @@ -1,216 +0,0 @@ -// client.js -// Copyright (C) 2024 DTP Technologies, LLC -// All Rights Reserved - -'use strict'; - -import express from 'express'; - -import { SiteController, SiteError } from '../../lib/site-lib.js'; -import { populateClientId, populateProjectId } from './lib/populators.js'; - -export default class ClientController extends SiteController { - - static get name ( ) { return 'ClientController'; } - static get slug ( ) { return 'client'; } - - constructor (dtp) { - super(dtp, ClientController.slug); - } - - async start ( ) { - const { dtp } = this; - const { - limiter: limiterService, - session: sessionService, - } = dtp.services; - - const limiterConfig = limiterService.config.client; - const authCheck = sessionService.authCheckMiddleware({ requireLogin: true }); - - function checkClientOwnership (req, res, next) { - if (!res.locals.client.user._id.equals(req.user._id)) { - throw new SiteError(401, 'This is not your client'); - } - return next(); - } - - function checkProjectOwnership (req, res, next) { - if (!res.locals.project.user._id.equals(req.user._id)) { - throw new SiteError(401, 'This is not your client'); - } - return next(); - } - - const router = express.Router(); - dtp.app.use('/client', authCheck, router); - - router.param('clientId', populateClientId(this)); - router.param('projectId', populateProjectId(this)); - - router.post( - '/:clientId/project/:projectId', - limiterService.create(limiterConfig.postProjectUpdate), - checkClientOwnership, - this.postProjectUpdate.bind(this), - ); - - router.post( - '/:clientId/project', - limiterService.create(limiterConfig.postProjectCreate), - checkClientOwnership, - this.postProjectCreate.bind(this), - ); - - router.post( - '/:clientId', - limiterService.create(limiterConfig.postClientUpdate), - checkClientOwnership, - this.postClientUpdate.bind(this), - ); - - router.post( - '/', - limiterService.create(limiterConfig.postClientCreate), - this.postClientCreate.bind(this), - ); - - router.get( - '/:clientId/project/create', - limiterService.create(limiterConfig.getProjectCreate), - checkClientOwnership, - this.getProjectCreate.bind(this), - ); - - router.get( - '/:clientId/project/:projectId/edit', - limiterService.create(limiterConfig.getProjectEditor), - checkProjectOwnership, - this.getProjectEditor.bind(this), - ); - - router.get( - '/:clientId/project/:projectId', - limiterService.create(limiterConfig.getProjectView), - checkProjectOwnership, - this.getProjectView.bind(this), - ); - - router.get( - '/create', - limiterService.create(limiterConfig.getClientEditor), - this.getClientEditor.bind(this), - ); - - router.get( - '/:clientId/edit', - limiterService.create(limiterConfig.getClientEditor), - checkClientOwnership, - this.getClientEditor.bind(this), - ); - - router.get( - '/:clientId', - limiterService.create(limiterConfig.getClientView), - checkClientOwnership, - this.getClientView.bind(this), - ); - - router.get( - '/', - limiterService.create(limiterConfig.getHome), - this.getHome.bind(this), - ); - - return router; - } - - async postProjectUpdate (req, res, next) { - const { client: clientService } = this.dtp.services; - try { - await clientService.updateProject(res.locals.project, req.body); - res.redirect(`/client/${res.locals.client._id}/project/${res.locals.project._id}`); - } catch (error) { - this.log.error('failed to update client project', { error }); - return next(error); - } - } - - async postProjectCreate (req, res, next) { - const { client: clientService } = this.dtp.services; - try { - res.locals.project = await clientService.createProject(res.locals.client, req.body); - res.redirect(`/client/${res.locals.client._id}/project/${res.locals.project._id}`); - } catch (error) { - this.log.error('failed to create client project', { error }); - return next(error); - } - } - - async postClientUpdate (req, res, next) { - const { client: clientService } = this.dtp.services; - try { - await clientService.updateClient(res.locals.client, req.body); - res.redirect(`/client/${res.locals.client._id}`); - } catch (error) { - this.log.error('failed to create client', { error }); - return next(error); - } - } - - async postClientCreate (req, res, next) { - const { client: clientService } = this.dtp.services; - try { - res.locals.client = await clientService.createClient(req.user, req.body); - res.redirect(`/client/${res.locals.client._id}`); - } catch (error) { - this.log.error('failed to create client', { error }); - return next(error); - } - } - - async getProjectCreate (req, res) { - res.render('client/project/editor'); - } - - async getProjectEditor (req, res) { - res.render('client/project/editor'); - } - - async getProjectView (req, res, next) { - const { task: taskService } = this.dtp.services; - try { - res.locals.taskGrid = await taskService.getTaskGridForProject(res.locals.project); - res.render('client/project/view'); - } catch (error) { - this.log.error('failed to present project view', { error }); - return next(error); - } - } - - async getClientEditor (req, res) { - res.render('client/editor'); - } - - async getClientView (req, res, next) { - const { client: clientService } = this.dtp.services; - try { - res.locals.projects = await clientService.getProjectsForClient(res.locals.client); - res.render('client/view'); - } catch (error) { - this.log.error('failed to present client home view', { error }); - return next(error); - } - } - - async getHome (req, res, next) { - const { client: clientService } = this.dtp.services; - try { - res.locals.clients = await clientService.getClientsForUser(req.user); - res.render('client/dashboard'); - } catch (error) { - this.log.error('failed to present client home view', { error }); - return next(error); - } - } -} \ No newline at end of file diff --git a/app/controllers/home.js b/app/controllers/home.js index 736900a..a1e4bd1 100644 --- a/app/controllers/home.js +++ b/app/controllers/home.js @@ -28,23 +28,13 @@ export default class HomeController extends SiteController { } async getHome (req, res, next) { - const { client: clientService, report: reportService, task: taskService } = this.dtp.services; try { if (!req.user) { return res.redirect('/welcome'); } res.locals.currentView = 'home'; - res.locals.pageDescription = 'DTP Time Tracker'; - - res.locals.projects = await clientService.getProjectsForUser(req.user); - res.locals.taskGrid = await taskService.getTaskGridForUser(req.user); - res.locals.weeklyEarnings = await reportService.getWeeklyEarnings(req.user); - - res.locals.managedProjects = await clientService.getProjectsForManager(req.user); - for (const project of res.locals.managedProjects) { - project.taskGrid = await taskService.getTaskGridForProject(project); - } + res.locals.pageDescription = 'DTP Base'; res.render('home'); } catch (error) { diff --git a/app/controllers/lib/populators.js b/app/controllers/lib/populators.js index 58d95ca..84eb21d 100644 --- a/app/controllers/lib/populators.js +++ b/app/controllers/lib/populators.js @@ -2,71 +2,22 @@ import { SiteError } from '../../../lib/site-lib.js'; -export function populateClientId (controller) { - return async function (req, res, next, clientId) { - const { client: clientService } = controller.dtp.services; +/* + * This is a sample populator. It doesn't run. There is no sample service, etc. + * This is simply the pattern you follow to declare a new ExpressJS parameter + * populator and export it from your populators library. + */ +export function populateSampleParameter (controller) { + return async function (req, res, next, sampleParameter) { + const { sample: sampleService } = controller.dtp.services; try { - res.locals.client = await clientService.getClientById(clientId); - if (!res.locals.client) { - throw new SiteError(404, 'Client not found'); - } - if (!res.locals.client.user._id.equals(req.user._id)) { - throw new SiteError(401, 'This is not your client'); - } - return next(); - } catch (error) { - controller.log.error('failed to populate client', { error }); - return next(error); - } - }; -} - -export function populateProjectId (controller) { - return async function (req, res, next, projectId) { - const { client: clientService } = controller.dtp.services; - try { - res.locals.project = await clientService.getProjectById(projectId); - if (!res.locals.project) { - throw new SiteError(404, 'Project not found'); - } - if (!res.locals.project.user._id.equals(req.user._id)) { - throw new SiteError(401, 'This is not your project'); - } - return next(); - } catch (error) { - controller.log.error('failed to populate project', { error }); - return next(error); - } - }; -} - -export function populateTaskId (controller) { - return async function (req, res, next, taskId) { - const { task: taskService } = controller.dtp.services; - try { - res.locals.task = await taskService.getTaskById(taskId); - if (!res.locals.task) { - throw new SiteError(404, 'Task not found'); - } - return next(); - } catch (error) { - controller.log.error('failed to populate taskId', { taskId, error }); - return next(error); - } - }; -} - -export function populateSessionId (controller) { - return async function (req, res, next, sessionId) { - const { task: taskService } = controller.dtp.services; - try { - res.locals.session = await taskService.getTaskSessionById(sessionId); - if (!res.locals.session) { - throw new SiteError(404, 'Task session not found'); + res.locals.sample = await sampleService.getSample(sampleParameter); + if (!res.locals.sample) { + throw new SiteError(404, 'Sample not found'); } return next(); } catch (error) { - controller.log.error('failed to populate sessionId', { sessionId, error }); + controller.log.error('failed to populate sampleParameter', { sampleParamater, error }); return next(error); } }; diff --git a/app/controllers/manager.js b/app/controllers/manager.js deleted file mode 100644 index a9bf7c6..0000000 --- a/app/controllers/manager.js +++ /dev/null @@ -1,43 +0,0 @@ -// manager.js -// Copyright (C) 2024 DTP Technologies, LLC -// All Rights Reserved - -'use strict'; - -import express from 'express'; - -import { SiteController } from '../../lib/site-lib.js'; -import { populateClientId, populateProjectId } from './lib/populators.js'; - -export default class ManagerController extends SiteController { - - static get name ( ) { return 'ManagerController'; } - static get slug ( ) { return 'manager'; } - - constructor (dtp) { - super(dtp, ManagerController.slug); - } - - async start ( ) { - const { dtp } = this; - const { - // limiter: limiterService, - session: sessionService, - } = dtp.services; - - // const limiterConfig = limiterService.config.manager; - const authCheck = sessionService.authCheckMiddleware({ requireLogin: true }); - - const router = express.Router(); - dtp.app.use('/manager', authCheck, router); - - router.param('clientId', populateClientId(this)); - router.param('projectId', populateProjectId(this)); - - router.get('/', this.getDashboard.bind(this)); - } - - async getDashboard (req, res) { - res.render('manager/dashboard'); - } -} \ No newline at end of file diff --git a/app/controllers/report.js b/app/controllers/report.js deleted file mode 100644 index 77a31f8..0000000 --- a/app/controllers/report.js +++ /dev/null @@ -1,52 +0,0 @@ -// report.js -// Copyright (C) 2024 DTP Technologies, LLC -// All Rights Reserved - -'use strict'; - -import express from 'express'; - -import { SiteController } from '../../lib/site-lib.js'; - -export default class ReportController extends SiteController { - - static get name ( ) { return 'ReportController'; } - static get slug ( ) { return 'report'; } - - constructor (dtp) { - super(dtp, ReportController.slug); - } - - async start ( ) { - const { - // limiter: limiterService, - session: sessionService, - } = this.dtp.services; - - // const limiterConfig = limiterService.config.report; - - const authCheck = sessionService.authCheckMiddleware({ requireLogin: true }); - - const router = express.Router(); - this.dtp.app.use('/report', authCheck, router); - - router.get('/', this.getDashboard.bind(this)); - } - - async getDashboard (req, res, next) { - const { report: reportService } = this.dtp.services; - try { - res.locals.pageTitle = 'Weekly Summary Report'; - res.locals.pageDescription = 'A breakdown of project and contractor performance for the current week.'; - - res.locals.weekStartDate = reportService.startOfWeek(); - res.locals.weeklyEarnings = await reportService.getWeeklyEarnings(req.user); - res.locals.dailyTimeWorked = await reportService.getDailyHoursWorkedForUser(req.user); - - res.render('report/dashboard'); - } catch (error) { - this.log.error('failed to present report dashboard', { error }); - return next(error); - } - } -} \ No newline at end of file diff --git a/app/controllers/task.js b/app/controllers/task.js deleted file mode 100644 index 51dcb6a..0000000 --- a/app/controllers/task.js +++ /dev/null @@ -1,257 +0,0 @@ -// task.js -// Copyright (C) 2024 DTP Technologies, LLC -// All Rights Reserved - -'use strict'; - -import express from 'express'; - -import { SiteController, SiteError } from '../../lib/site-lib.js'; -import { populateTaskId, populateSessionId } from './lib/populators.js'; - -export default class TaskController extends SiteController { - - static get name ( ) { return 'TaskController'; } - static get slug ( ) { return 'task'; } - - constructor (dtp) { - super(dtp, TaskController); - } - - async start ( ) { - const { dtp } = this; - - const { - limiter: limiterService, - session: sessionService, - } = dtp.services; - const limiterConfig = limiterService.config.task; - - const multer = this.createMulter(TaskController.slug, { - limits: { - fileSize: 1024 * 1000 * 5, - }, - }); - - const authCheck = sessionService.authCheckMiddleware({ requireLogin: true }); - - const router = express.Router(); - dtp.app.use('/task', authCheck, router); - - router.use(async (req, res, next) => { - res.locals.currentView = TaskController.name; - return next(); - }); - - async function checkTaskOwnership (req, res, next) { - if (Array.isArray(res.locals.task.project.managers) && (res.locals.task.project.managers.length > 0)) { - res.locals.manager = res.locals.task.project.managers.find((manager) => manager._id.equals(req.user._id)); - } - if (!res.locals.manager && !res.locals.task.user._id.equals(req.user._id)) { - return next(new SiteError(401, 'This is not your task')); - } - return next(); - } - - async function checkSessionOwnership (req, res, next) { - if (Array.isArray(res.locals.task.project.managers) && (res.locals.task.project.managers.length > 0)) { - res.locals.manager = res.locals.task.project.managers.find((manager) => manager._id.equals(req.user._id)); - } - if (!res.locals.manager && !res.locals.session.user._id.equals(req.user._id)) { - throw new SiteError(401, 'This is not your session'); - } - return next(); - } - - router.param('taskId', populateTaskId(this)); - router.param('sessionId', populateSessionId(this)); - - router.post( - '/:taskId/session/start', - limiterService.create(limiterConfig.postStartTaskSession), - this.postStartTaskSession.bind(this), - ); - - router.post( - '/:taskId/session/:sessionId/screenshot', - checkSessionOwnership, - limiterService.create(limiterConfig.postTaskSessionScreenshot), - multer.single('image'), - this.postTaskSessionScreenshot.bind(this), - ); - - router.post( - '/:taskId/session/:sessionId/status', - limiterService.create(limiterConfig.postTaskSessionStatus), - checkSessionOwnership, - this.postTaskSessionStatus.bind(this), - ); - - router.post( - '/:taskId/session/:sessionId/close', - limiterService.create(limiterConfig.postCloseTaskSession), - checkSessionOwnership, - this.postCloseTaskSession.bind(this), - ); - - router.post( - '/:taskId/start', - limiterService.create(limiterConfig.postStartTask), - checkTaskOwnership, - this.postStartTask.bind(this), - ); - router.post( - '/:taskId/close', - limiterService.create(limiterConfig.postCloseTask), - checkTaskOwnership, - this.postCloseTask.bind(this), - ); - - router.post( - '/', - limiterService.create(limiterConfig.postCreateTask), - this.postCreateTask.bind(this), - ); - - router.get( - '/:taskId/session/:sessionId', - limiterService.create(limiterConfig.getTaskSessionView), - checkSessionOwnership, - this.getTaskSessionView.bind(this), - ); - - router.get( - '/:taskId', - limiterService.create(limiterConfig.getTaskView), - checkTaskOwnership, - this.getTaskView.bind(this), - ); - } - - async postStartTaskSession (req, res) { - const { task: taskService } = this.dtp.services; - try { - res.locals.session = await taskService.createTaskSession(res.locals.task); - res.status(200).json({ - success: true, - session: res.locals.session, - }); - } catch (error) { - this.log.error('failed to create task session', { error }); - res.status(error.statusCode || 500).json({ - success: false, - message: error.message, - }); - } - } - - async postTaskSessionScreenshot (req, res) { - const { task: taskService } = this.dtp.services; - try { - await taskService.addTaskSessionScreenshot(res.locals.session, req.file); - res.status(200).json({ success: true }); - } catch (error) { - this.log.error('failed to add task session screenshot', { error }); - res.status(error.statusCode || 500).json({ - success: false, - message: error.message, - }); - } - } - - async postTaskSessionStatus (req, res) { - const { task: taskService } = this.dtp.services; - try { - this.log.debug('updating task session status', { - sessionId: res.locals.session._id, - params: req.body, - }); - await taskService.setTaskSessionStatus(res.locals.session, req.body.status); - - const displayList = this.createDisplayList('set-session-status'); - displayList.showNotification( - 'Session status updated', - 'success', - 'bottom-center', - 5000, - ); - - res.status(200).json({ success: true, displayList }); - } catch (error) { - this.log.error('failed to update task session status', { error }); - res.status(error.statusCode || 500).json({ - success: false, - message: error.message, - }); - } - } - - async postCloseTaskSession (req, res) { - const { task: taskService } = this.dtp.services; - try { - await taskService.closeTaskSession(res.locals.session); - res.status(200).json({ success: true }); - } catch (error) { - this.log.error('failed to close task session', { error }); - res.status(error.statusCode || 500).json({ - success: false, - message: error.message, - }); - } - } - - async postStartTask (req, res, next) { - const { task: taskService } = this.dtp.services; - try { - await taskService.startTask(res.locals.task); - res.redirect(`/task/${res.locals.task._id}`); - } catch (error) { - this.log.error('failed to close task session', { error }); - return next(error); - } - } - - async postCloseTask (req, res, next) { - const { task: taskService } = this.dtp.services; - try { - await taskService.closeTask(res.locals.task); - res.redirect('/'); - } catch (error) { - this.log.error('failed to close task', { error }); - return next(error); - } - } - - async postCreateTask (req, res, next) { - const { task: taskService } = this.dtp.services; - try { - res.locals.task = await taskService.createTask(req.user, req.body); - res.redirect(`/task/${res.locals.task._id}`); - } catch (error) { - this.log.error('failed to create new task', { error }); - return next(error); - } - } - - async getTaskSessionView (req, res) { - res.render('task/session/view'); - } - - async getTaskView (req, res, next) { - const { report: reportService, task: taskService } = this.dtp.services; - try { - res.locals.pagination = this.getPaginationParameters(req, 50); - res.locals.sessions = await taskService.getSessionsForTask( - res.locals.task, - res.locals.pagination, - ); - - res.locals.weekStartDate = reportService.startOfWeek(); - - res.render('task/view'); - } catch (error) { - this.log.error('failed to present the Task view', { error }); - return next(error); - } - } -} \ No newline at end of file diff --git a/app/models/client-project.js b/app/models/client-project.js deleted file mode 100644 index 7d29733..0000000 --- a/app/models/client-project.js +++ /dev/null @@ -1,20 +0,0 @@ -// project.js -// Copyright (C) 2024 DTP Technologies, LLC -// All Rights Reserved - -'use strict'; - -import mongoose from 'mongoose'; -const Schema = mongoose.Schema; - -const ClientProjectSchema = new Schema({ - created: { type: Date, default: Date.now, required: true, index: 1 }, - user: { type: Schema.ObjectId, required: true, index: 1, ref: 'User' }, - client: { type: Schema.ObjectId, required: true, index: 1, ref: 'Client' }, - managers: { type: [Schema.ObjectId], ref: 'User' }, - name: { type: String, required: true }, - description: { type: String }, - hourlyRate: { type: Number, required: true }, -}); - -export default mongoose.model('ClientProject', ClientProjectSchema); \ No newline at end of file diff --git a/app/models/client.js b/app/models/client.js deleted file mode 100644 index eb6e868..0000000 --- a/app/models/client.js +++ /dev/null @@ -1,23 +0,0 @@ -// client.js -// Copyright (C) 2024 DTP Technologies, LLC -// All Rights Reserved - -'use strict'; - -import mongoose from 'mongoose'; -const Schema = mongoose.Schema; - -const MAX_HOURS_PER_WEEK = 7 * 24; - -const ClientSchema = new Schema({ - user: { type: Schema.ObjectId, required: true, index: 1, ref: 'User' }, - name: { type: String, required: true }, - description: { type: String }, - hoursLimit: { type: Number, default: 0, max: MAX_HOURS_PER_WEEK, required: true }, - weeklyTotals: { - timeWorked: { type: Number, default: 0, required: true }, - billable: { type: Number, default: 0, required: true }, - }, -}); - -export default mongoose.model('Client', ClientSchema); \ No newline at end of file diff --git a/app/models/lib/constants.js b/app/models/lib/constants.js index 8f19e0d..bbe28ef 100644 --- a/app/models/lib/constants.js +++ b/app/models/lib/constants.js @@ -1,4 +1,5 @@ 'use strict'; -export const MIN_ROOM_CAPACITY = 4; -export const MAX_ROOM_CAPACITY = 25; \ No newline at end of file +/* + * Declare model constants here and import them into whatever needs them. + */ \ No newline at end of file diff --git a/app/models/lib/media.js b/app/models/lib/media.js index 29d004b..25a9657 100644 --- a/app/models/lib/media.js +++ b/app/models/lib/media.js @@ -7,15 +7,6 @@ import mongoose from 'mongoose'; const Schema = mongoose.Schema; -const EPISODE_STATUS_LIST = [ - 'starting', // the stream is connecting - 'live', // the stream is live - 'ending', // the stream has ended, queued for processing - 'processing', // the stream is being processed for DVR - 'replay', // the stream is available on the DVR - 'expired', // the stream is expired (removed) -]; - const VIDEO_STATUS_LIST = [ 'new', // the video (original) is on storage / queued 'processing', // the video is being processed for distribution @@ -23,15 +14,7 @@ const VIDEO_STATUS_LIST = [ 'removed', // the video has been removed ]; -const ROOM_STATUS_LIST = [ - 'starting', // the room is starting a call - 'live', // the room has a live call - 'shutdown', // the room is closing it's live call - 'expired', // the room has failed to check in - 'crashed', // the room's worker or server has crashed -]; - -const MediaMetadataSchema = new Schema({ +const VideoMetadataSchema = new Schema({ type: { type: String }, size: { type: Number }, bitRate: { type: Number }, @@ -58,9 +41,7 @@ const AudioMetadataSchema = new Schema({ }); export { - EPISODE_STATUS_LIST, VIDEO_STATUS_LIST, - ROOM_STATUS_LIST, - MediaMetadataSchema, + VideoMetadataSchema, AudioMetadataSchema, }; \ No newline at end of file diff --git a/app/models/task-session.js b/app/models/task-session.js deleted file mode 100644 index 02bb3e2..0000000 --- a/app/models/task-session.js +++ /dev/null @@ -1,37 +0,0 @@ -// task-session.js -// Copyright (C) 2024 DTP Technologies, LLC -// All Rights Reserved - -'use strict'; - -import mongoose from 'mongoose'; -const Schema = mongoose.Schema; - -const STATUS_LIST = [ - 'active', - 'reconnecting', - 'finished', - 'expired', -]; - -const ScreenshotSchema = new Schema({ - created: { type: Date, required: true, default: Date.now, index: 1 }, - image: { type: Schema.ObjectId, required: true, ref: 'Image' }, -}); - -const TaskSessionSchema = new Schema({ - created: { type: Date, required: true, default: Date.now, index: 1 }, - lastUpdated: { type: Date, required: true, default: Date.now, index: 1 }, - finished: { type: Date, index: 1 }, - duration: { type: Number, default: 0, required: true }, - hourlyRate: { type: Number, required: true }, - user: { type: Schema.ObjectId, required: true, index: 1, ref: 'User' }, - client: { type: Schema.ObjectId, required: true, index: 1, ref: 'Client' }, - project: { type: Schema.ObjectId, required: true, index: 1, ref: 'ClientProject' }, - task: { type: Schema.ObjectId, required: true, index: 1, ref: 'Task' }, - status: { type: String, enum: STATUS_LIST, default: 'active', required: true }, - notes: { type: [String], default: [ ] }, - screenshots: { type: [ScreenshotSchema] }, -}); - -export default mongoose.model('TaskSession', TaskSessionSchema); \ No newline at end of file diff --git a/app/models/task.js b/app/models/task.js deleted file mode 100644 index df23a88..0000000 --- a/app/models/task.js +++ /dev/null @@ -1,23 +0,0 @@ -// task.js -// Copyright (C) 2024 DTP Technologies, LLC -// All Rights Reserved - -'use strict'; - -import mongoose from 'mongoose'; -const Schema = mongoose.Schema; - -const STATUS_LIST = ['pending', 'active', 'finished']; - -const TaskSchema = new Schema({ - created: { type: Date, required: true, default: Date.now, index: 1 }, - finished: { type: Date, index: 1 }, - duration: { type: Number, default: 0, required: true }, - user: { type: Schema.ObjectId, required: true, index: 1, ref: 'User' }, - client: { type: Schema.ObjectId, required: true, index: 1, ref: 'Client' }, - project: { type: Schema.ObjectId, required: true, index: 1, ref: 'ClientProject' }, - status: { type: String, enum: STATUS_LIST, default: 'pending', required: true }, - note: { type: String, required: true }, -}); - -export default mongoose.model('Task', TaskSchema); \ No newline at end of file diff --git a/app/models/user.js b/app/models/user.js index 2571460..03cdd8c 100644 --- a/app/models/user.js +++ b/app/models/user.js @@ -45,7 +45,7 @@ const UserSchema = new Schema({ }, bio: { type: String }, ui: { - theme: { type: String, default: 'tracker-light', required: true }, + theme: { type: String, default: 'dtp-light', required: true }, }, flags: { type: UserFlagsSchema, default: { }, required: true, select: false }, permissions: { type: UserPermissionsSchema, default: { }, required: true, select: false }, diff --git a/app/models/video.js b/app/models/video.js index cf3f2d0..61b5bb1 100644 --- a/app/models/video.js +++ b/app/models/video.js @@ -9,7 +9,7 @@ const Schema = mongoose.Schema; import { VIDEO_STATUS_LIST, - MediaMetadataSchema, + VideoMetadataSchema, } from './lib/media.js'; const VideoSchema = new Schema({ @@ -24,7 +24,7 @@ const VideoSchema = new Schema({ media: { bucket: { type: String, required: true }, key: { type: String, required: true }, - metadata: { type: MediaMetadataSchema }, + metadata: { type: VideoMetadataSchema }, }, }); diff --git a/app/services/client.js b/app/services/client.js deleted file mode 100644 index 05cd485..0000000 --- a/app/services/client.js +++ /dev/null @@ -1,293 +0,0 @@ -// client.js -// Copyright (C) 2024 DTP Technologies, LLC -// All Rights Reserved - -'use strict'; - -import mongoose from 'mongoose'; -const Client = mongoose.model('Client'); -const ClientProject = mongoose.model('ClientProject'); - -import { SiteService, SiteError } from '../../lib/site-lib.js'; - -export default class ClientService extends SiteService { - - static get name ( ) { return 'ClientService'; } - static get slug () { return 'client'; } - - constructor (dtp) { - super(dtp, ClientService); - } - - async start ( ) { - const { user: userService } = this.dtp.services; - - this.populateClient = [ - { - path: 'user', - select: userService.USER_SELECT, - }, - ]; - - this.populateClientProject = [ - { - path: 'user', - select: userService.USER_SELECT, - }, - { - path: 'client', - populate: this.populateClient, - }, - { - path: 'managers', - select: userService.USER_SELECT, - }, - ]; - } - - async createClient (user, clientDefinition) { - const { text: textService } = this.dtp.services; - - const client = new Client(); - client.user = user._id; - client.name = textService.filter(clientDefinition.name); - if (clientDefinition.description && clientDefinition.description.length > 0) { - client.description = textService.filter(clientDefinition.description); - } - client.hoursLimit = clientDefinition.hoursLimit; - - await client.save(); - - return client.toObject(); - } - - async updateClient (client, clientDefinition) { - const { text: textService } = this.dtp.services; - const update = { $set: { }, $unset: { } }; - - let changed = false; - - if (clientDefinition.name !== client.name) { - update.$set.name = textService.filter(clientDefinition.name); - changed = true; - } - - if (clientDefinition.description && (clientDefinition.description.length > 0)) { - if (clientDefinition.description !== client.description) { - update.$set.description = textService.filter(clientDefinition.description); - changed = true; - } - } else { - update.$unset.description = 1; - changed = true; - } - - if (clientDefinition.hoursLimit !== client.hoursLimit) { - update.$set.hoursLimit = clientDefinition.hoursLimit; - changed = true; - } - - if (!changed) { - return; - } - - await Client.updateOne({ _id: client._id }, update); - } - - async getClientById (clientId) { - const client = Client - .findOne({ _id: clientId }) - .populate(this.populateClient) - .lean(); - return client; - } - - async getClientsForUser (user) { - const clients = Client - .find({ user: user._id }) - .sort({ name: 1 }) - .populate(this.populateClient) - .lean(); - return clients; - } - - async createProject (client, projectDefinition) { - const { text: textService } = this.dtp.services; - - const project = new ClientProject(); - project.user = client.user._id; - project.client = client._id; - - project.name = textService.filter(projectDefinition.name); - if (projectDefinition.description && (projectDefinition.description.length > 0)) { - project.description = textService.filter(projectDefinition.description); - } - - if (projectDefinition.managers && (projectDefinition.managers.length > 0)) { - project.managers = await this.parseManagerNames(projectDefinition.managers); - } - - project.hourlyRate = projectDefinition.hourlyRate; - - await project.save(); - - return project.toObject(); - } - - async updateProject (project, projectDefinition) { - const { text: textService } = this.dtp.services; - const update = { $set: { }, $unset: { } }; - let changed = false; - - if (projectDefinition.name !== project.name) { - update.$set.name = textService.filter(projectDefinition.name); - changed = true; - } - - if (projectDefinition.description !== project.description) { - update.$set.description = textService.filter(projectDefinition.description); - changed = true; - } - - if (projectDefinition.managers && (projectDefinition.managers.length > 0)) { - update.$set.managers = await this.parseManagerNames(projectDefinition.managers); - changed = true; - } else { - update.$unset.managers = 1; - } - - if (projectDefinition.hourlyRate !== project.hourlyRate) { - update.$set.hourlyRate = projectDefinition.hourlyRate; - changed = true; - } - - if (!changed) { - return; - } - - await ClientProject.updateOne({ _id: project._id }, update); - } - - async parseManagerNames (managerNames) { - const { user: userService } = this.dtp.services; - const managers = [ ]; - - managerNames = managerNames.split(',').map((m) => m.trim().toLowerCase()); - for (const name of managerNames) { - const user = await userService.getByUsername(name); - if (!user) { - throw new SiteError(400, `Trying to set unknown user "${name}" as project manager`); - } - managers.push(user._id); - } - - return managers; - } - - async getProjectById (projectId) { - const project = ClientProject - .findOne({ _id: projectId }) - .populate(this.populateClientProject) - .lean(); - return project; - } - - async getProjectsForUser (user) { - const projects = await ClientProject - .find({ user: user._id }) - .sort({ name: 1 }) - .populate(this.populateClientProject) - .lean(); - return projects; - } - - async getProjectsForClient (client) { - const projects = await ClientProject - .find({ client: client._id }) - .sort({ name: 1 }) - .populate(this.populateClientProject) - .lean(); - return projects; - } - - async getProjectsForManager (manager) { - const projects = await ClientProject - .find({ managers: manager._id }) - .sort({ name: 1 }) - .populate(this.populateClientProject) - .lean(); - return projects; - } - - async addTimeWorked (client, duration) { - const { task: taskService } = this.dtp.services; - - const durationLimit = client.hoursLimit * 3600; - if ((client.weeklyTotals.timeWorked + duration) > durationLimit) { - this.log.alert('clamping time worked to weekly hours limit', { - user: { - _id: client.user._id, - username: client.user.username, - }, - client: { - _id: client._id, - hoursLimit: client.hoursLimit, - }, - }); - await Client.updateOne( - { _id: client._id }, - { - $set: { 'weeklyTotals.timeWorked': durationLimit }, - }, - ); - - this.log.alert('Ending work sessions for user due to hours limit reached', { - user: { - _id: client.user._id, - username: client.user.username, - }, - client: { - _id: client._id, - hoursLimit: client.hoursLimit, - }, - }); - await taskService.closeTaskSessionForUser(client.user); - - const displayList = this.createDisplayList('session-control'); - displayList.showNotification( - 'Hours limit reached. The task work session has been closed.', - 'danger', - 'bottom-center', - 5000, - ); - - this.dtp.emitter - .to(client.user._id.toString()) - .emit('session-control', { - cmd: 'end-session', - displayList, - }); - - return; - } - - this.log.info('adding time worked to client weekly totals', { - user: { - _id: client.user._id, - username: client.user.username, - }, - client: { - _id: client._id, - hoursLimit: client.hoursLimit, - currentHours: client.weeklyTotals.timeWorked / 3600, - }, - hours: duration / 3600, - }); - await Client.updateOne( - { _id: client._id }, - { - $inc: { 'weeklyTotals.timeWorked': duration }, - }, - ); - } -} \ No newline at end of file diff --git a/app/services/report.js b/app/services/report.js deleted file mode 100644 index a1b3c4f..0000000 --- a/app/services/report.js +++ /dev/null @@ -1,141 +0,0 @@ -// report.js -// Copyright (C) 2024 DTP Technologies, LLC -// All Rights Reserved - -'use strict'; - -import mongoose from 'mongoose'; -const TaskSession = mongoose.model('TaskSession'); - -import dayjs from 'dayjs'; - -import { SiteService } from '../../lib/site-lib.js'; - -export default class ReportService extends SiteService { - - static get name ( ) { return 'ReportService'; } - static get slug () { return 'report'; } - - constructor (dtp) { - super(dtp, ReportService); - } - - async getWeeklyEarnings (user) { - const { client: clientService } = this.dtp.services; - const NOW = new Date(); - - const dateStart = this.startOfWeek(NOW); - const dateEnd = dayjs(dateStart).add(1, 'week').toDate(); - - const data = await TaskSession.aggregate([ - { - $match: { - user: user._id, - $and: [ - { created: { $gte: dateStart } }, - { finished: { $lt: dateEnd } }, - ], - }, - }, - { - $group: { - _id: { project: '$project' }, - sessionCount: { $sum: 1 }, - duration: { $sum: '$duration' }, - billable: { - $sum: { - $multiply: [ - '$hourlyRate', - { $divide: ['$duration', 3600 ] }, - ], - }, - }, - }, - }, - { - $project: { - _id: 0, - project: '$_id.project', - sessionCount: '$sessionCount', - duration: '$duration', - billable: '$billable', - }, - }, - ]); - - const response = await TaskSession.populate(data, [ - { - path: 'project', - populate: clientService.populateClientProject, - }, - ]); - - return response; - } - - async getDailyHoursWorkedForUser (user) { - const NOW = new Date(); - - const dateStart = this.startOfWeek(NOW); - const dateEnd = dayjs(dateStart).add(1, 'week').toDate(); - - const response = await TaskSession.aggregate([ - { - $match: { - $and: [ - { user: user._id }, - { finished: { $gt: dateStart } }, - { created: { $lt: dateEnd } }, - ], - } - }, - { - $group: { - _id: { - year: { $year: '$created' }, - month: { $month: '$created' }, - day: { $dayOfMonth: '$created' }, - }, - workSessionCount: { $sum: 1 }, - timeWorked: { $sum: '$duration' }, - }, - }, - { - $project: { - _id: false, - date: { - $dateFromParts: { - year: '$_id.year', - month: '$_id.month', - day: '$_id.day', - }, - }, - workSessionCount: '$workSessionCount', - hoursWorked: { $divide: ['$timeWorked', 3600] }, - }, - }, - ]); - - if (response.length < 7) { - let currentDay = dayjs(dateStart).add(response.length, 'day'); - while (response.length < 7) { - response.push({ - date: currentDay, - workSessionCount: 0, - hoursWorked: 0, - }); - currentDay = dayjs(currentDay).add(1, 'day'); - } - } - - return response; - } - - startOfWeek (date) { - date = date || new Date(); - date.setHours(0,0,0,0); - - var diff = date.getDate() - date.getDay() + (date.getDay() === 0 ? -6 : 1); - return new Date(date.setDate(diff)); - } -} \ No newline at end of file diff --git a/app/services/task.js b/app/services/task.js deleted file mode 100644 index 7199f4d..0000000 --- a/app/services/task.js +++ /dev/null @@ -1,329 +0,0 @@ -// task.js -// Copyright (C) 2024 DTP Technologies, LLC -// All Rights Reserved - -'use strict'; - -import mongoose from 'mongoose'; -const Task = mongoose.model('Task'); -const TaskSession = mongoose.model('TaskSession'); - -import dayjs from 'dayjs'; - -import { SiteService, SiteError } from '../../lib/site-lib.js'; - -export default class TaskService extends SiteService { - - static get name ( ) { return 'TaskService'; } - static get slug () { return 'task'; } - - constructor (dtp) { - super(dtp, TaskService); - } - - async start ( ) { - const { client: clientService, user: userService } = this.dtp.services; - - // const { jobQueue: jobQueueService } = this.dtp.services; - // this.linksQueue = jobQueueService.getJobQueue('links', this.dtp.config.jobQueues.links); - - this.populateTask = [ - { - path: 'user', - select: userService.USER_SELECT, - }, - { - path: 'client', - populate: clientService.populateClient, - }, - { - path: 'project', - populate: clientService.populateClientProject, - }, - ]; - - this.populateTaskSession = [ - { - path: 'user', - select: userService.USER_SELECT, - }, - { - path: 'client', - populate: clientService.populateClient, - }, - { - path: 'project', - populate: clientService.populateClientProject, - }, - { - path: 'task', - populate: this.populateTask, - }, - { - path: 'screenshots.image', - } - ]; - } - - async createTask (user, taskDefinition) { - const { text: textService } = this.dtp.services; - const NOW = new Date(); - - const task = new Task(); - task.created = NOW; - task.user = user._id; - task.client = mongoose.Types.ObjectId.createFromHexString(taskDefinition.clientId); - task.project = mongoose.Types.ObjectId.createFromHexString(taskDefinition.projectId); - task.status = 'pending'; - task.note = textService.filter(taskDefinition.note); - - await task.save(); - - return task.toObject(); - } - - async startTask (task) { - if (task.status !== 'pending') { - throw new SiteError(400, 'The task is not in the pending state'); - } - await Task.updateOne( - { _id: task._id }, - { $set: { status: 'active' } }, - ); - } - - async closeTask (task) { - await TaskSession - .find({ task: task._id, status: 'active' }) - .cursor() - .eachAsync(async (session) => { - await this.closeTaskSession(session); - }); - - await Task.updateOne( - { _id: task._id }, - { $set: { status: 'finished' } }, - ); - } - - async getTasksForUser (user, options, pagination) { - const search = { user: user._id }; - - options = options || { }; - if (options.pending) { - search.status = 'pending'; - } else if (options.active) { - search.status = 'active'; - } else if (options.finished) { - search.status = 'finished'; - } - - const tasks = await Task - .find(search) - .sort({ created: -1 }) - .skip(pagination.skip) - .limit(pagination.cpp) - .populate(this.populateTask) - .lean(); - - return tasks.reverse(); - } - - async getTasksForProject (project, options, pagination) { - const search = { project: project._id }; - - options = options || { }; - if (options.pending) { - search.status = 'pending'; - } else if (options.active) { - search.status = 'active'; - } else if (options.finished) { - search.status = 'finished'; - } - - const tasks = await Task - .find(search) - .sort({ created: -1 }) - .skip(pagination.skip) - .limit(pagination.cpp) - .populate(this.populateTask) - .lean(); - - return tasks.reverse(); - } - - async getTaskGridForUser (user) { - const pagination = { skip: 0, cpp: 10 }; - const pendingTasks = await this.getTasksForUser(user, { pending: true }, pagination); - const activeTasks = await this.getTasksForUser(user, { active: true }, pagination); - const finishedTasks = await this.getTasksForUser(user, { finished: true }, pagination); - return { pendingTasks, activeTasks, finishedTasks }; - } - - async getTaskGridForProject (project) { - const pagination = { skip: 0, cpp: 10 }; - const pendingTasks = await this.getTasksForProject(project, { pending: true }, pagination); - const activeTasks = await this.getTasksForProject(project, { active: true }, pagination); - const finishedTasks = await this.getTasksForProject(project, { finished: true }, pagination); - return { pendingTasks, activeTasks, finishedTasks }; - } - - async getTaskById (taskId) { - const task = await Task - .findOne({ _id: taskId }) - .populate(this.populateTask) - .lean(); - return task; - } - - async createTaskSession (task) { - const NOW = new Date(); - - if (await TaskSession.findOne({ user: task.user._id, status: 'active' })) { - throw new SiteError(401, "Can't start new session with a currently active session."); - } - - let session = new TaskSession(); - session.created = NOW; - session.lastUpdated = NOW; - session.hourlyRate = task.project.hourlyRate; - session.user = task.user._id; - session.client = task.client._id; - session.project = task.project._id; - session.task = task._id; - session.status = 'active'; - - await session.save(); - session = await TaskSession.populate(session, this.populateTaskSession); - - this.log.info('task session created', { - user: { - _id: task.user._id, - username: task.user.username, - }, - session: { - _id: session._id, - }, - }); - - return session.toObject(); - } - - async addTaskSessionScreenshot (session, file) { - const NOW = new Date(); - const { image: imageService } = this.dtp.services; - const image = await imageService.create(session.user._id, { }, file); - await TaskSession.updateOne( - { _id: session._id }, - { - $push: { - screenshots: { - created: NOW, - image: image._id, - }, - }, - }, - ); - - const displayList = this.createDisplayList('screenshot-accepted'); - displayList.showNotification( - 'Screenshot accepted', - 'success', - 'bottom-center', - 3000, - ); - - this.dtp.emitter - .to(session.task._id.toString()) - .emit('session-control', { - displayList, - }); - } - - async setTaskSessionStatus (session, status) { - if (status === session.status) { - return; // do nothing - } - if (!['active', 'reconnecting'].includes(status)) { - throw new SiteError(400, 'Can only set status to active or reconnecting'); - } - - this.log.info('updating task session status', { - user: { - _id: session.user._id, - username: session.user.username, - }, - session: { _id: session._id }, - status, - }); - - await TaskSession.updateOne({ _id: session._id }, { $set: { status } }); - } - - async closeTaskSession (session, status = 'finished') { - const { client: clientService } = this.dtp.services; - const NOW = new Date(); - - if (session.status !== 'active') { - throw new SiteError(400, 'The session is not currently active'); - } - - const duration = dayjs(NOW).diff(session.created, 'second'); - await TaskSession.updateOne( - { _id: session._id }, - { - $set: { - finished: NOW, - status, - duration, - }, - }, - ); - - await Task.updateOne( - { _id: session.task._id }, - { - $inc: { duration }, - }, - ); - - await clientService.addTimeWorked(session.client, duration); - - this.log.info('task session closed', { - user: { - _id: session.user._id, - username: session.user.username, - }, - duration, - }); - } - - async getSessionsForTask (task, pagination) { - const sessions = TaskSession - .find({ task: task._id }) - .sort({ created: 1 }) - .skip(pagination.skip) - .limit(pagination.cpp) - .populate(this.populateTaskSession) - .lean(); - return sessions; - } - - async getTaskSessionById (sessionId) { - const session = await TaskSession - .findOne({ _id: sessionId }) - .populate(this.populateTaskSession) - .lean(); - return session; - } - - async closeTaskSessionForUser (user) { - await TaskSession - .find({ user: user._id, status: 'active' }) - .populate(this.populateTaskSession) - .cursor() - .eachAsync(async (session) => { - await this.closeTaskSession(session); - }); - } -} \ No newline at end of file diff --git a/app/services/text.js b/app/services/text.js index 95e737c..8315b6c 100644 --- a/app/services/text.js +++ b/app/services/text.js @@ -10,12 +10,15 @@ const User = mongoose.model('User'); const Link = mongoose.model('Link'); import striptags from 'striptags'; -import unzalgo from 'unzalgo'; import shoetest from 'shoetest'; import diacritics from 'diacritics'; import DtpTextFilter from './lib/edit-with-vi.js'; -import { SiteService, SiteError } from '../../lib/site-lib.js'; +import { + SiteService, + SiteError, + SiteUnzalgo, +} from '../../lib/site-lib.js'; export default class TextService extends SiteService { @@ -37,7 +40,7 @@ export default class TextService extends SiteService { * @returns The cleaned text */ clean (text) { - text = unzalgo.clean(text); + text = SiteUnzalgo.clean(text); text = striptags(text.trim()); return text; } diff --git a/app/views/client/dashboard.pug b/app/views/client/dashboard.pug deleted file mode 100644 index 1e6e7fb..0000000 --- a/app/views/client/dashboard.pug +++ /dev/null @@ -1,24 +0,0 @@ -extends ../layout/main -block view-content - - section.uk-section.uk-section-default.uk-section-small - .uk-container - - .uk-margin - div(uk-grid) - .uk-width-expand - h1 Clients - .uk-width-auto - a(href="/client/create").uk-button.uk-button-default.uk-border-rounded Create Client - - .uk-margin - if Array.isArray(clients) && (clients.length > 0) - ul.uk-list.uk-list-divider - each client in clients - li - a(href=`/client/${client._id}`).uk-link-reset.uk-display-block - .uk-text-lead= client.name - div= client.description || 'No description' - - else - div There are no clients \ No newline at end of file diff --git a/app/views/client/editor.pug b/app/views/client/editor.pug deleted file mode 100644 index 984f44d..0000000 --- a/app/views/client/editor.pug +++ /dev/null @@ -1,54 +0,0 @@ -extends ../layout/main -block view-content - - section.uk-section.uk-section-default.uk-section-small - .uk-container - - .uk-margin - - var actionUrl = !!client ? `/client/${client._id}` : "/client"; - form(method="POST", action= actionUrl).uk-form - .uk-card.uk-card-default.uk-card-small - .uk-card-header - h1.uk-card-title #[span= !!client ? "Edit" : "New"] Client - - .uk-card-body - .uk-margin - label(for="name").uk-form-label Client name - input( - id="name", - name="name", - type="text", - value= !!client ? client.name : undefined, - placeholder="Enter client name", - ).uk-input - - .uk-margin - label(for="description").uk-form-label Client description - textarea( - id="name", - name="description", - rows=4, - placeholder="Enter client description", - ).uk-textarea.uk-resize-vertical= !!client ? client.description : undefined - - .uk-margin - label(for="hours-limit").uk-form-label Max. hours per week - input( - id="hours-limit", - name="hoursLimit", - type="number", - value= !!client ? (client.hoursLimit || 40) : 40, - placeholder="Enter max. hours/week (0=unlimited)", - ).uk-input - .uk-text-small.uk-text-muted Use zero or 168 (7 x 24) for unlimited hours per week. - - .uk-card-footer - div(uk-grid).uk-grid-small.uk-flex-right - .uk-width-auto - a(href=`/client/${client._id}`).uk-button.uk-button-default.uk-border-rounded - i.fa-solid.fa-chevron-left - span.uk-margin-small-left Back - .uk-width-auto - button(type="submit").uk-button.uk-button-default.uk-border-rounded - i.fa-solid.fa-save - span.uk-margin-small-left #{!!client ? "Update" : "Create"} Client \ No newline at end of file diff --git a/app/views/client/project/editor.pug b/app/views/client/project/editor.pug deleted file mode 100644 index 0320246..0000000 --- a/app/views/client/project/editor.pug +++ /dev/null @@ -1,60 +0,0 @@ -extends ../../layout/main -block view-content - - section.uk-section.uk-section-default.uk-section-small - .uk-container - - .uk-margin - - var actionUrl = !!project ? `/client/${client._id}/project/${project._id}` : `/client/${client._id}/project`; - form(method="POST", action= actionUrl).uk-form - .uk-card.uk-card-default.uk-card-small - .uk-card-header - div(uk-grid).uk-grid-small.uk-flex-middle - .uk-width-expand - h1.uk-card-title #[span= !!project ? "Edit" : "New"] Project - .uk-width-auto - div #{client.name} - - .uk-card-body - .uk-margin - label(for="name").uk-form-label Project name - input( - id="name", - name="name", - type="text", - value= !!project ? project.name : undefined, - placeholder="Enter project name", - ).uk-input - - .uk-margin - label(for="description").uk-form-label Project description - textarea( - id="name", - name="description", - rows=4, - placeholder="Enter project description" - ).uk-textarea.uk-resize-vertical= !!project ? project.description : "" - - .uk-margin - label(for="managers").uk-form-label Managers - input( - id="managers", - name="managers", - type="text", - value= !!project ? (project.managers || [ ]).map((m) => m.username).join(',') : undefined, - placeholder="Enter comma-separated list of usernames", - ).uk-input - .uk-text-small.uk-text-muted Managers can review work sessions and access billing information. - - .uk-margin - label(for="hourly-rate").uk-form-label Hourly rate - input( - id="hourly-rate", - name="hourlyRate", - type="number", - value= !!project ? project.hourlyRate : undefined, - placeholder="Enter hourly rate", - ).uk-input - - .uk-card-footer.uk-flex.uk-flex-right - button(type="submit").uk-button.uk-button-default.uk-border-rounded #[span= !!project ? "Update" : "Create"] Project \ No newline at end of file diff --git a/app/views/client/project/view.pug b/app/views/client/project/view.pug deleted file mode 100644 index 24c3968..0000000 --- a/app/views/client/project/view.pug +++ /dev/null @@ -1,39 +0,0 @@ -extends ../../layout/main -block view-content - - include ../../task/components/grid - - section.uk-section.uk-section-default.uk-section-small - .uk-container - - .uk-margin - div(uk-grid).uk-grid-small.uk-flex-middle - .uk-width-expand - h1.uk-margin-remove= project.name - .uk-width-auto - a(href=`/client/${client._id}/project/${project._id}/edit`).uk-button.uk-button-default.uk-button-small - i.fa-solid.fa-cog - span.uk-margin-small-left Settings - .uk-width-auto - .uk-text-bold - a(href=`/client/${client._id}`).uk-link-reset= client.name - .uk-text-small #{numeral(project.hourlyRate).format('$0,0.00')}/hr - - if project.description - div= project.description - - .uk-margin-medium - form(method="POST", action="/task").uk-form - input(type="hidden", name="clientId", value= client._id) - input(type="hidden", name="projectId", value= project._id) - .uk-card.uk-card-secondary.uk-card-small.uk-border-rounded - .uk-card-body.uk-border-rounded - label(for="note").uk-form-label.sr-only Task note - div(uk-grid).uk-grid-small - .uk-width-expand - input(id="note", name="note", placeholder="What will you be working on?").uk-input.uk-border-rounded - .uk-width-auto - button(type="submit").uk-button.uk-button-default.uk-border-rounded Create Task - - .uk-margin-medium - +renderTaskGrid(taskGrid.pendingTasks, taskGrid.activeTasks, taskGrid.finishedTasks) \ No newline at end of file diff --git a/app/views/client/view.pug b/app/views/client/view.pug deleted file mode 100644 index 00695ce..0000000 --- a/app/views/client/view.pug +++ /dev/null @@ -1,32 +0,0 @@ -extends ../layout/main -block view-content - - section.uk-section.uk-section-default.uk-section-small - .uk-container - - .uk-margin-medium - div(uk-grid).uk-grid-small - .uk-width-expand - h1.uk-margin-remove= client.name - if client.description - div= client.description - .uk-width-auto - a(href=`/client/${client._id}/edit`).uk-button.uk-button-default.uk-button-small - i.fa-solid.fa-cog - span.uk-margin-small-left Settings - .uk-width-auto - a(href=`/client/${client._id}/project/create`).uk-button.uk-button-default.uk-button-small - i.fa-solid.fa-plus - span.uk-margin-small-left Add Project - - .uk-margin - h2 Projects - if Array.isArray(projects) && (projects.length > 0) - ul.uk-list.uk-list-divided - each project in projects - li - a(href=`/client/${client._id}/project/${project._id}`).uk-link-reset.uk-display-block - .uk-text-bold= project.name - .uk-text-small= project.description || 'No description' - else - div There are no projects \ No newline at end of file diff --git a/app/views/components/navbar.pug b/app/views/components/navbar.pug index dda215c..13ae90f 100644 --- a/app/views/components/navbar.pug +++ b/app/views/components/navbar.pug @@ -9,13 +9,6 @@ nav(style="background: #000000;").uk-navbar-container.uk-light li a(href="/", aria-label="Back to Home").uk-navbar-item .uk-text-bold.no-select Home - if user - li - a(href="/client", aria-label="Manage your clients").uk-navbar-item - .uk-text-bold.no-select Clients - li - a(href="/report", aria-label="View your work reports").uk-navbar-item - .uk-text-bold.no-select Reports .uk-navbar-right if !user diff --git a/app/views/home.pug b/app/views/home.pug index d099865..5970310 100644 --- a/app/views/home.pug +++ b/app/views/home.pug @@ -1,39 +1,8 @@ extends layout/main block view-content - include task/components/grid - include report/components/weekly-summary - - if Array.isArray(projects) && (projects.length > 0) - section.uk-section.uk-section-muted.uk-section-small - .uk-container - h1 Your Projects - if weeklyEarnings.length > 0 - +renderWeeklySummaryReport(weeklyEarnings) - else - div No time worked this week. - - section.uk-section.uk-section-default.uk-section-small - .uk-container - +renderTaskGrid( - taskGrid.pendingTasks, - taskGrid.activeTasks, - taskGrid.finishedTasks, - ) - - if Array.isArray(managedProjects) && (managedProjects.length > 0) - section.uk-section.uk-section-muted.uk-section-small - .uk-container - h1 Projects You Manage - - section.uk-section.uk-section-default.uk-section-small - .uk-container - ul.uk-list.uk-list-divider - each project in managedProjects - li - h2= project.name - +renderTaskGrid( - project.taskGrid.pendingTasks, - project.taskGrid.activeTasks, - project.taskGrid.finishedTasks, - ) \ No newline at end of file + section.uk-section.uk-section-default.uk-section-small + .uk-container + + h1= site.name + p Welcome to the authenticated home page. You are logged in. This app does nothing, so get to work building your features and ship often! \ No newline at end of file diff --git a/app/views/layout/main.pug b/app/views/layout/main.pug index 9a0df72..e16e740 100644 --- a/app/views/layout/main.pug +++ b/app/views/layout/main.pug @@ -22,7 +22,7 @@ html(lang='en', data-obs-widget= obsWidget) block vendorcss - link(rel='stylesheet', href=`/dist/${(user) ? user.ui.theme : 'tracker-light'}.css?v=${pkg.version}`) + link(rel='stylesheet', href=`/dist/${(user) ? user.ui.theme : 'dtp-light'}.css?v=${pkg.version}`) link(rel='stylesheet', href=`/pretty-checkbox/pretty-checkbox.min.css?v=${pkg.version}`) block viewcss diff --git a/app/views/manager/dashboard.pug b/app/views/manager/dashboard.pug deleted file mode 100644 index 152726e..0000000 --- a/app/views/manager/dashboard.pug +++ /dev/null @@ -1,12 +0,0 @@ -extends ../layout/main -block view-content - - include ../user/components/profile-picture - - section.uk-section.uk-section-default.uk-section-small - .uk-container - - h1 Projects You Manage - if Array.isArray(projects) && (projects.length > 0) - else - div You don't manage any projects at this time. \ No newline at end of file diff --git a/app/views/report/components/weekly-summary.pug b/app/views/report/components/weekly-summary.pug deleted file mode 100644 index d9158c9..0000000 --- a/app/views/report/components/weekly-summary.pug +++ /dev/null @@ -1,20 +0,0 @@ -mixin renderWeeklySummaryReport (data) - .uk-overflow-auto - table.uk-table.uk-table-small.no-select - thead - tr - th Project - th Client - th Sessions - th Time Worked - th Billable - tbody - each row in data - tr - td.uk-table-expand - a(href=`/client/${row.project.client._id}/project/${row.project._id}`)=row.project.name - td - a(href=`/client/${row.project.client._id}`)= row.project.client.name - td= formatCount(row.sessionCount) - td= numeral(row.duration).format('0:00:00') - td= numeral(row.billable).format('$0,0.00') \ No newline at end of file diff --git a/app/views/report/dashboard.pug b/app/views/report/dashboard.pug deleted file mode 100644 index f7eaf96..0000000 --- a/app/views/report/dashboard.pug +++ /dev/null @@ -1,41 +0,0 @@ -extends ../layout/main -block view-content - - include components/weekly-summary - - section.uk-section.uk-section-default.uk-section - .uk-container - h1 Week of #{dayjs(weekStartDate).format('MMMM DD')} - +renderWeeklySummaryReport(weeklyEarnings) - - .uk-margin-medium - h2 Daily Hours - .uk-overflow-auto - table.uk-table.uk-table-small.uk-table-divider - thead - tr - th.uk-table-expand Day of Week - th.uk-text-nowrap.uk-text-right.uk-table-shrink Session Count - th.uk-text-nowrap.uk-text-right.uk-table-shrink Hours Worked - th.uk-text-nowrap.uk-text-right.uk-table-shrink Time Worked - tbody - - - var totalHoursWorked = 0; - var totalSessionCount = 0; - - each day in dailyTimeWorked - - - totalHoursWorked += day.hoursWorked; - totalSessionCount += day.workSessionCount; - - tr - td.uk-table-expand= dayjs(day.date).format('dddd') - td.uk-text-right.uk-text-nowrap.uk-table-shrink= (day.workSessionCount > 0) ? formatCount(day.workSessionCount) : '---' - td.uk-text-right.uk-text-nowrap.uk-table-shrink= (day.hoursWorked > 0) ? numeral(day.hoursWorked).format('0.00') : '---' - td.uk-text-right.uk-text-nowrap.uk-table-shrink= (day.hoursWorked > 0) ? numeral(day.hoursWorked * 3600).format('0:00:00') : '---' - tfoot - tr - td.uk-table-expand TOTALS - td.uk-text-right.uk-text-nowrap.uk-table-shrink= formatCount(totalSessionCount) - td.uk-text-right.uk-text-nowrap.uk-table-shrink= numeral(totalHoursWorked).format('0.00') - td.uk-text-right.uk-text-nowrap.uk-table-shrink= numeral(totalHoursWorked * 3600).format('0:00:00') \ No newline at end of file diff --git a/app/views/task/components/grid.pug b/app/views/task/components/grid.pug deleted file mode 100644 index 25c44b8..0000000 --- a/app/views/task/components/grid.pug +++ /dev/null @@ -1,23 +0,0 @@ -include list -mixin renderTaskGrid (pendingTasks, activeTasks, finishedTasks) - div(uk-grid).uk-grid-divider - div(class="uk-width1-1 uk-width-1-3@m") - h3 Pending Tasks - if Array.isArray(pendingTasks) && (pendingTasks.length > 0) - +renderTaskList(pendingTasks) - else - div No pending tasks - - div(class="uk-width1-1 uk-width-1-3@m") - h3 Active Tasks - if Array.isArray(activeTasks) && (activeTasks.length > 0) - +renderTaskList(activeTasks) - else - div No active tasks - - div(class="uk-width1-1 uk-width-1-3@m") - h3 Finished Tasks - if Array.isArray(finishedTasks) && (finishedTasks.length > 0) - +renderTaskList(finishedTasks) - else - div No finished tasks \ No newline at end of file diff --git a/app/views/task/components/list.pug b/app/views/task/components/list.pug deleted file mode 100644 index db41828..0000000 --- a/app/views/task/components/list.pug +++ /dev/null @@ -1,11 +0,0 @@ -mixin renderTaskList (tasks) - ul.uk-list.uk-list-divider - each task in tasks - li - a(href=`/task/${task._id}`).uk-display-block.uk-link-reset - div(uk-grid).uk-grid-small.uk-flex-middle - .uk-width-expand= task.note - .uk-width-auto - .uk-text-small= numeral(task.duration).format('00:00:00') - .uk-text-small.uk-text-muted - span #{task.project.name}, #{task.client.name} diff --git a/app/views/task/session/view.pug b/app/views/task/session/view.pug deleted file mode 100644 index 4964a6d..0000000 --- a/app/views/task/session/view.pug +++ /dev/null @@ -1,55 +0,0 @@ -extends ../../layout/main -block view-content - - include ../../user/components/profile-picture - - section.uk-section.uk-section-default.uk-section-small - .uk-container - - .uk-margin-medium - div(uk-grid).uk-grid-small - .uk-width-auto - +renderProfilePicture(user) - .uk-width-expand - .uk-text-large= user.displayName || user.username - div Work session #[code #{session._id}] for task "#{task.note}" - .uk-width-auto.uk-text-right - div= numeral(session.duration).format('HH:MM:SS') - .uk-text-small #{numeral(session.hourlyRate * (session.duration / 60 / 60)).format('$0,0.00')} - - .uk-margin-medium - .uk-flex.uk-flex-between.uk-flex-wrap - .uk-width-auto - .uk-margin - .uk-text-small.uk-text-bold Created - div= dayjs(session.created).format('MMM DD, YYYY [at] hh:mm:ss a') - .uk-width-auto - .uk-margin - .uk-text-small.uk-text-bold Last Updated - div= dayjs(session.lastUpdated).format('MMM DD, YYYY [at] hh:mm:ss a') - .uk-width-auto - .uk-margin - .uk-text-small.uk-text-bold Finished - div= dayjs(session.finished).format('MMM DD, YYYY [at] hh:mm:ss a') - .uk-width-auto - .uk-margin - .uk-text-small.uk-text-bold Project - div= session.project.name - .uk-width-auto - .uk-margin - .uk-text-small.uk-text-bold Client - div= session.client.name - - .uk-margin-medium - if Array.isArray(session.screenshots) && (session.screenshots.length > 0) - h3 Screenshots - div(class="uk-child-width-1-2 uk-child-width-1-3@s uk-child-width-1-4@m uk-child-width-1-5@l uk-child-width-1-6@xl", uk-grid, uk-lightbox="animation: slide").uk-grid-small - each screenshot in session.screenshots - a(href=`/image/${screenshot.image._id}`, - data-type="image", - data-caption= `${dayjs(screenshot.created).format('MMM DD [at] h:mm a')} | ${screenshot.image.metadata.format.toUpperCase()} | ${numeral(screenshot.image.size).format('0,0.0b')}`, - ).uk-link-reset - img(src=`/image/${screenshot.image._id}`, width= screenshot.image.metadata.width, height= screenshot.image.metadata.height, alt="Image attachment") - .uk-text-small.uk-text-muted= dayjs(screenshot.created).format('MMM DD [at] h:mm a') - else - div No screenshots were filed by the session. \ No newline at end of file diff --git a/app/views/task/view.pug b/app/views/task/view.pug deleted file mode 100644 index e520290..0000000 --- a/app/views/task/view.pug +++ /dev/null @@ -1,102 +0,0 @@ -extends ../layout/main -block view-content - - include ../user/components/profile-picture - - section.uk-section.uk-section-default.uk-section-small - .uk-container - - .uk-margin - div(uk-grid) - div(class="uk-width-1-1 uk-width-expand@m") - .uk-margin-medium - div(uk-grid).uk-grid-small - .uk-width-auto - +renderProfilePicture(user) - .uk-width-expand - div(style="line-height: 1;").uk-text-lead.uk-text-truncated.uk-margin-small= task.note - if !manager - case task.status - when 'pending' - .uk-margin-small - form(method="POST", action=`/task/${task._id}/start`).uk-form - button(type="submit").uk-button.uk-button-default.uk-button-small.uk-border-rounded Start Task - when 'active' - .uk-margin-small - form(method="POST", action=`/task/${task._id}/close`).uk-form - button(type="submit").uk-button.uk-button-default.uk-button-small.uk-border-rounded Finish Task - when 'finished' - .uk-text-success Finished task - else - div Task status: #{task.status} - - if task.status === 'active' && !manager - .uk-width-auto.uk-text-right - .pretty.p-switch.p-slim - input( - id="active-toggle", - type="checkbox", - data-task-id= task._id, - onchange="return dtp.app.taskActivityToggle(event);", - ) - .state.p-success - label(for="active-toggle") - span.sr-only Active - #current-session-duration.uk-text-small.no-select= numeral(0).format('HH:MM:SS') - - - var timeRemaining = task.client.hoursLimit - (task.client.weeklyTotals.timeWorked / 3600); - .uk-text-small.no-select avail: #[span#time-remaining= numeral(timeRemaining).format('0,0.00')] - - .uk-margin-medium - - .uk-margin - h3(style="line-height: 1em;").uk-padding-remove.uk-margin-remove Work Sessions - small.uk-text-muted Week of #{dayjs(weekStartDate).format('MMMM DD')} - - if Array.isArray(sessions) && (sessions.length > 0) - - - var totalTimeWorked = 0; - var totalBillable = 0; - - table.uk-table.uk-table-small.uk-table-divider - thead - tr - th.uk-table-expand Start Time - th.uk-text-nowrap.uk-table-shrink Tracked - th.uk-text-nowrap.uk-table-shrink Billable - tbody - each session in sessions - - - totalTimeWorked += session.duration; - totalBillable += session.hourlyRate * (session.duration / 3600); - tr - td.uk-table-expand - a(href=`/task/${task._id}/session/${session._id}`, - onclick="return dtp.app.performSessionNavigation(event);", - ).uk-link-reset.uk-display-block= dayjs(session.created).format('dddd [at] h:mm a') - td.uk-text-right.uk-text-nowrap.uk-table-shrink= numeral(session.duration).format('HH:MM:SS') - td.uk-text-right.uk-text-nowrap.uk-table-shrink= numeral(session.hourlyRate * (session.duration / 3600)).format('$0,00.00') - tfoot - tr - td.uk-table-expand TOTALS - td.uk-text-right.uk-text-nowrap.uk-table-shrink= numeral(totalTimeWorked).format('HH:MM:SS') - td.uk-text-right.uk-text-nowrap.uk-table-shrink #{numeral(totalBillable).format('$0,0.00')} - else - div No work sessions - - if !manager && (task.status === 'active') - div(class="uk-width-1-1 uk-flex-first uk-width-large@m uk-flex-last@m") - .uk-margin - video( - id="capture-preview", - poster="/img/default-poster.svg", - playsinline, muted, - ).dtp-video.no-select - div(class="uk-visible@m").uk-margin.uk-text-small.uk-text-muted - p One image will be captured from this live preview every 10 minutes. It will be uploaded and stored in the work session with a timestamp. - p When you start a work session, you will select the screen, application, or browser tab to share. - -block viewjs - script. - window.dtp = window.dtp || { }; - window.dtp.task = !{JSON.stringify(task)}; \ No newline at end of file diff --git a/app/views/user/settings.pug b/app/views/user/settings.pug index 7d2dc25..f318bee 100644 --- a/app/views/user/settings.pug +++ b/app/views/user/settings.pug @@ -68,8 +68,8 @@ block view-content .uk-margin label(for="ui-theme").uk-form-label UI Theme select(id="ui-theme", name="uiTheme").uk-select - option(value="tracker-light", selected= (user.ui.theme === 'tracker-light')) Light - option(value="tracker-dark", selected= (user.ui.theme === 'tracker-dark')) Dark + option(value="dtp-light", selected= (user.ui.theme === 'dtp-light')) Light + option(value="dtp-dark", selected= (user.ui.theme === 'dtp-dark')) Dark li .uk-margin diff --git a/app/views/welcome/home.pug b/app/views/welcome/home.pug index 908d9dc..87bf5ee 100644 --- a/app/views/welcome/home.pug +++ b/app/views/welcome/home.pug @@ -24,9 +24,6 @@ block view-content h2 About #{site.name} - p #{site.name} is a real-time communications tool with high quality audio and video, extremely low latency, instant messaging, voicemail, and an easy-to-use interface. It removes and avoids everything unnecessary while focusing on being excellent at making calls and helping people stay in touch. + p #{site.name} provides an application development harness for building secure and scalable production-ready web applications. - p There is no app to download from a store. #{site.name} is a Progressive Web App (PWA) that runs in your browser. It can be installed as a desktop or mobile app if you like, and provides an even sleeker interface if you do. It can do a better job delivering notifications when installed as an app. - - .uk-margin - .uk-text-small.uk-text-muted Anonymous use is not supported. A user account in good standing is required to use the app. #{site.name} is not free for hosting group and conference calls. Free members can make and receive calls, but can't create group/conference calls and don't have voicemail services. \ No newline at end of file + p There is no app to download from a store. #{site.name} is a Progressive Web App (PWA) that runs in your browser. It can be installed as a desktop or mobile app if you like, and provides a more native interface. #{site.name} can do a better job delivering notifications when installed as an app. \ No newline at end of file diff --git a/app/workers/tracker-monitor.js b/app/workers/tracker-monitor.js deleted file mode 100644 index 9cdc996..0000000 --- a/app/workers/tracker-monitor.js +++ /dev/null @@ -1,107 +0,0 @@ -// tracker-monitor.js -// Copyright (C) 2024 DTP Technologies, LLC -// All Rights Reserved - -'use strict'; - -import 'dotenv/config'; - -import path, { dirname } from 'path'; -import dayjs from 'dayjs'; - -import { SiteRuntime } from '../../lib/site-lib.js'; - -import { CronJob } from 'cron'; -const CRON_TIMEZONE = 'America/New_York'; - -class TrackerMonitorWorker extends SiteRuntime { - - static get name ( ) { return 'TrackerMonitorWorker'; } - static get slug ( ) { return 'trackerMonitor'; } - - constructor (rootPath) { - super(TrackerMonitorWorker, rootPath); - } - - async start ( ) { - await super.start(); - - const mongoose = await import('mongoose'); - this.TaskSession = mongoose.model('TaskSession'); - - this.viewModel = { }; - await this.populateViewModel(this.viewModel); - - /* - * Cron jobs - */ - - const sessionExpireSchedule = '* */5 * * * *'; // Every 5 minutes - this.cronJob = new CronJob( - sessionExpireSchedule, - this.expireTaskSessions.bind(this), - null, - true, - CRON_TIMEZONE, - ); - await this.expireTaskSessions(); - - /* - * Bull Queue job processors - */ - - // this.log.info('registering queue job processor', { config: this.config.jobQueues.links }); - // this.linksProcessingQueue = this.services.jobQueue.getJobQueue('links', this.config.jobQueues.links); - // this.linksProcessingQueue.process('link-ingest', 1, this.ingestLink.bind(this)); - - this.log.info('Tracker Monitor online'); - } - - async shutdown ( ) { - this.log.alert('ChatLinksWorker shutting down'); - await super.shutdown(); - } - - async expireTaskSessions ( ) { - const { task: taskService } = this.services; - - const NOW = new Date(); - const oldestDate = dayjs(NOW).subtract(10, 'minute'); - - this.log.debug('scanning for defunct sessions'); - await this.TaskSession - .find({ - $and: [ - { status: { $in: ['active', 'reconnecting'] } }, - { lastUpdated: { $lt: oldestDate } }, - ], - }) - .cursor() - .eachAsync(async (session) => { - this.log.info('expiring defunct task work session', { - session: { - _id: session._id, - created: session.created, - lastUpdated: session.lastUpdated, - }, - }); - taskService.closeTaskSession(session, 'expired'); - }); - } -} - -(async ( ) => { - - try { - const { fileURLToPath } = await import('node:url'); - const __dirname = dirname(fileURLToPath(import.meta.url)); // jshint ignore:line - - const worker = new TrackerMonitorWorker(path.resolve(__dirname, '..', '..')); - await worker.start(); - - } catch (error) { - console.error('failed to start tracker monitor worker', { error }); - process.exit(-1); - } - -})(); \ No newline at end of file diff --git a/assets/icon/dtp-chat.app-icon.png b/assets/icon/dtp-chat.app-icon.png deleted file mode 100644 index a174aff..0000000 Binary files a/assets/icon/dtp-chat.app-icon.png and /dev/null differ diff --git a/assets/icon/dtp-chat.app-icon/icon-114x114.png b/assets/icon/dtp-chat.app-icon/icon-114x114.png deleted file mode 100644 index a0183fd..0000000 Binary files a/assets/icon/dtp-chat.app-icon/icon-114x114.png and /dev/null differ diff --git a/assets/icon/dtp-chat.app-icon/icon-120x120.png b/assets/icon/dtp-chat.app-icon/icon-120x120.png deleted file mode 100644 index 12465e2..0000000 Binary files a/assets/icon/dtp-chat.app-icon/icon-120x120.png and /dev/null differ diff --git a/assets/icon/dtp-chat.app-icon/icon-144x144.png b/assets/icon/dtp-chat.app-icon/icon-144x144.png deleted file mode 100644 index a429832..0000000 Binary files a/assets/icon/dtp-chat.app-icon/icon-144x144.png and /dev/null differ diff --git a/assets/icon/dtp-chat.app-icon/icon-150x150.png b/assets/icon/dtp-chat.app-icon/icon-150x150.png deleted file mode 100644 index ed9c2d4..0000000 Binary files a/assets/icon/dtp-chat.app-icon/icon-150x150.png and /dev/null differ diff --git a/assets/icon/dtp-chat.app-icon/icon-152x152.png b/assets/icon/dtp-chat.app-icon/icon-152x152.png deleted file mode 100644 index 82be507..0000000 Binary files a/assets/icon/dtp-chat.app-icon/icon-152x152.png and /dev/null differ diff --git a/assets/icon/dtp-chat.app-icon/icon-16x16.png b/assets/icon/dtp-chat.app-icon/icon-16x16.png deleted file mode 100644 index ee02676..0000000 Binary files a/assets/icon/dtp-chat.app-icon/icon-16x16.png and /dev/null differ diff --git a/assets/icon/dtp-chat.app-icon/icon-180x180.png b/assets/icon/dtp-chat.app-icon/icon-180x180.png deleted file mode 100644 index ecf2e7f..0000000 Binary files a/assets/icon/dtp-chat.app-icon/icon-180x180.png and /dev/null differ diff --git a/assets/icon/dtp-chat.app-icon/icon-192x192.png b/assets/icon/dtp-chat.app-icon/icon-192x192.png deleted file mode 100644 index a0d8f54..0000000 Binary files a/assets/icon/dtp-chat.app-icon/icon-192x192.png and /dev/null differ diff --git a/assets/icon/dtp-chat.app-icon/icon-256x256.png b/assets/icon/dtp-chat.app-icon/icon-256x256.png deleted file mode 100644 index fbd23cd..0000000 Binary files a/assets/icon/dtp-chat.app-icon/icon-256x256.png and /dev/null differ diff --git a/assets/icon/dtp-chat.app-icon/icon-310x310.png b/assets/icon/dtp-chat.app-icon/icon-310x310.png deleted file mode 100644 index 8b34bf7..0000000 Binary files a/assets/icon/dtp-chat.app-icon/icon-310x310.png and /dev/null differ diff --git a/assets/icon/dtp-chat.app-icon/icon-32x32.png b/assets/icon/dtp-chat.app-icon/icon-32x32.png deleted file mode 100644 index da88e0c..0000000 Binary files a/assets/icon/dtp-chat.app-icon/icon-32x32.png and /dev/null differ diff --git a/assets/icon/dtp-chat.app-icon/icon-36x36.png b/assets/icon/dtp-chat.app-icon/icon-36x36.png deleted file mode 100644 index f73b4c6..0000000 Binary files a/assets/icon/dtp-chat.app-icon/icon-36x36.png and /dev/null differ diff --git a/assets/icon/dtp-chat.app-icon/icon-384x384.png b/assets/icon/dtp-chat.app-icon/icon-384x384.png deleted file mode 100644 index 704436c..0000000 Binary files a/assets/icon/dtp-chat.app-icon/icon-384x384.png and /dev/null differ diff --git a/assets/icon/dtp-chat.app-icon/icon-48x48.png b/assets/icon/dtp-chat.app-icon/icon-48x48.png deleted file mode 100644 index 02c8574..0000000 Binary files a/assets/icon/dtp-chat.app-icon/icon-48x48.png and /dev/null differ diff --git a/assets/icon/dtp-chat.app-icon/icon-512x512.png b/assets/icon/dtp-chat.app-icon/icon-512x512.png deleted file mode 100644 index 9debbe7..0000000 Binary files a/assets/icon/dtp-chat.app-icon/icon-512x512.png and /dev/null differ diff --git a/assets/icon/dtp-chat.app-icon/icon-57x57.png b/assets/icon/dtp-chat.app-icon/icon-57x57.png deleted file mode 100644 index 349e689..0000000 Binary files a/assets/icon/dtp-chat.app-icon/icon-57x57.png and /dev/null differ diff --git a/assets/icon/dtp-chat.app-icon/icon-60x60.png b/assets/icon/dtp-chat.app-icon/icon-60x60.png deleted file mode 100644 index 43b02c7..0000000 Binary files a/assets/icon/dtp-chat.app-icon/icon-60x60.png and /dev/null differ diff --git a/assets/icon/dtp-chat.app-icon/icon-70x70.png b/assets/icon/dtp-chat.app-icon/icon-70x70.png deleted file mode 100644 index 87e8506..0000000 Binary files a/assets/icon/dtp-chat.app-icon/icon-70x70.png and /dev/null differ diff --git a/assets/icon/dtp-chat.app-icon/icon-72x72.png b/assets/icon/dtp-chat.app-icon/icon-72x72.png deleted file mode 100644 index ba5e38c..0000000 Binary files a/assets/icon/dtp-chat.app-icon/icon-72x72.png and /dev/null differ diff --git a/assets/icon/dtp-chat.app-icon/icon-76x76.png b/assets/icon/dtp-chat.app-icon/icon-76x76.png deleted file mode 100644 index cf2e3b9..0000000 Binary files a/assets/icon/dtp-chat.app-icon/icon-76x76.png and /dev/null differ diff --git a/assets/icon/dtp-chat.app-icon/icon-96x96.png b/assets/icon/dtp-chat.app-icon/icon-96x96.png deleted file mode 100644 index aaac378..0000000 Binary files a/assets/icon/dtp-chat.app-icon/icon-96x96.png and /dev/null differ diff --git a/client/img/app-icon.png b/client/img/app-icon.png index 1bb258e..2b11396 100644 Binary files a/client/img/app-icon.png and b/client/img/app-icon.png differ diff --git a/client/img/icon/icon-114x114.png b/client/img/icon/icon-114x114.png index dd8aba2..34e0c0a 100644 Binary files a/client/img/icon/icon-114x114.png and b/client/img/icon/icon-114x114.png differ diff --git a/client/img/icon/icon-120x120.png b/client/img/icon/icon-120x120.png index 5d6a5d4..ea70074 100644 Binary files a/client/img/icon/icon-120x120.png and b/client/img/icon/icon-120x120.png differ diff --git a/client/img/icon/icon-144x144.png b/client/img/icon/icon-144x144.png index 2f3ce51..0218fb6 100644 Binary files a/client/img/icon/icon-144x144.png and b/client/img/icon/icon-144x144.png differ diff --git a/client/img/icon/icon-150x150.png b/client/img/icon/icon-150x150.png index c80f261..b95c00d 100644 Binary files a/client/img/icon/icon-150x150.png and b/client/img/icon/icon-150x150.png differ diff --git a/client/img/icon/icon-152x152.png b/client/img/icon/icon-152x152.png index c2a418c..790db61 100644 Binary files a/client/img/icon/icon-152x152.png and b/client/img/icon/icon-152x152.png differ diff --git a/client/img/icon/icon-16x16.png b/client/img/icon/icon-16x16.png index f84c448..185998b 100644 Binary files a/client/img/icon/icon-16x16.png and b/client/img/icon/icon-16x16.png differ diff --git a/client/img/icon/icon-180x180.png b/client/img/icon/icon-180x180.png index 2b62c4e..725e24f 100644 Binary files a/client/img/icon/icon-180x180.png and b/client/img/icon/icon-180x180.png differ diff --git a/client/img/icon/icon-192x192.png b/client/img/icon/icon-192x192.png index 9ca324a..f0c698b 100644 Binary files a/client/img/icon/icon-192x192.png and b/client/img/icon/icon-192x192.png differ diff --git a/client/img/icon/icon-256x256.png b/client/img/icon/icon-256x256.png index c300431..646ea5a 100644 Binary files a/client/img/icon/icon-256x256.png and b/client/img/icon/icon-256x256.png differ diff --git a/client/img/icon/icon-310x310.png b/client/img/icon/icon-310x310.png index 78181ee..7ced692 100644 Binary files a/client/img/icon/icon-310x310.png and b/client/img/icon/icon-310x310.png differ diff --git a/client/img/icon/icon-32x32.png b/client/img/icon/icon-32x32.png index f43696e..7e8e035 100644 Binary files a/client/img/icon/icon-32x32.png and b/client/img/icon/icon-32x32.png differ diff --git a/client/img/icon/icon-36x36.png b/client/img/icon/icon-36x36.png index b90b1a9..9916eea 100644 Binary files a/client/img/icon/icon-36x36.png and b/client/img/icon/icon-36x36.png differ diff --git a/client/img/icon/icon-384x384.png b/client/img/icon/icon-384x384.png index 2635bc2..d8c032e 100644 Binary files a/client/img/icon/icon-384x384.png and b/client/img/icon/icon-384x384.png differ diff --git a/client/img/icon/icon-48x48.png b/client/img/icon/icon-48x48.png index c7a6d16..ff552dd 100644 Binary files a/client/img/icon/icon-48x48.png and b/client/img/icon/icon-48x48.png differ diff --git a/client/img/icon/icon-512x512.png b/client/img/icon/icon-512x512.png index 166fb65..ef625c9 100644 Binary files a/client/img/icon/icon-512x512.png and b/client/img/icon/icon-512x512.png differ diff --git a/client/img/icon/icon-57x57.png b/client/img/icon/icon-57x57.png index 971001f..4e1dd78 100644 Binary files a/client/img/icon/icon-57x57.png and b/client/img/icon/icon-57x57.png differ diff --git a/client/img/icon/icon-60x60.png b/client/img/icon/icon-60x60.png index 676d3cf..f6dfbb8 100644 Binary files a/client/img/icon/icon-60x60.png and b/client/img/icon/icon-60x60.png differ diff --git a/client/img/icon/icon-70x70.png b/client/img/icon/icon-70x70.png index 8251b9f..db32e6c 100644 Binary files a/client/img/icon/icon-70x70.png and b/client/img/icon/icon-70x70.png differ diff --git a/client/img/icon/icon-72x72.png b/client/img/icon/icon-72x72.png index ddf77de..bb9ba70 100644 Binary files a/client/img/icon/icon-72x72.png and b/client/img/icon/icon-72x72.png differ diff --git a/client/img/icon/icon-76x76.png b/client/img/icon/icon-76x76.png index 6422e45..153b45e 100644 Binary files a/client/img/icon/icon-76x76.png and b/client/img/icon/icon-76x76.png differ diff --git a/client/img/icon/icon-96x96.png b/client/img/icon/icon-96x96.png index a4cff5e..60dcce7 100644 Binary files a/client/img/icon/icon-96x96.png and b/client/img/icon/icon-96x96.png differ diff --git a/client/img/nav-icon.png b/client/img/nav-icon.png index 0c10246..83a94b4 100644 Binary files a/client/img/nav-icon.png and b/client/img/nav-icon.png differ diff --git a/client/js/time-tracker-audio.js b/client/js/base-audio.js similarity index 95% rename from client/js/time-tracker-audio.js rename to client/js/base-audio.js index cf13ce1..457f4dd 100644 --- a/client/js/time-tracker-audio.js +++ b/client/js/base-audio.js @@ -1,19 +1,17 @@ -// time-tracker-audio.js +// base-audio.js // Copyright (C) 2024 DTP Technologies, LLC // All Rights Reserved 'use strict'; -const DTP_COMPONENT_NAME = 'TimeTrackerAudio'; - import DtpLog from 'lib/dtp-log'; const AudioContext = window.AudioContext || window.webkitAudioContext; -export default class TimeTrackerAudio { +export default class BaseAudio { constructor ( ) { - this.log = new DtpLog(DTP_COMPONENT_NAME); + this.log = new DtpLog('BaseAudio'); } start ( ) { diff --git a/client/js/base-client.js b/client/js/base-client.js new file mode 100644 index 0000000..4a5e74e --- /dev/null +++ b/client/js/base-client.js @@ -0,0 +1,340 @@ +// base-client.js +// Copyright (C) 2024 DTP Technologies, LLC +// All Rights Reserved + +'use strict'; + +const dtp = window.dtp = window.dtp || { }; + +import DtpApp from 'lib/dtp-app.js'; +import BaseAudio from './base-audio.js'; + +import QRCode from 'qrcode'; +import Cropper from 'cropperjs'; + +import dayjs from 'dayjs'; +import dayjsRelativeTime from 'dayjs/plugin/relativeTime.js'; +dayjs.extend(dayjsRelativeTime); + +export class BaseApp extends DtpApp { + + static get SFX_SAMPLE_SOUND ( ) { return 'sample-sound'; } + + constructor (user) { + super('BaseApp', user); + + this.loadSettings(); + + this.notificationPermission = 'default'; + this.haveFocus = true; // hard to load the app w/o also being the focused app + + window.addEventListener('dtp-load', this.onDtpLoad.bind(this)); + window.addEventListener('focus', this.onWindowFocus.bind(this)); + window.addEventListener('blur', this.onWindowBlur.bind(this)); + + /* + * Page Visibility API hooks + */ + window.addEventListener('pageshow', this.onWindowPageShow.bind(this)); + window.addEventListener('pagehide', this.onWindowPageHide.bind(this)); + window.addEventListener('freeze', this.onWindowFreeze.bind(this)); + window.addEventListener('resume', this.onWindowResume.bind(this)); + + this.updateTimestamps(); + + this.log.info('constructor', 'BaseApp client online'); + } + + async onDtpLoad ( ) { + this.log.info('onDtpLoad', 'dtp-load event received. Connecting to platform.'); + + await this.connect({ + mode: 'User', + onSocketConnect: this.onBaseSocketConnect.bind(this), + onSocketDisconnect: this.onBaseSocketDisconnect.bind(this), + }); + } + + async onWindowFocus (event) { + this.log.debug('onWindowFocus', 'window has received focus', { event }); + this.haveFocus = true; + } + + async onWindowBlur (event) { + this.log.debug('onWindowBlur', 'window has lost focus', { event }); + this.haveFocus = false; + } + + async onWindowPageShow (event) { + this.log.debug('onWindowPageShow', 'the page is being shown', { event }); + } + + async onWindowPageHide (event) { + this.log.debug('onWindowPageHide', 'the page is being hidden', { event }); + if (!event.persisted) { + await this.socket.disconnect(); + } + } + + async onWindowFreeze (event) { + this.log.debug('onWindowFreeze', 'the page is being frozen', { event }); + } + + async onWindowResume (event) { + this.log.debug('onWindowResume', 'the page is being resumed', { event }); + } + + async startAudio ( ) { + this.log.info('startAudio', 'starting audio'); + this.audio = new BaseAudio(); + this.audio.start(); + try { + await Promise.all([ + this.audio.loadSound(BaseApp.SFX_SAMPLE_SOUND, '/static/sfx/sample-sound.mp3'), + ]); + } catch (error) { + this.log.error('startAudio', 'failed to load sound', { error }); + // fall through + } + } + + async onBaseSocketConnect (socket) { + this.log.debug('onSocketConnect', 'attaching socket events'); + + this.systemMessageHandler = this.onSystemMessage.bind(this); + socket.on('system-message', this.systemMessageHandler); + } + + async onBaseSocketDisconnect (socket) { + this.log.debug('onSocketDisconnect', 'detaching socket events'); + + socket.off('system-message', this.systemMessageHandler); + delete this.systemMessageHandler; + + if (this.taskSession) { + await this.setTaskSessionStatus('reconnecting'); + } + } + + async onSystemMessage (message) { + if (message.displayList) { + this.displayEngine.executeDisplayList(message.displayList); + } + } + + async confirmNavigation (event) { + const target = event.currentTarget || event.target; + event.preventDefault(); + event.stopPropagation(); + + const href = target.getAttribute('href'); + const hrefTarget = target.getAttribute('target'); + const text = target.textContent; + const whitelist = [ + 'digitaltelepresence.com', + 'www.digitaltelepresence.com', + ]; + try { + const url = new URL(href); + if (!whitelist.includes(url.hostname)) { + await UIkit.modal.confirm(`

You are navigating to ${href}, a link or button that was displayed as:

${text}

Please only open links to destinations you trust and want to visit.
`); + } + window.open(href, hrefTarget); + } catch (error) { + this.log.info('confirmNavigation', 'navigation canceled', { error }); + } + + return true; + } + + async generateOtpQR (canvas, keyURI) { + QRCode.toCanvas(canvas, keyURI); + } + + async generateQRCanvas (canvas, uri) { + this.log.info('generateQRCanvas', 'creating QR code canvas', { uri }); + QRCode.toCanvas(canvas, uri, { width: 256 }); + } + + async closeAllDropdowns ( ) { + const dropdowns = document.querySelectorAll('.uk-dropdown.uk-open'); + for (const dropdown of dropdowns) { + this.log.info('closeAllDropdowns', 'closing dropdown', { dropdown }); + UIkit.dropdown(dropdown).hide(false); + } + } + + async initSettingsView ( ) { + this.log.info('initSettingsView', 'settings', { settings: this.settings }); + } + + loadSettings ( ) { + this.settings = { }; + if (window.localStorage) { + if (window.localStorage.settings) { + this.settings = JSON.parse(window.localStorage.settings); + } else { + this.saveSettings(); + } + } + } + + saveSettings ( ) { + if (!window.localStorage) { return; } + window.localStorage.settings = JSON.stringify(this.settings); + } + + async submitImageForm (event) { + event.preventDefault(); + event.stopPropagation(); + + const formElement = event.currentTarget || event.target; + const form = new FormData(formElement); + + this.cropper.getCroppedCanvas().toBlob(async (imageData) => { + try { + const imageId = formElement.getAttribute('data-image-id'); + form.append('imageFile', imageData, imageId); + + this.log.info('submitImageForm', 'uploading image', { event, action: formElement.action }); + const response = await fetch(formElement.action, { + method: formElement.method, + body: form, + }); + await this.processResponse(response); + } catch (error) { + UIkit.modal.alert(`Failed to upload image: ${error.message}`); + } + }); + + return; + } + + async selectImageFile (event) { + event.preventDefault(); + + const imageId = event.target.getAttribute('data-image-id'); + + let cropperOptions = event.target.getAttribute('data-cropper-options'); + if (cropperOptions) { + cropperOptions = JSON.parse(cropperOptions); + } + + const fileSelectContainerId = event.target.getAttribute('data-file-select-container'); + if (!fileSelectContainerId) { + UIkit.modal.alert('Missing file select container element ID information'); + return; + } + + const fileSelectContainer = document.getElementById(fileSelectContainerId); + if (!fileSelectContainer) { + UIkit.modal.alert('Missing file select element'); + return; + } + + const fileSelect = fileSelectContainer.querySelector('input[type="file"]'); + if (!fileSelect.files || (fileSelect.files.length === 0)) { + return; + } + + const selectedFile = fileSelect.files[0]; + if (!selectedFile) { + return; + } + + this.log.debug('selectImageFile', 'thumbnail file select', { event, selectedFile }); + + const filter = /^(image\/jpg|image\/jpeg|image\/png)$/i; + if (!filter.test(selectedFile.type)) { + UIkit.modal.alert(`Unsupported image file type selected: ${selectedFile.type}`); + return; + } + + const fileSizeId = event.target.getAttribute('data-file-size-element'); + const FILE_MAX_SIZE = parseInt(fileSelect.getAttribute('data-file-max-size'), 10); + const fileSize = document.getElementById(fileSizeId); + fileSize.textContent = numeral(selectedFile.size).format('0,0.0b'); + if (selectedFile.size > (FILE_MAX_SIZE)) { + UIkit.modal.alert(`File is too large: ${fileSize.textContent}. Custom thumbnail images may be up to ${numeral(FILE_MAX_SIZE).format('0,0.00b')} in size.`); + return; + } + + const reader = new FileReader(); + reader.onload = (e) => { + const img = document.getElementById(imageId); + img.onload = (e) => { + console.log('image loaded', e, img.naturalWidth, img.naturalHeight); + + fileSelectContainer.querySelector('#file-name').textContent = selectedFile.name; + fileSelectContainer.querySelector('#file-modified').textContent = dayjs(selectedFile.lastModifiedDate).fromNow(); + fileSelectContainer.querySelector('#image-resolution-w').textContent = img.naturalWidth.toString(); + fileSelectContainer.querySelector('#image-resolution-h').textContent = img.naturalHeight.toString(); + + fileSelectContainer.querySelector('#file-select').setAttribute('hidden', true); + fileSelectContainer.querySelector('#file-info').removeAttribute('hidden'); + + fileSelectContainer.querySelector('#file-save-btn').removeAttribute('hidden'); + }; + + img.src = e.target.result; + this.createImageCropper(img, cropperOptions); + }; + + reader.readAsDataURL(selectedFile); + } + + async createImageCropper (img, options) { + // https://github.com/fengyuanchen/cropperjs/blob/main/README.md#options + options = Object.assign({ + aspectRatio: 1, + viewMode: 1, // restrict the crop box not to exceed the size of the canvas + dragMode: 'move', + autoCropArea: 0.85, + restore: false, + guides: false, + center: false, + highlight: false, + cropBoxMovable: true, + cropBoxResizable: true, + toggleDragModeOnDblclick: false, + modal: true, + }, options); + this.log.info("createImageCropper", "Creating image cropper", { img }); + this.cropper = new Cropper(img, options); + } + + async removeImageFile (event) { + const target = event.target || event.currentTarget; + const imageType = target.getAttribute('data-image-type'); + + try { + this.log.info('removeImageFile', 'request to remove image', event); + + let imageUrl; + switch (imageType) { + case 'profile-picture-file': + imageUrl = `/user/${this.user._id}/profile-photo`; + break; + + default: + throw new Error(`Invalid image type: ${imageType}`); + } + + const response = await fetch(imageUrl, { method: 'DELETE' }); + if (!response.ok) { + throw new Error('Server error'); + } + + await this.processResponse(response); + } catch (error) { + this.log.error('removeImageFile', 'failed to remove image', { error }); + UIkit.modal.alert(`Failed to remove image: ${error.message}`); + } + } + + async onWindowResize ( ) { + if (this.chat.messageList && this.chat.isAtBottom) { + this.chat.messageList.scrollTo(0, this.chat.messageList.scrollHeight + 50000); + } + } +} \ No newline at end of file diff --git a/client/js/index.js b/client/js/index.js index d6b870d..863ceba 100644 --- a/client/js/index.js +++ b/client/js/index.js @@ -7,14 +7,14 @@ const DTP_COMPONENT_NAME = 'DtpChat'; const dtp = window.dtp = window.dtp || { }; -import { TimeTrackerApp } from './time-tracker-client.js'; +import { BaseApp } from './base-client.js'; import DtpWebLog from 'lib/dtp-log.js'; window.addEventListener('load', async ( ) => { dtp.log = new DtpWebLog(DTP_COMPONENT_NAME); dtp.env = document.body.getAttribute('data-dtp-env'); - dtp.app = new TimeTrackerApp(dtp.user); + dtp.app = new BaseApp(dtp.user); dtp.log.info('load handler', 'application instance created', { env: dtp.env }); await dtp.app.startAudio(); diff --git a/client/js/time-tracker-client.js b/client/js/time-tracker-client.js deleted file mode 100644 index fb552c5..0000000 --- a/client/js/time-tracker-client.js +++ /dev/null @@ -1,652 +0,0 @@ -// time-tracker-client.js -// Copyright (C) 2024 DTP Technologies, LLC -// All Rights Reserved - -'use strict'; - -const DTP_COMPONENT_NAME = 'TimeTrackerApp'; -const dtp = window.dtp = window.dtp || { }; - -import DtpApp from 'lib/dtp-app.js'; -import TimeTrackerAudio from './time-tracker-audio.js'; - -import QRCode from 'qrcode'; -import Cropper from 'cropperjs'; - -import dayjs from 'dayjs'; -import dayjsRelativeTime from 'dayjs/plugin/relativeTime.js'; -dayjs.extend(dayjsRelativeTime); - -export class TimeTrackerApp extends DtpApp { - - static get SCREENSHOT_INTERVAL ( ) { return 1000 * 60 * 10; } - - static get SFX_TRACKER_START ( ) { return 'tracker-start'; } - static get SFX_TRACKER_UPDATE ( ) { return 'tracker-update'; } - static get SFX_TRACKER_STOP ( ) { return 'tracker-stop'; } - - constructor (user) { - super(DTP_COMPONENT_NAME, user); - this.loadSettings(); - this.log.info('constructor', 'TimeTrackerApp client online'); - - this.notificationPermission = 'default'; - this.haveFocus = true; // hard to load the app w/o also being the focused app - - this.capturePreview = document.querySelector('video#capture-preview'); - this.dragFeedback = document.querySelector('.dtp-drop-feedback'); - - this.currentSessionStartTime = null; - this.currentSessionDuration = document.querySelector('#current-session-duration'); - this.currentSessionTimeRemaining = document.querySelector('#time-remaining'); - - window.addEventListener('dtp-load', this.onDtpLoad.bind(this)); - window.addEventListener('focus', this.onWindowFocus.bind(this)); - window.addEventListener('blur', this.onWindowBlur.bind(this)); - - /* - * Page Visibility API hooks - */ - window.addEventListener('pageshow', this.onWindowPageShow.bind(this)); - window.addEventListener('pagehide', this.onWindowPageHide.bind(this)); - window.addEventListener('freeze', this.onWindowFreeze.bind(this)); - window.addEventListener('resume', this.onWindowResume.bind(this)); - - this.updateTimestamps(); - } - - async onDtpLoad ( ) { - this.log.info('onDtpLoad', 'dtp-load event received. Connecting to platform.'); - - await this.connect({ - mode: 'User', - onSocketConnect: this.onTrackerSocketConnect.bind(this), - onSocketDisconnect: this.onTrackerSocketDisconnect.bind(this), - }); - } - - async onWindowFocus (event) { - this.log.debug('onWindowFocus', 'window has received focus', { event }); - this.haveFocus = true; - } - - async onWindowBlur (event) { - this.log.debug('onWindowBlur', 'window has lost focus', { event }); - this.haveFocus = false; - } - - async onWindowPageShow (event) { - this.log.debug('onWindowPageShow', 'the page is being shown', { event }); - } - - async onWindowPageHide (event) { - this.log.debug('onWindowPageHide', 'the page is being hidden', { event }); - if (!event.persisted) { - await this.socket.disconnect(); - } - } - - async onWindowFreeze (event) { - this.log.debug('onWindowFreeze', 'the page is being frozen', { event }); - } - - async onWindowResume (event) { - this.log.debug('onWindowResume', 'the page is being resumed', { event }); - } - - async startAudio ( ) { - this.log.info('startAudio', 'starting audio'); - this.audio = new TimeTrackerAudio(); - this.audio.start(); - try { - await Promise.all([ - this.audio.loadSound(TimeTrackerApp.SFX_TRACKER_START, '/static/sfx/tracker-start.mp3'), - this.audio.loadSound(TimeTrackerApp.SFX_TRACKER_UPDATE, '/static/sfx/tracker-update.mp3'), - this.audio.loadSound(TimeTrackerApp.SFX_TRACKER_STOP, '/static/sfx/tracker-stop.mp3'), - ]); - } catch (error) { - this.log.error('startAudio', 'failed to load sound', { error }); - // fall through - } - } - - async onDragEnter (event) { - event.preventDefault(); - event.stopPropagation(); - - this.log.info('onDragEnter', 'something being dragged has entered the stage', { event }); - this.dragFeedback.classList.add('feedback-active'); - } - - async onDragLeave (event) { - event.preventDefault(); - event.stopPropagation(); - - this.log.info('onDragLeave', 'something being dragged has left the stage', { event }); - this.dragFeedback.classList.remove('feedback-active'); - } - - async onDragOver (event) { - /* - * Inform that we want "copy" as a drop effect and prevent all default - * processing so we'll actually get the files in the drop event. If this - * isn't done, you simply won't get the files in the drop. - */ - event.preventDefault(); - event.stopPropagation(); // this ends now! - event.dataTransfer.dropEffect = 'copy'; - - // this.log.info('onDragOver', 'something was dragged over the stage', { event }); - this.dragFeedback.classList.add('feedback-active'); - } - - async onDrop (event) { - event.preventDefault(); - event.stopPropagation(); - - for (const file of event.dataTransfer.files) { - this.log.info('onDrop', 'a file has been dropped', { file }); - } - this.log.info('onFileDrop', 'something was dropped on the stage', { event, files: event.files }); - this.dragFeedback.classList.remove('feedback-active'); - } - - async onTrackerSocketConnect (socket) { - this.log.debug('onSocketConnect', 'attaching socket events'); - - this.systemMessageHandler = this.onSystemMessage.bind(this); - socket.on('system-message', this.systemMessageHandler); - - this.sessionControlHandler = this.onSessionControl.bind(this); - socket.on('session-control', this.sessionControlHandler); - - if (dtp.task) { - await this.socket.joinChannel(dtp.task._id, 'Task'); - } - if (this.taskSession) { - await this.setTaskSessionStatus('active'); - } - } - - async onTrackerSocketDisconnect (socket) { - this.log.debug('onSocketDisconnect', 'detaching socket events'); - - socket.off('session-control', this.sessionControlHandler); - delete this.sessionControlHandler; - - socket.off('system-message', this.systemMessageHandler); - delete this.systemMessageHandler; - - if (this.taskSession) { - await this.setTaskSessionStatus('reconnecting'); - } - } - - async onSystemMessage (message) { - if (message.displayList) { - this.displayEngine.executeDisplayList(message.displayList); - } - } - - async onSessionControl (message) { - const activityToggle = document.querySelector(''); - if (message.cmd) { - switch (message.cmd) { - case 'end-session': - try { - await this.closeTaskSession(); - activityToggle.checked = false; - } catch (error) { - this.log.error('onSessionControl', 'failed to close task work session', { error }); - return; - } - break; - - default: - this.log.error('onSessionControl', 'invalid command received', { cmd: message.cmd }); - return; - } - } - if (message.displayList) { - this.displayEngine.executeDisplayList(message.displayList); - } - } - - async confirmNavigation (event) { - const target = event.currentTarget || event.target; - event.preventDefault(); - event.stopPropagation(); - - const href = target.getAttribute('href'); - const hrefTarget = target.getAttribute('target'); - const text = target.textContent; - const whitelist = [ - 'chat.digitaltelepresence.com', - 'digitaltelepresence.com', - 'sites.digitaltelepresence.com', - 'tracker.digitaltelepresence.com', - 'www.digitaltelepresence.com', - ]; - try { - const url = new URL(href); - if (!whitelist.includes(url.hostname)) { - await UIkit.modal.confirm(`

You are navigating to ${href}, a link or button that was displayed as:

${text}

Please only open links to destinations you trust and want to visit.
`); - } - window.open(href, hrefTarget); - } catch (error) { - this.log.info('confirmNavigation', 'navigation canceled', { error }); - } - - return true; - } - - async performSessionNavigation (event) { - const target = event.currentTarget || event.target; - - event.preventDefault(); - event.stopPropagation(); - - const href = target.getAttribute('href'); - const hrefTarget = target.getAttribute('target'); - if (this.taskSession || (hrefTarget && (hrefTarget.length > 0))) { - return window.open(href, hrefTarget); - } - - window.location = href; - return true; - } - - async generateOtpQR (canvas, keyURI) { - QRCode.toCanvas(canvas, keyURI); - } - - async generateQRCanvas (canvas, uri) { - this.log.info('generateQRCanvas', 'creating QR code canvas', { uri }); - QRCode.toCanvas(canvas, uri, { width: 256 }); - } - - async closeAllDropdowns ( ) { - const dropdowns = document.querySelectorAll('.uk-dropdown.uk-open'); - for (const dropdown of dropdowns) { - this.log.info('closeAllDropdowns', 'closing dropdown', { dropdown }); - UIkit.dropdown(dropdown).hide(false); - } - } - - async initSettingsView ( ) { - this.log.info('initSettingsView', 'settings', { settings: this.settings }); - } - - loadSettings ( ) { - this.settings = { }; - if (window.localStorage) { - if (window.localStorage.settings) { - this.settings = JSON.parse(window.localStorage.settings); - } else { - this.saveSettings(); - } - } - } - - saveSettings ( ) { - if (!window.localStorage) { return; } - window.localStorage.settings = JSON.stringify(this.settings); - } - - async submitImageForm (event) { - event.preventDefault(); - event.stopPropagation(); - - const formElement = event.currentTarget || event.target; - const form = new FormData(formElement); - - this.cropper.getCroppedCanvas().toBlob(async (imageData) => { - try { - const imageId = formElement.getAttribute('data-image-id'); - form.append('imageFile', imageData, imageId); - - this.log.info('submitImageForm', 'uploading image', { event, action: formElement.action }); - const response = await fetch(formElement.action, { - method: formElement.method, - body: form, - }); - await this.processResponse(response); - } catch (error) { - UIkit.modal.alert(`Failed to upload image: ${error.message}`); - } - }); - - return; - } - - async selectImageFile (event) { - event.preventDefault(); - - const imageId = event.target.getAttribute('data-image-id'); - - //z read the cropper options from the element on the page - let cropperOptions = event.target.getAttribute('data-cropper-options'); - if (cropperOptions) { - cropperOptions = JSON.parse(cropperOptions); - } - this.log.debug('selectImageFile', 'cropper options', { cropperOptions }); //z remove when done - - const fileSelectContainerId = event.target.getAttribute('data-file-select-container'); - if (!fileSelectContainerId) { - UIkit.modal.alert('Missing file select container element ID information'); - return; - } - - const fileSelectContainer = document.getElementById(fileSelectContainerId); - if (!fileSelectContainer) { - UIkit.modal.alert('Missing file select element'); - return; - } - - const fileSelect = fileSelectContainer.querySelector('input[type="file"]'); - if (!fileSelect.files || (fileSelect.files.length === 0)) { - return; - } - - const selectedFile = fileSelect.files[0]; - if (!selectedFile) { - return; - } - - this.log.debug('selectImageFile', 'thumbnail file select', { event, selectedFile }); - - const filter = /^(image\/jpg|image\/jpeg|image\/png)$/i; - if (!filter.test(selectedFile.type)) { - UIkit.modal.alert(`Unsupported image file type selected: ${selectedFile.type}`); - return; - } - - const fileSizeId = event.target.getAttribute('data-file-size-element'); - const FILE_MAX_SIZE = parseInt(fileSelect.getAttribute('data-file-max-size'), 10); - const fileSize = document.getElementById(fileSizeId); - fileSize.textContent = numeral(selectedFile.size).format('0,0.0b'); - if (selectedFile.size > (FILE_MAX_SIZE)) { - UIkit.modal.alert(`File is too large: ${fileSize.textContent}. Custom thumbnail images may be up to ${numeral(FILE_MAX_SIZE).format('0,0.00b')} in size.`); - return; - } - - // const IMAGE_WIDTH = parseInt(event.target.getAttribute('data-image-w')); - // const IMAGE_HEIGHT = parseInt(event.target.getAttribute('data-image-h')); - - const reader = new FileReader(); - reader.onload = (e) => { - const img = document.getElementById(imageId); - img.onload = (e) => { - console.log('image loaded', e, img.naturalWidth, img.naturalHeight); - // if (img.naturalWidth !== IMAGE_WIDTH || img.naturalHeight !== IMAGE_HEIGHT) { - // UIkit.modal.alert(`This image must be ${IMAGE_WIDTH}x${IMAGE_HEIGHT}`); - // img.setAttribute('hidden', ''); - // img.src = ''; - // return; - // } - - fileSelectContainer.querySelector('#file-name').textContent = selectedFile.name; - fileSelectContainer.querySelector('#file-modified').textContent = dayjs(selectedFile.lastModifiedDate).fromNow(); - fileSelectContainer.querySelector('#image-resolution-w').textContent = img.naturalWidth.toString(); - fileSelectContainer.querySelector('#image-resolution-h').textContent = img.naturalHeight.toString(); - - fileSelectContainer.querySelector('#file-select').setAttribute('hidden', true); - fileSelectContainer.querySelector('#file-info').removeAttribute('hidden'); - - fileSelectContainer.querySelector('#file-save-btn').removeAttribute('hidden'); - }; - - // set the image as the "src" of the in the DOM. - img.src = e.target.result; - - //z create cropper and set options here - this.createImageCropper(img, cropperOptions); - }; - - // read in the file, which will trigger everything else in the event handler above. - reader.readAsDataURL(selectedFile); - } - - async createImageCropper (img, options) { - // https://github.com/fengyuanchen/cropperjs/blob/main/README.md#options - options = Object.assign({ - aspectRatio: 1, - viewMode: 1, // restrict the crop box not to exceed the size of the canvas - dragMode: 'move', - autoCropArea: 0.85, - restore: false, - guides: false, - center: false, - highlight: false, - cropBoxMovable: true, - cropBoxResizable: true, - toggleDragModeOnDblclick: false, - modal: true, - }, options); - this.log.info("createImageCropper", "Creating image cropper", { img }); - this.cropper = new Cropper(img, options); - } - - async removeImageFile (event) { - const target = event.target || event.currentTarget; - const imageType = target.getAttribute('data-image-type'); - const channelId = dtp.channel ? dtp.channel._id : dtp.channel; - - try { - this.log.info('removeImageFile', 'request to remove image', event); - - let imageUrl; - switch (imageType) { - case 'channel-thumbnail-file': - imageUrl = `/channel/${channelId}/thumbnail`; - break; - - case 'profile-picture-file': - imageUrl = `/user/${this.user._id}/profile-photo`; - break; - - default: - throw new Error(`Invalid image type: ${imageType}`); - } - - const response = await fetch(imageUrl, { method: 'DELETE' }); - if (!response.ok) { - throw new Error('Server error'); - } - - await this.processResponse(response); - } catch (error) { - this.log.error('removeImageFile', 'failed to remove image', { error }); - UIkit.modal.alert(`Failed to remove image: ${error.message}`); - } - } - - async onWindowResize ( ) { - if (this.chat.messageList && this.chat.isAtBottom) { - this.chat.messageList.scrollTo(0, this.chat.messageList.scrollHeight + 50000); - } - } - - async taskActivityToggle (event) { - const target = event.currentTarget || event.target; - event.preventDefault(); - event.stopPropagation(); - - try { - if (target.checked) { - await this.startScreenCapture(); - return this.startTaskSession(); - } - await this.stopScreenCapture(); - this.closeTaskSession(); - } catch (error) { - if (target.checked) { - target.checked = false; - } - this.log.error('taskActivityToggle', 'failed to start task work session', { error }); - UIkit.modal.alert(`Failed to start work session: ${error.message}`); - } - } - - async startTaskSession ( ) { - try { - const url = `/task/${dtp.task._id}/session/start`; - const response = await fetch(url, { method: 'POST' }); - await this.checkResponse(response); - - const json = await response.json(); - if (!json.success) { - throw new Error(json.message); - } - - this.taskSession = json.session; - this.currentSessionStartTime = new Date(); - - this.screenshotInterval = setInterval(this.captureScreenshot.bind(this), TimeTrackerApp.SCREENSHOT_INTERVAL); - this.sessionDisplayUpdateInterval = setInterval(this.updateSessionDisplay.bind(this), 250); - this.currentSessionDuration.classList.add('uk-text-success'); - - this.log.info('startTaskSession', 'task session started', { session: this.taskSession }); - } catch (error) { - this.log.error('startTaskSession', 'failed to start task session', { error }); - UIkit.modal.alert(`Failed to start task session: ${error.message}`); - throw new Error('failed to start task session', { cause: error }); - } - } - - async setTaskSessionStatus (status) { - try { - const url = `/task/${dtp.task._id}/session/${this.taskSession._id}/status`; - const body = JSON.stringify({ status }); - const response = await fetch(url, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Content-Length': body.length, - }, - body, - }); - await this.processResponse(response); - this.taskSession.status = status; - } catch (error) { - UIkit.notification({ - message: `Failed to update task session status: ${error.message}`, - status: 'danger', - pos: 'bottom-center', - timeout: 5000, - }); - } - } - - async closeTaskSession ( ) { - try { - const url = `/task/${dtp.task._id}/session/${this.taskSession._id}/close`; - const response = await fetch(url , { method: 'POST' }); - await this.processResponse(response); - - clearInterval(this.sessionDisplayUpdateInterval); - delete this.sessionDisplayUpdateInterval; - - clearInterval(this.screenshotInterval); - delete this.screenshotInterval; - - this.currentSessionDuration.classList.remove('uk-text-success'); - delete this.currentSessionStartTime; - } catch (error) { - this.log.error('closeTaskSession', 'failed to close task session', { - session: this.taskSession, - error, - }); - UIkit.modal.alert(`Failed to start task session: ${error.message}`); - throw new Error('failed to close task session', { cause: error }); - } - } - - async startScreenCapture ( ) { - this.captureStream = await navigator.mediaDevices.getDisplayMedia({ video: true, audio: false }); - this.capturePreview.srcObject = this.captureStream; - this.capturePreview.play(); - } - - async stopScreenCapture ( ) { - if (!this.captureStream) { - return; - } - - this.capturePreview.pause(); - this.capturePreview.srcObject = null; - - this.captureStream.getTracks().forEach(track => track.stop()); - delete this.captureStream; - } - - async updateSessionDisplay ( ) { - if (this.taskSession.status === 'reconnecting') { - this.currentSessionDuration.textContent = '---'; - this.currentSessionTimeRemaining.textContent = '---'; - return; - } - - const NOW = new Date(); - const duration = dayjs(NOW).diff(this.currentSessionStartTime, 'second'); - this.currentSessionDuration.textContent = numeral(duration).format('HH:MM:SS'); - - const timeRemaining = - this.taskSession.client.hoursLimit - - ((this.taskSession.client.weeklyTotals.timeWorked + duration) / 3600) - ; - this.currentSessionTimeRemaining.textContent = numeral(timeRemaining).format('0,0.00'); - } - - async captureScreenshot ( ) { - if (!this.captureStream || !this.taskSession) { - return; - } - try { - const tracks = this.captureStream.getVideoTracks(); - const constraints = tracks[0].getSettings(); - - this.log.info('startScreenCapture', 'creating capture canvas', { - width: constraints.width, - height: constraints.height, - }); - - const captureCanvas = document.createElement('canvas'); - captureCanvas.width = constraints.width; - captureCanvas.height = constraints.height; - - const captureContext = captureCanvas.getContext('2d'); - - /* - * Capture the current preview stream frame to the capture canvas - */ - captureContext.drawImage( - this.capturePreview, - 0, 0, - captureCanvas.width, - captureCanvas.height, - ); - /* - * Generate a PNG Blob from the capture canvas - */ - captureCanvas.toBlob( - async (blob) => { - const formData = new FormData(); - formData.append('image', blob, 'screenshot.png'); - - const url = `/task/${dtp.task._id}/session/${this.taskSession._id}/screenshot`; - const response = await fetch(url, { - method: 'POST', - body: formData, - }); - - await this.processResponse(response); - this.log.info('captureScreenshot', 'screenshot posted to task session'); - }, - 'image/png', - ); - } catch (error) { - this.log.error('captureScreenshot', 'failed to capture screenshot', { error }); - } - } -} \ No newline at end of file diff --git a/client/static/sfx/tracker-start.mp3 b/client/static/sfx/sample-sound.mp3 similarity index 100% rename from client/static/sfx/tracker-start.mp3 rename to client/static/sfx/sample-sound.mp3 diff --git a/client/static/sfx/tracker-stop.mp3 b/client/static/sfx/tracker-stop.mp3 deleted file mode 100644 index a1d8a50..0000000 Binary files a/client/static/sfx/tracker-stop.mp3 and /dev/null differ diff --git a/client/static/sfx/tracker-update.mp3 b/client/static/sfx/tracker-update.mp3 deleted file mode 100644 index bf4a561..0000000 Binary files a/client/static/sfx/tracker-update.mp3 and /dev/null differ diff --git a/config/limiter.js b/config/limiter.js index 8c74001..b982012 100644 --- a/config/limiter.js +++ b/config/limiter.js @@ -67,62 +67,6 @@ export default { }, }, - /* - * ClientController - */ - client: { - postProjectUpdate: { - total: 100, - expire: ONE_HOUR, - message: "You are updating projects too quickly", - }, - postProjectCreate: { - total: 10, - expire: ONE_HOUR, - message: "You are creating projects too quickly", - }, - postClientUpdate: { - total: 10, - expire: ONE_HOUR, - message: "You are updating clients too quickly", - }, - postClientCreate: { - total: 10, - expire: ONE_HOUR, - message: "You are creating clients too quickly", - }, - getProjectCreate: { - total: 30, - expire: ONE_HOUR, - message: "You are creating projects too quickly", - }, - getProjectEditor: { - total: 250, - expire: ONE_HOUR, - message: "You are editing projects too quickly", - }, - getProjectView: { - total: 250, - expire: ONE_HOUR, - message: "You are viewing projects too quickly", - }, - getClientEditor: { - total: 100, - expire: ONE_HOUR, - message: "You are creating or editing clients too quickly", - }, - getClientView: { - total: 250, - expire: ONE_HOUR, - message: "You are viewing clients too quickly", - }, - getHome: { - total: 250, - expire: ONE_HOUR, - message: "You are accessing the clients dashboard too quickly", - }, - }, - /* * EmailController */ @@ -192,57 +136,6 @@ export default { } }, - /* - * TaskController - */ - task: { - postStartTaskSession: { - total: 12, - expire: ONE_HOUR, - message: 'You are starting task work sessions too quickly', - }, - postTaskSessionScreenshot: { - total: 20, - expire: ONE_HOUR, - message: 'You are uploading session screenshots too quickly', - }, - postTaskSessionStatus: { - total: 100, - expire: ONE_HOUR, - message: 'You are changing task work session status too quickly', - }, - postCloseTaskSession: { - total: 12, - expire: ONE_HOUR, - message: 'You are closing work sessions too quickly', - }, - postStartTask: { - total: 60, - expire: ONE_HOUR, - message: 'You are starting tasks too quickly', - }, - postCloseTask: { - total: 60, - expire: ONE_HOUR, - message: 'You are closing tasks too quickly', - }, - postCreateTask: { - total: 60, - expire: ONE_HOUR, - message: 'You are creating tasks too quickly', - }, - getTaskSessionView: { - total: 20, - expire: ONE_MINUTE, - message: 'You are opening sessions too quickly', - }, - getTaskView: { - total: 20, - expire: ONE_MINUTE, - message: 'You are opening tasks too quickly', - }, - }, - /* * UserController */ diff --git a/config/reserved-names.js b/config/reserved-names.js index b0cdb99..3424feb 100644 --- a/config/reserved-names.js +++ b/config/reserved-names.js @@ -1,5 +1,5 @@ // reserved-names.js -// Copyright (C) 2022,2023 DTP Technologies, LLC +// Copyright (C) 2024 DTP Technologies, LLC // All Rights Reserved 'use strict'; @@ -7,12 +7,9 @@ export default [ 'admin', 'auth', - 'client', 'email', 'image', 'manifest', - 'report', - 'task', 'user', 'video', 'welcome', diff --git a/data/patches/client-stats.js b/data/patches/client-stats.js deleted file mode 100644 index 614eaa5..0000000 --- a/data/patches/client-stats.js +++ /dev/null @@ -1,35 +0,0 @@ -'use strict'; - -/* globals db */ - -const clients = db.clients.find(); -while (clients.hasNext()) { - let timeWorked = 0, billable = 0; - - const client = clients.next(); - const sessions = db.tasksessions.find({ client: client._id }); - while (sessions.hasNext()) { - const session = sessions.next(); - timeWorked += session.duration; - billable += session.hourlyRate * (session.duration / 3600); - } - - /* - * Fix some JavaScript goofiness with numbers (round correctly to 2nd decimal - * digit). - */ - billable = Math.round((billable + Number.EPSILON) * 100) / 100; - - print(`client: ${client._id}:${client.name} time:${timeWorked} bill:${billable}`); - db.clients.updateOne( - { _id: client._id }, - { - $set: { - weeklyTotals: { - timeWorked, - billable, - }, - }, - }, - ); -} \ No newline at end of file diff --git a/dtp-time-tracker-cli.js b/dtp-base-cli.js similarity index 73% rename from dtp-time-tracker-cli.js rename to dtp-base-cli.js index 0627818..8dedaca 100644 --- a/dtp-time-tracker-cli.js +++ b/dtp-base-cli.js @@ -1,4 +1,4 @@ -// dtp-time-tracker-cli.js +// dtp-base-cli.js // Copyright (C) 2024 DTP Technologies, LLC // All Rights Reserved @@ -48,18 +48,6 @@ class SiteTerminalApp extends SiteRuntime { handler: this.revoke.bind(this), help: 'revoke [admin|moderator] username', }, - 'probe': { - handler: this.probeMediaFile.bind(this), - help: 'probe filename', - }, - 'transcode-mov': { - handler: this.transcodeMov.bind(this), - help: 'transcode-mov filename', - }, - 'transcode-gif': { - handler: this.transcodeGif.bind(this), - help: 'transcode-gif filename', - }, }; } @@ -152,45 +140,6 @@ class SiteTerminalApp extends SiteRuntime { break; } } - - async probeMediaFile (args) { - const { media: mediaService } = this.services; - const filename = args.shift(); - const probe = await mediaService.ffprobe(filename); - this.log.info('FFPROBE result', { probe }); - } - - async transcodeMov (args) { - const { video: videoService } = this.services; - - const filename = args.shift(); - const stat = await fs.promises.stat(filename); - const file = { - path: filename, - size: stat.size, - type: 'video/quicktime', - }; - - await videoService.transcodeMov(file); - - this.log.info('transcode output ready', { filename: file.path }); - } - - async transcodeGif (args) { - const { video: videoService } = this.services; - - const filename = args.shift(); - const stat = await fs.promises.stat(filename); - const file = { - path: filename, - size: stat.size, - type: 'image/gif', - }; - - await videoService.transcodeGif(file); - - this.log.info('transcode output ready', { filename: file.path }); - } } (async ( ) => { @@ -201,7 +150,7 @@ class SiteTerminalApp extends SiteRuntime { await app.start(); await app.run(process.argv.slice(2)); } catch (error) { - console.error('failed to start Time Tracker terminal application', error); + console.error('failed to start terminal interface', error); } finally { await app.shutdown(); process.nextTick(( ) => { diff --git a/dtp-time-tracker.js b/dtp-base.js similarity index 99% rename from dtp-time-tracker.js rename to dtp-base.js index c95893f..ae359bb 100644 --- a/dtp-time-tracker.js +++ b/dtp-base.js @@ -1,4 +1,4 @@ -// dtp-chat.js +// dtp-base.js // Copyright (C) 2024 DTP Technologies, LLC // All Rights Reserved diff --git a/lib/site-lib.js b/lib/site-lib.js index 4075b67..f9e7b3e 100644 --- a/lib/site-lib.js +++ b/lib/site-lib.js @@ -1,5 +1,5 @@ // site-lib.js -// Copyright (C) 2022,2024 DTP Technologies, LLC +// Copyright (C) 2024 DTP Technologies, LLC // All Rights Reserved 'use strict'; @@ -10,4 +10,5 @@ export { SiteError } from './site-error.js'; export { SiteLog } from './site-log.js'; export { SiteController } from './site-controller.js'; export { SiteService } from './site-service.js'; -export { SiteRuntime } from './site-runtime.js'; \ No newline at end of file +export { SiteRuntime } from './site-runtime.js'; +export { SiteUnzalgo } from './site-unzalgo.js'; \ No newline at end of file diff --git a/lib/site-unzalgo.js b/lib/site-unzalgo.js new file mode 100644 index 0000000..ce2d83f --- /dev/null +++ b/lib/site-unzalgo.js @@ -0,0 +1,119 @@ +// site-unzalgo.js +// Copyright (C) 2024 DTP Technologies, LLC +// All Rights Reserved + +'use strict'; + +import { percentile } from "stats-lite"; + +const categories = /[\p{Mn}\p{Me}]+/u; +const DEFAULT_DETECTION_THRESHOLD = 0.55; +const DEFAULT_TARGET_DENSITY = 0; +const compose = string => string.normalize("NFC"); +const decompose = string => string.normalize("NFD"); +const computeZalgoDensity = string => [...string].filter(character => categories.test(character)).length / Math.max(string.length, 1); +const clamp = x => Math.max(Math.min(x, 1), 0); + +export class SiteUnzalgo { + + /** + * Computes a score ∈ [0, 1] for every word in the input string. Each score represents the ratio of combining characters to total characters in a word. + * @param {string} string + * The input string for which to compute scores. + * @return {number[]} + * An array of scores where each score describes the Zalgo ratio of a word. + */ + static computeScores (string) { + const wordScores = []; + /** + * Trimming here allows us to return early. + * Without trimming, we risk dividing by `0` later when computing the score. + */ + if (!string.trim().length) { + wordScores.push(0); + } + else { + for (const word of decompose(string).split(/\s+/)) { + let banned = 0; + for (const character of word) { + if (categories.test(character)) { + ++banned; + } + } + const score = banned / word.length; + wordScores.push(score); + } + } + return wordScores; + } + + /** + * Determines if the string consists of Zalgo text. Note that the occurrence of a combining character is not enough to trigger the detection. Instead, it computes a ratio for the input string and checks if it exceeds a given threshold. Thus, internationalized strings aren't automatically classified as Zalgo text. + * @param {string} string + * A string for which a Zalgo text check is run. + * @param {number} detectionThreshold + * A threshold ∈ [0, 1]. The higher the threshold, the more combining characters are needed for it to be detected as Zalgo text. + * @return {boolean} + * Whether the string is a Zalgo text string. + */ + static isZalgo (string, detectionThreshold = DEFAULT_DETECTION_THRESHOLD) { + const wordScores = SiteUnzalgo.computeScores(string); + const totalScore = percentile(wordScores, 0.75); + return totalScore >= clamp(detectionThreshold); + } + + /** + * Removes all combining characters for every word in a string if the word is classified as Zalgo text. + * If `targetDensity` is specified, not all the Zalgo characters will be removed. Instead, they will be thinned out uniformly. + * @param {string} string + * A string for which combining characters are removed for every word whose Zalgo property is met. + * @param {object} options + * Options for cleaning. + * @param {number} [options.detectionThreshold=DEFAULT_DETECTION_THRESHOLD] + * A threshold ∈ [0, 1]. The higher the threshold, the more combining characters are needed for it to be detected as Zalgo text. + * @param {number} [options.targetDensity=DEFAULT_TARGET_DENSITY] + * A threshold ∈ [0, 1]. The higher the density, the more Zalgo characters will be part of the resulting string. The result is guaranteed to have a Zalgo-character density that is less than or equal to the one provided. + * @return {string} + * A cleaned, more readable string. + */ + static clean ( + string, + { + detectionThreshold = DEFAULT_DETECTION_THRESHOLD, + targetDensity = DEFAULT_TARGET_DENSITY + } = { }, + ) { + let cleaned = ""; + const effectiveTargetDensity = clamp(targetDensity); + for (const word of decompose(string).split(/(\s+)/)) { + if (SiteUnzalgo.isZalgo(word, detectionThreshold)) { + let cleanedWord = ""; + const letters = [...word].map(character => ({ + character, + isCandidate: categories.test(character) + })); + for (let i = 0; i < letters.length; ++i) { + const { + character, + isCandidate + } = letters[i]; + if (isCandidate) { + const admissionProjection = cleanedWord + word.substr(i); + const omissionProjection = cleanedWord + word.substr(i + 1); + const admissionDistance = effectiveTargetDensity - computeZalgoDensity(admissionProjection); + const omissionDistance = effectiveTargetDensity - computeZalgoDensity(omissionProjection); + if (Math.abs(omissionDistance) <= Math.abs(admissionDistance)) { + continue; + } + } + cleanedWord += character; + } + cleaned += cleanedWord; + } + else { + cleaned += word; + } + } + return compose(cleaned); + } +} \ No newline at end of file diff --git a/nodemon.json b/nodemon.json index 44641af..b7345b3 100644 --- a/nodemon.json +++ b/nodemon.json @@ -8,4 +8,4 @@ "app/workers/**/*", "node_modules/**/*" ] -} \ No newline at end of file +} diff --git a/package.json b/package.json index 46da6be..beb47f2 100644 --- a/package.json +++ b/package.json @@ -1,14 +1,14 @@ { - "name": "dtp-time-tracker", + "name": "dtp-base", "type": "module", - "version": "1.0.21", + "version": "0.0.1", "description": "", - "main": "dtp-time-tracker.js", + "main": "dtp-base.js", "scripts": { - "dev": "nodemon dtp-time-tracker.js", + "dev": "nodemon dtp-base.js", "build": "NODE_ENV=production pnpm webpack --config webpack.config.js" }, - "repository": "git@git.digitaltelepresence.com:digital-telepresence/dtp-time-tracker.git", + "repository": "git@git.digitaltelepresence.com:digital-telepresence/dtp-base.git", "author": "Rob Colbert", "license": "LicenseRef-LICENSE", "private": true, @@ -38,7 +38,6 @@ "ioredis": "^5.3.2", "jsdom": "^24.0.0", "marked": "^12.0.1", - "mediasoup": "^3.13.24", "mime": "^4.0.1", "minio": "^7.1.3", "mongoose": "^8.3.1", @@ -59,11 +58,11 @@ "shoetest": "^1.2.2", "slug": "^9.0.0", "socket.io": "^4.7.5", + "stats-lite": "^2.2.0", "striptags": "^3.2.0", "svg-captcha": "^1.4.0", "systeminformation": "^5.22.7", "uikit": "^3.19.4", - "unzalgo": "^3.0.0", "user-agents": "^1.1.174", "uuid": "^9.0.1" }, @@ -85,4 +84,4 @@ "workbox-webpack-plugin": "^7.0.0" }, "packageManager": "pnpm@9.0.4+sha256.caa915eaae9d9aefccf50ee8aeda25a2f8684d8f9d5c6e367eaf176d97c1f89e" -} +} \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d4b19b1..43a0618 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -80,9 +80,6 @@ dependencies: marked: specifier: ^12.0.1 version: 12.0.1 - mediasoup: - specifier: ^3.13.24 - version: 3.14.1 mime: specifier: ^4.0.1 version: 4.0.1 @@ -143,6 +140,9 @@ dependencies: socket.io: specifier: ^4.7.5 version: 4.7.5 + stats-lite: + specifier: ^2.2.0 + version: 2.2.0 striptags: specifier: ^3.2.0 version: 3.2.0 @@ -155,9 +155,6 @@ dependencies: uikit: specifier: ^3.19.4 version: 3.19.4 - unzalgo: - specifier: ^3.0.0 - version: 3.0.0 user-agents: specifier: ^1.1.174 version: 1.1.178 @@ -262,7 +259,7 @@ packages: '@babel/traverse': 7.24.1 '@babel/types': 7.24.0 convert-source-map: 2.0.0 - debug: 4.3.4(supports-color@9.4.0) + debug: 4.3.4 gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 @@ -343,7 +340,7 @@ packages: '@babel/core': 7.24.4 '@babel/helper-compilation-targets': 7.23.6 '@babel/helper-plugin-utils': 7.24.0 - debug: 4.3.4(supports-color@9.4.0) + debug: 4.3.4 lodash.debounce: 4.0.8 resolve: 1.22.8 transitivePeerDependencies: @@ -1393,7 +1390,7 @@ packages: '@babel/helper-split-export-declaration': 7.22.6 '@babel/parser': 7.24.4 '@babel/types': 7.24.0 - debug: 4.3.4(supports-color@9.4.0) + debug: 4.3.4 globals: 11.12.0 transitivePeerDependencies: - supports-color @@ -1809,7 +1806,7 @@ packages: peerDependencies: socket.io-adapter: ^2.5.4 dependencies: - debug: 4.3.4(supports-color@9.4.0) + debug: 4.3.4 notepack.io: 3.0.1 socket.io-adapter: 2.5.4 uid2: 1.0.0 @@ -1820,7 +1817,7 @@ packages: /@socket.io/redis-emitter@5.1.0: resolution: {integrity: sha512-QQUFPBq6JX7JIuM/X1811ymKlAfwufnQ8w6G2/59Jaqp09hdF1GJ/+e8eo/XdcmT0TqkvcSa2TT98ggTXa5QYw==} dependencies: - debug: 4.3.4(supports-color@9.4.0) + debug: 4.3.4 notepack.io: 3.0.1 socket.io-parser: 4.2.4 transitivePeerDependencies: @@ -1844,12 +1841,6 @@ packages: dependencies: '@types/node': 20.12.7 - /@types/debug@4.1.12: - resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} - dependencies: - '@types/ms': 0.7.34 - dev: false - /@types/eslint-scope@3.7.7: resolution: {integrity: sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==} dependencies: @@ -1872,10 +1863,6 @@ packages: resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==} dev: true - /@types/ini@4.1.0: - resolution: {integrity: sha512-mTehMtc+xtnWBBvqizcqYCktKDBH2WChvx1GU3Sfe4PysFDXiNe+1YwtpVX1MDtCa4NQrSPw2+3HmvXHY3gt1w==} - dev: false - /@types/json-schema@7.0.15: resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} dev: true @@ -1884,10 +1871,6 @@ packages: resolution: {integrity: sha512-TifLZlFudklWlMBfhubvgqTXRzLDI5pCbGa4P8a3wPyUQSW+1xQ5eDsreP9DWHX3tjq1ke96uYG/nwundroWcA==} dev: false - /@types/ms@0.7.34: - resolution: {integrity: sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==} - dev: false - /@types/node@20.12.7: resolution: {integrity: sha512-wq0cICSkRLVaf3UGLMGItu/PtdY7oaXaI/RVU+xliKVOtRna3PRY57ZDfztpDL0n11vfymMUnXv8QwYCO7L1wg==} dependencies: @@ -2111,7 +2094,7 @@ packages: resolution: {integrity: sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==} engines: {node: '>= 14'} dependencies: - debug: 4.3.4(supports-color@9.4.0) + debug: 4.3.4 transitivePeerDependencies: - supports-color dev: false @@ -2607,11 +2590,6 @@ packages: fsevents: 2.3.3 dev: true - /chownr@2.0.0: - resolution: {integrity: sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==} - engines: {node: '>=10'} - dev: false - /chrome-trace-event@1.0.3: resolution: {integrity: sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==} engines: {node: '>=6.0'} @@ -2936,11 +2914,6 @@ packages: rrweb-cssom: 0.6.0 dev: false - /data-uri-to-buffer@4.0.1: - resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==} - engines: {node: '>= 12'} - dev: false - /data-urls@5.0.0: resolution: {integrity: sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==} engines: {node: '>=18'} @@ -3006,7 +2979,7 @@ packages: supports-color: 5.5.0 dev: true - /debug@4.3.4(supports-color@9.4.0): + /debug@4.3.4: resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} engines: {node: '>=6.0'} peerDependencies: @@ -3016,7 +2989,6 @@ packages: optional: true dependencies: ms: 2.1.2 - supports-color: 9.4.0 /decamelize@1.2.0: resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==} @@ -3220,7 +3192,7 @@ packages: resolution: {integrity: sha512-9Z0qLB0NIisTRt1DZ/8U2k12RJn8yls/nXMZLn+/N8hANT3TcYjKFKcwbw5zFQiN4NTde3TSY9zb79e1ij6j9Q==} dependencies: '@socket.io/component-emitter': 3.1.1 - debug: 4.3.4(supports-color@9.4.0) + debug: 4.3.4 engine.io-parser: 5.2.2 ws: 8.11.0 xmlhttprequest-ssl: 2.0.0 @@ -3245,7 +3217,7 @@ packages: base64id: 2.0.0 cookie: 0.4.2 cors: 2.8.5 - debug: 4.3.4(supports-color@9.4.0) + debug: 4.3.4 engine.io-parser: 5.2.2 ws: 8.11.0 transitivePeerDependencies: @@ -3540,14 +3512,6 @@ packages: engines: {node: '>= 4.9.1'} dev: true - /fetch-blob@3.2.0: - resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==} - engines: {node: ^12.20 || >= 14.13} - dependencies: - node-domexception: 1.0.0 - web-streams-polyfill: 3.3.3 - dev: false - /filelist@1.0.4: resolution: {integrity: sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==} dependencies: @@ -3608,10 +3572,6 @@ packages: hasBin: true dev: true - /flatbuffers@24.3.25: - resolution: {integrity: sha512-3HDgPbgiwWMI9zVB7VYBHaMrbOO7Gm0v+yD2FV/sCKj+9NDeVL7BOBYUuhWAQGKWOzBo8S9WdMvV0eixO233XQ==} - dev: false - /follow-redirects@1.15.6: resolution: {integrity: sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==} engines: {node: '>=4.0'} @@ -3636,13 +3596,6 @@ packages: mime-types: 2.1.35 dev: false - /formdata-polyfill@4.0.10: - resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} - engines: {node: '>=12.20.0'} - dependencies: - fetch-blob: 3.2.0 - dev: false - /forwarded@0.2.0: resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} engines: {node: '>= 0.6'} @@ -3670,13 +3623,6 @@ packages: universalify: 2.0.1 dev: true - /fs-minipass@2.1.0: - resolution: {integrity: sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==} - engines: {node: '>= 8'} - dependencies: - minipass: 3.3.6 - dev: false - /fs.realpath@1.0.0: resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} dev: true @@ -3786,16 +3732,6 @@ packages: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} dev: true - /h264-profile-level-id@2.0.0(supports-color@9.4.0): - resolution: {integrity: sha512-X4CLryVbVA0CtjTExS4G5U1gb2Z4wa32AF8ukVmFuLdw2JRq2aHisor7SY5SYTUUrUSqq0KdPIO18sql6IWIQw==} - engines: {node: '>=16'} - dependencies: - '@types/debug': 4.1.12 - debug: 4.3.4(supports-color@9.4.0) - transitivePeerDependencies: - - supports-color - dev: false - /has-bigints@1.0.2: resolution: {integrity: sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==} dev: true @@ -3882,7 +3818,7 @@ packages: engines: {node: '>= 14'} dependencies: agent-base: 7.1.1 - debug: 4.3.4(supports-color@9.4.0) + debug: 4.3.4 transitivePeerDependencies: - supports-color dev: false @@ -3903,7 +3839,7 @@ packages: engines: {node: '>= 14'} dependencies: agent-base: 7.1.1 - debug: 4.3.4(supports-color@9.4.0) + debug: 4.3.4 transitivePeerDependencies: - supports-color dev: false @@ -3973,11 +3909,6 @@ packages: /inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} - /ini@4.1.2: - resolution: {integrity: sha512-AMB1mvwR1pyBFY/nSevUX6y8nJWS63/SzUKD3JyQn97s4xgIdgQPT75IRouIiBAN4yLQBUShNYVW0+UG25daCw==} - engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - dev: false - /internal-slot@1.0.7: resolution: {integrity: sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==} engines: {node: '>= 0.4'} @@ -3998,7 +3929,7 @@ packages: dependencies: '@ioredis/commands': 1.2.0 cluster-key-slot: 1.1.2 - debug: 4.3.4(supports-color@9.4.0) + debug: 4.3.4 denque: 2.1.0 lodash.defaults: 4.2.0 lodash.isarguments: 3.1.0 @@ -4542,21 +4473,6 @@ packages: engines: {node: '>= 0.6'} dev: false - /mediasoup@3.14.1: - resolution: {integrity: sha512-gzfF4HH/g2+SzDp0wgOcgxoL8d4e4+BN7jvy4ydiG+5p2ZmVTHPz4wrvYXfQNHDRlp3jtkjLoFWTPvFTWvfoTg==} - engines: {node: '>=18'} - requiresBuild: true - dependencies: - '@types/ini': 4.1.0 - debug: 4.3.4(supports-color@9.4.0) - flatbuffers: 24.3.25 - h264-profile-level-id: 2.0.0(supports-color@9.4.0) - ini: 4.1.2 - node-fetch: 3.3.2 - supports-color: 9.4.0 - tar: 6.2.1 - dev: false - /memfs@4.8.2: resolution: {integrity: sha512-j4WKth315edViMBGkHW6NTF0QBjsTrcRDmYNcGsPq+ozMEyCCCIlX2d2mJ5wuh6iHvJ3FevUrr48v58YRqVdYg==} engines: {node: '>= 4.0.0'} @@ -4677,26 +4593,6 @@ packages: xml2js: 0.5.0 dev: false - /minipass@3.3.6: - resolution: {integrity: sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==} - engines: {node: '>=8'} - dependencies: - yallist: 4.0.0 - dev: false - - /minipass@5.0.0: - resolution: {integrity: sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==} - engines: {node: '>=8'} - dev: false - - /minizlib@2.1.2: - resolution: {integrity: sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==} - engines: {node: '>= 8'} - dependencies: - minipass: 3.3.6 - yallist: 4.0.0 - dev: false - /mitt@1.2.0: resolution: {integrity: sha512-r6lj77KlwqLhIUku9UWYes7KJtsczvolZkzp8hbaDPPaE24OmWl5s539Mytlj22siEQKosZ26qCBgda2PKwoJw==} dev: true @@ -4708,12 +4604,6 @@ packages: minimist: 1.2.8 dev: false - /mkdirp@1.0.4: - resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==} - engines: {node: '>=10'} - hasBin: true - dev: false - /mongodb-connection-string-url@3.0.0: resolution: {integrity: sha512-t1Vf+m1I5hC2M5RJx/7AtxgABy1cZmIPQRMXw+gEIPn/cZNF3Oiy+l0UIypUwVB5trcWHq3crg2g3uAR9aAwsQ==} dependencies: @@ -4797,7 +4687,7 @@ packages: resolution: {integrity: sha512-iQMncpmEK8R8ncT8HJGsGc9Dsp8xcgYMVSbs5jgnm1lFHTZqMJTUWTDx1LBO8+mK3tPNZWFLBghQEIOULSTHZg==} engines: {node: '>=14.0.0'} dependencies: - debug: 4.3.4(supports-color@9.4.0) + debug: 4.3.4 transitivePeerDependencies: - supports-color dev: false @@ -4871,20 +4761,6 @@ packages: resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} dev: true - /node-domexception@1.0.0: - resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} - engines: {node: '>=10.5.0'} - dev: false - - /node-fetch@3.3.2: - resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - dependencies: - data-uri-to-buffer: 4.0.1 - fetch-blob: 3.2.0 - formdata-polyfill: 4.0.10 - dev: false - /node-gyp-build-optional-packages@5.0.7: resolution: {integrity: sha512-YlCCc6Wffkx0kHkmam79GKvDQ6x+QZkMjFGrIMxgFNILFvGSbCp2fCBC55pGTT9gVaz8Na5CLmxt/urtzRv36w==} hasBin: true @@ -5915,7 +5791,7 @@ packages: /socket.io-adapter@2.5.4: resolution: {integrity: sha512-wDNHGXGewWAjQPt3pyeYBtpWSq9cLE5UW1ZUPL/2eGK9jtse/FpXib7epSTsz0Q0m+6sg6Y4KtcFTlah1bdOVg==} dependencies: - debug: 4.3.4(supports-color@9.4.0) + debug: 4.3.4 ws: 8.11.0 transitivePeerDependencies: - bufferutil @@ -5927,7 +5803,7 @@ packages: engines: {node: '>=10.0.0'} dependencies: '@socket.io/component-emitter': 3.1.1 - debug: 4.3.4(supports-color@9.4.0) + debug: 4.3.4 engine.io-client: 6.5.3 socket.io-parser: 4.2.4 transitivePeerDependencies: @@ -5941,7 +5817,7 @@ packages: engines: {node: '>=10.0.0'} dependencies: '@socket.io/component-emitter': 3.1.1 - debug: 4.3.4(supports-color@9.4.0) + debug: 4.3.4 transitivePeerDependencies: - supports-color @@ -5952,7 +5828,7 @@ packages: accepts: 1.3.8 base64id: 2.0.0 cors: 2.8.5 - debug: 4.3.4(supports-color@9.4.0) + debug: 4.3.4 engine.io: 6.5.4 socket.io-adapter: 2.5.4 socket.io-parser: 4.2.4 @@ -6181,10 +6057,6 @@ packages: has-flag: 4.0.0 dev: true - /supports-color@9.4.0: - resolution: {integrity: sha512-VL+lNrEoIXww1coLPOmiEmK/0sGigko5COxI09KzHc2VJXJsQ37UaQ+8quuxjDeA7+KnLGTWRyOXSLLR2Wb4jw==} - engines: {node: '>=12'} - /supports-preserve-symlinks-flag@1.0.0: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} @@ -6212,18 +6084,6 @@ packages: engines: {node: '>=6'} dev: true - /tar@6.2.1: - resolution: {integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==} - engines: {node: '>=10'} - dependencies: - chownr: 2.0.0 - fs-minipass: 2.1.0 - minipass: 5.0.0 - minizlib: 2.1.2 - mkdirp: 1.0.4 - yallist: 4.0.0 - dev: false - /temp-dir@2.0.0: resolution: {integrity: sha512-aoBAniQmmwtcKp/7BzsH8Cxzv8OL736p7v1ihGb5e9DJ9kTwGWHrQrVB5+lfVDzfGrdRzXch+ig7LHaY1JTOrg==} engines: {node: '>=8'} @@ -6503,13 +6363,6 @@ packages: resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} engines: {node: '>= 0.8'} - /unzalgo@3.0.0: - resolution: {integrity: sha512-yRSDlFaYpFJK2VO0iI4I2E3l1CF8puFNL00nh7beZ/q4XSxd9XPNIlsTvfOz/fF2P6tMBLWNVLWpLBvJ9/11ZQ==} - deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. - dependencies: - stats-lite: 2.2.0 - dev: false - /upath@1.2.0: resolution: {integrity: sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==} engines: {node: '>=4'} @@ -6616,11 +6469,6 @@ packages: '@zxing/text-encoding': 0.9.0 dev: false - /web-streams-polyfill@3.3.3: - resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} - engines: {node: '>= 8'} - dev: false - /webidl-conversions@4.0.2: resolution: {integrity: sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==} dev: true diff --git a/release b/release index bc1f647..63222c6 100755 --- a/release +++ b/release @@ -14,4 +14,4 @@ git checkout master git pull . develop git push origin master -git checkout develop +git checkout develop \ No newline at end of file diff --git a/restart b/restart index c8a004c..c62580e 100755 --- a/restart +++ b/restart @@ -1,9 +1,9 @@ #!/bin/bash -echo "Stopping production services for Time Tracker" -sudo supervisorctl stop tracker-web:* -sudo supervisorctl stop tracker-host-services:* +echo "Stopping production services for DTP Base" +sudo supervisorctl stop base-web:* +sudo supervisorctl stop base-host-services:* -echo "Starting production services for Time Tracker" -sudo supervisorctl start tracker-host-services:* -sudo supervisorctl start tracker-web:* \ No newline at end of file +echo "Starting production services for DTP Base" +sudo supervisorctl start base-host-services:* +sudo supervisorctl start base-web:* \ No newline at end of file diff --git a/start-local b/start-local index 51d43f6..66ef5ae 100755 --- a/start-local +++ b/start-local @@ -4,14 +4,12 @@ #MINIO_REGION_COMMENT="" #export MINIO_REGION_NAME MINIO_REGION_COMMENT -MINIO_ROOT_USER="dtp-time-tracker" -MINIO_ROOT_PASSWORD="06f281ba-a8e4-4d69-8769-3e8f2dd60630" +MINIO_ROOT_USER="dtp-base" +MINIO_ROOT_PASSWORD="20362b02-0baa-489e-b722-e82889333c24" export MINIO_ROOT_USER MINIO_ROOT_PASSWORD forever start --killSignal=SIGINT app/workers/host-services.js -forever start --killSignal=SIGINT app/workers/tracker-monitor.js minio server ./data/minio --address ":9080" --console-address ":9081" -forever stop app/workers/tracker-monitor.js forever stop app/workers/host-services.js \ No newline at end of file diff --git a/webpack.config.js b/webpack.config.js index 746d1eb..85543f9 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -1,5 +1,5 @@ // webpack.config.js -// Copyright (C) 2022 Rob Colbert @rob@nicecrew.digital +// Copyright (C) 2024 DTP Technologies, LLC // All Rights Reserved 'use strict'; @@ -21,10 +21,10 @@ if (webpackMode === 'development') { plugins.push( new BrowserSyncPlugin({ proxy: { - target: 'http://dev.tracker.digitaltelepresence.com:3000', + target: 'http://dev.base.digitaltelepresence.com:3000', ws: true, }, - host: 'dev.tracker.digitaltelepresence.com', + host: 'dev.base.digitaltelepresence.com', open: 'local', port: 3333, cors: true, @@ -50,8 +50,8 @@ if (webpackMode === 'development') { export default { entry: { 'app': './client/js/index.js', - 'tracker-light': './client/css/dtp-light.less', - 'tracker-dark': './client/css/dtp-dark.less', + 'dtp-light': './client/css/dtp-light.less', + 'dtp-dark': './client/css/dtp-dark.less', }, devtool: 'source-map', mode: webpackMode, @@ -107,4 +107,4 @@ export default { }, ], }, -}; +}; \ No newline at end of file