Compare commits

...

20 Commits

Author SHA1 Message Date
Carlos Valente 37786c3528 chore: bump version to 3.0.2 2024-05-13 14:15:18 +02:00
Carlos Valente c6d36d2497 refactor: forgiving test 2024-05-13 08:25:57 +02:00
Carlos Valente 2394eced93 refactor: forgiving test 2024-05-13 08:08:43 +02:00
Carlos Valente 13aeeb278d fix: filename may not exist 2024-05-13 08:08:43 +02:00
Carlos Valente af5d6e1aee fix: prevent stale selection 2024-05-13 08:06:28 +02:00
Carlos Valente bc036bdae7 refactor: forviging test
Allow a scenario where the operation execution happens in the next millisecond
2024-05-12 20:15:59 +02:00
Carlos Valente 266a84e5d3 chore: update keywords 2024-05-12 20:15:59 +02:00
Carlos Valente 92c3148e49 chore: bump version to 3.0.1 2024-05-12 20:15:59 +02:00
Carlos Valente efd05bcc34 refactor: simplify overview logic 2024-05-12 17:16:33 +02:00
Carlos Valente 3c7e061e51 refactor: improve detection of day after
fix: prevent mutation of initial object

refactor: download project file

Update apps/client/src/features/rundown/event-block/EventBlock.utils.ts

Co-authored-by: Alex Christoffer Rasmussen <ac@omnivox.dk>

Update apps/client/src/features/rundown/event-block/__tests__/EventBlock.utils.test.ts

Co-authored-by: Alex Christoffer Rasmussen <ac@omnivox.dk>
2024-05-12 17:16:33 +02:00
Carlos Valente 07074260a4 refactor: improve integration updates 2024-05-12 17:03:13 +02:00
Carlos Valente c57b0423af refactor: leading updates in timers 2024-05-12 17:03:13 +02:00
Carlos Valente 52145d5e1e chore: update element order 2024-05-11 14:58:28 +02:00
Alex Christoffer Rasmussen b2668e61a5 refactor: improve logging from http integration (#946) 2024-05-11 14:23:07 +02:00
Carlos Valente 7da3480dfe style: improve readability of shortcuts 2024-05-11 14:22:29 +02:00
Carlos Valente e8cfdbe3b9 refactor: simplify tests 2024-05-11 14:22:29 +02:00
Carlos Valente fd052355c3 style: uniform button size 2024-05-11 14:22:29 +02:00
Carlos Valente 00f04fb78c refactor: download project file 2024-05-10 22:35:12 +02:00
Carlos Valente 6f80de58ca fix: prevent mutation of initial object 2024-05-10 21:59:01 +02:00
Carlos Valente 5977266332 chore: update readme 2024-05-07 21:34:49 +02:00
35 changed files with 708 additions and 311 deletions
+11 -26
View File
@@ -128,57 +128,42 @@ Found a bug? [Open an issue](https://github.com/cpvalente/ontime/issues/new).
When installing the app you would see warning screens from the Operating System like: When installing the app you would see warning screens from the Operating System like:
in Windows
`Microsoft Defender SmartScreen prevented an unrecognised app from starting. Running this app might put your PC at risk.` `Microsoft Defender SmartScreen prevented an unrecognised app from starting. Running this app might put your PC at risk.`
or
`Ontime can't be opened because it is from an unidentified developer`
or in Linux or in Linux
`Could Not Display "ontime-linux.AppImage` `Could Not Display "ontime-linux.AppImage`
We currently only sign MacOS releases. \
Purchasing the certificates for both Mac and Windows would mean a recurrent expense which we are not able to cover.
You can circumvent this by allowing the execution of the app manually. You can circumvent this by allowing the execution of the app manually.
- In Windows: click more and select "Run Anyway" - In Windows: click `more` -> `Run Anyway`
- in macOS: the solution in macOS is different across versions, please refer to - In Linux: right-click the AppImage file: `Properties` -> `Permissions` -> `Allow Executing File as a Program`
the [apple documentation](https://support.apple.com/en-gb/guide/mac-help/mh40616/mac)
- In Linux: right-click the AppImage file -> Properties -> Permissions -> select Allow Executing
File as a Program
Long story short: Ontime app is unsigned. \
Purchasing the certificates for both Mac and Windows would mean a recurrent expense and is not a priority. \
This is unlikely to change in future. \
If you have tips on how to improve this or would like to sponsor the code signing, If you have tips on how to improve this or would like to sponsor the code signing,
please [open an issue](https://github.com/cpvalente/ontime/issues/new) please [open an issue](https://github.com/cpvalente/ontime/issues/new)
#### Safari
There are known issues with Safari versions lower than 13:
- Spacing and text styles might have small inconsistencies
- Table view does not work
There is no plan for any further work on this.
## Contributing ## Contributing
Looking to contribute? All types of help are appreciated, from coding to testing and feature specification. Looking to contribute? All types of help are appreciated, from coding to testing and feature specification.
If you are a developer and would like to contribute with some code, please open an issue to discuss before opening a If you are a developer and would like to contribute with code, please open an issue to discuss before opening a Pull Request.
Pull Request.
Information about the project setup can be found in the [development documentation](./DEVELOPMENT.md) Information about the project setup can be found in the [development documentation](./DEVELOPMENT.md)
## Help ## Links
Help is underway! ... and can be found [here](https://docs.getontime.no) See the [Ontime website](https://getontime.no) here and the link to the [documentation](https://docs.getontime.no)
## License ## License
This project is licensed under the terms of the GNU GPL v3 This project is licensed under the terms of the GNU GPL v3
## SPONSOR ## Sponsor
You can help the development of this project or say thank you with a one time donation. \ You can help the development of this project or say thank you with a one time donation. \
See [the terms of donations](https://github.com/cpvalente/ontime/blob/master/.github/FUNDING.md). See [the terms of donations](https://github.com/cpvalente/ontime/blob/master/.github/FUNDING.md).
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "ontime-ui", "name": "ontime-ui",
"version": "3.0.0", "version": "3.0.2",
"private": true, "private": true,
"type": "module", "type": "module",
"dependencies": { "dependencies": {
+4 -4
View File
@@ -11,15 +11,15 @@ const dbPath = `${apiEntryUrl}/db`;
/** /**
* HTTP request to the current DB * HTTP request to the current DB
*/ */
async function getDb(): Promise<AxiosResponse<DatabaseModel>> { async function getDb(fileName?: string): Promise<AxiosResponse<DatabaseModel>> {
return axios.get(`${dbPath}/download`); return axios.post(`${dbPath}/download/`, { fileName });
} }
/** /**
* Request download of the current project file * Request download of the current project file
* @param fileName * @param fileName
*/ */
export async function downloadProject(fileName: string = 'ontime-project') { export async function downloadProject(fileName: string) {
try { try {
const { data, name } = await fileDownload(fileName); const { data, name } = await fileDownload(fileName);
@@ -152,7 +152,7 @@ export async function getInfo(): Promise<GetInfo> {
* @returns * @returns
*/ */
async function fileDownload(fileName: string): Promise<{ data: DatabaseModel; name: string }> { async function fileDownload(fileName: string): Promise<{ data: DatabaseModel; name: string }> {
const response = await getDb(); const response = await getDb(fileName);
const headerLine = response.headers['Content-Disposition']; const headerLine = response.headers['Content-Disposition'];
@@ -1,3 +1,5 @@
import { MILLIS_PER_HOUR, MILLIS_PER_MINUTE, MILLIS_PER_SECOND } from 'ontime-utils';
import { forgivingStringToMillis, millisToDelayString } from '../dateConfig'; import { forgivingStringToMillis, millisToDelayString } from '../dateConfig';
describe('test forgivingStringToMillis()', () => { describe('test forgivingStringToMillis()', () => {
@@ -6,22 +8,22 @@ describe('test forgivingStringToMillis()', () => {
{ value: '', expect: 0 }, { value: '', expect: 0 },
{ value: '0', expect: 0 }, { value: '0', expect: 0 },
{ value: '-0', expect: 0 }, { value: '-0', expect: 0 },
{ value: '1', expect: 60 * 1000 }, { value: '1', expect: MILLIS_PER_MINUTE },
{ value: '-1', expect: 60 * 1000 }, { value: '-1', expect: MILLIS_PER_MINUTE },
{ value: '0h0m0s', expect: 0 }, { value: '0h0m0s', expect: 0 },
{ value: '0h0m1s', expect: 1000 }, { value: '0h0m1s', expect: MILLIS_PER_SECOND },
{ value: '0h1m0s', expect: 1000 * 60 }, { value: '0h1m0s', expect: MILLIS_PER_MINUTE },
{ value: '1h0m0s', expect: 1000 * 60 * 60 }, { value: '1h0m0s', expect: MILLIS_PER_HOUR },
{ value: '23h0m0s', expect: 1000 * 60 * 60 * 23 }, { value: '23h0m0s', expect: 23 * MILLIS_PER_HOUR },
{ value: '12h12m12s', expect: 12 * 1000 + 12 * 60 * 1000 + 12 * 1000 * 60 * 60 }, { value: '12h12m12s', expect: 12 * MILLIS_PER_SECOND + 12 * MILLIS_PER_MINUTE + 12 * MILLIS_PER_HOUR },
{ value: '12H12M12S', expect: 12 * 1000 + 12 * 60 * 1000 + 12 * 1000 * 60 * 60 }, { value: '12H12M12S', expect: 12 * MILLIS_PER_SECOND + 12 * MILLIS_PER_MINUTE + 12 * MILLIS_PER_HOUR },
{ value: '2m', expect: 2 * 60 * 1000 }, { value: '2m', expect: 2 * MILLIS_PER_MINUTE },
{ value: '1h5s', expect: 1000 * 60 * 60 + 1000 * 5 }, { value: '1h5s', expect: MILLIS_PER_HOUR + 5 * MILLIS_PER_SECOND },
{ value: '1h2m', expect: 1000 * 60 * 60 + 1000 * 60 * 2 }, { value: '1h2m', expect: MILLIS_PER_HOUR + 2 * MILLIS_PER_MINUTE },
]; ];
for (const s of testData) { for (const s of testData) {
it(`handles ${s.value} to left`, () => { it(`handles ${s.value}`, () => {
expect(typeof forgivingStringToMillis(s.value)).toBe('number'); expect(typeof forgivingStringToMillis(s.value)).toBe('number');
expect(forgivingStringToMillis(s.value)).toBe(s.expect); expect(forgivingStringToMillis(s.value)).toBe(s.expect);
}); });
@@ -30,10 +32,10 @@ describe('test forgivingStringToMillis()', () => {
describe('parses strings correctly', () => { describe('parses strings correctly', () => {
const ts = [ const ts = [
{ value: '1.1.1', expect: 60 * 60 * 1000 + 60 * 1000 + 1000 }, { value: '1.1.1', expect: MILLIS_PER_HOUR + MILLIS_PER_MINUTE + MILLIS_PER_SECOND },
{ value: '12.1.1', expect: 12 * 60 * 60 * 1000 + 60 * 1000 + 1000 }, { value: '12.1.1', expect: 12 * MILLIS_PER_HOUR + MILLIS_PER_MINUTE + MILLIS_PER_SECOND },
{ value: '12.55.1', expect: 12 * 60 * 60 * 1000 + 55 * 60 * 1000 + 1000 }, { value: '12.55.1', expect: 12 * MILLIS_PER_HOUR + 55 * MILLIS_PER_MINUTE + MILLIS_PER_SECOND },
{ value: '12.55.40', expect: 12 * 60 * 60 * 1000 + 55 * 60 * 1000 + 40 * 1000 }, { value: '12.55.40', expect: 12 * MILLIS_PER_HOUR + 55 * MILLIS_PER_MINUTE + 40 * MILLIS_PER_SECOND },
]; ];
for (const s of ts) { for (const s of ts) {
@@ -45,15 +47,15 @@ describe('test forgivingStringToMillis()', () => {
describe('parses time strings', () => { describe('parses time strings', () => {
const ts = [ const ts = [
{ value: '1h2m3s', expect: 60 * 60 * 1000 + 2 * 60 * 1000 + 3 * 1000 }, { value: '1h2m3s', expect: MILLIS_PER_HOUR + 2 * MILLIS_PER_MINUTE + 3 * MILLIS_PER_SECOND },
{ value: '1h3s', expect: 60 * 60 * 1000 + 3 * 1000 }, { value: '1h3s', expect: MILLIS_PER_HOUR + 3 * MILLIS_PER_SECOND },
{ value: '1h2m', expect: 60 * 60 * 1000 + 2 * 60 * 1000 }, { value: '1h2m', expect: MILLIS_PER_HOUR + 2 * MILLIS_PER_MINUTE },
{ value: '10h', expect: 10 * 60 * 60 * 1000 }, { value: '10h', expect: 10 * MILLIS_PER_HOUR },
{ value: '10m', expect: 10 * 60 * 1000 }, { value: '10m', expect: 10 * MILLIS_PER_MINUTE },
{ value: '10s', expect: 10 * 1000 }, { value: '10s', expect: 10 * MILLIS_PER_SECOND },
{ value: '120h', expect: 120 * 60 * 60 * 1000 }, { value: '120h', expect: 120 * MILLIS_PER_HOUR },
{ value: '120m', expect: 120 * 60 * 1000 }, { value: '120m', expect: 120 * MILLIS_PER_MINUTE },
{ value: '120s', expect: 120 * 1000 }, { value: '120s', expect: 120 * MILLIS_PER_SECOND },
]; ];
for (const s of ts) { for (const s of ts) {
@@ -65,20 +67,20 @@ describe('test forgivingStringToMillis()', () => {
describe('handles am/pm', () => { describe('handles am/pm', () => {
const ampm = [ const ampm = [
{ value: '9:10:11am', expect: 9 * 60 * 60 * 1000 + 10 * 60 * 1000 + 11 * 1000 }, { value: '9:10:11am', expect: 9 * MILLIS_PER_HOUR + 10 * MILLIS_PER_MINUTE + 11 * MILLIS_PER_SECOND },
{ value: '9:10:11a', expect: 9 * 60 * 60 * 1000 + 10 * 60 * 1000 + 11 * 1000 }, { value: '9:10:11a', expect: 9 * MILLIS_PER_HOUR + 10 * MILLIS_PER_MINUTE + 11 * MILLIS_PER_SECOND },
{ value: '9:10:11pm', expect: (12 + 9) * 60 * 60 * 1000 + 10 * 60 * 1000 + 11 * 1000 }, { value: '9:10:11pm', expect: (12 + 9) * MILLIS_PER_HOUR + 10 * MILLIS_PER_MINUTE + 11 * MILLIS_PER_SECOND },
{ value: '9:10:11p', expect: (12 + 9) * 60 * 60 * 1000 + 10 * 60 * 1000 + 11 * 1000 }, { value: '9:10:11p', expect: (12 + 9) * MILLIS_PER_HOUR + 10 * MILLIS_PER_MINUTE + 11 * MILLIS_PER_SECOND },
{ value: '9:10am', expect: 9 * 60 * 60 * 1000 + 10 * 60 * 1000 }, { value: '9:10am', expect: 9 * MILLIS_PER_HOUR + 10 * MILLIS_PER_MINUTE },
{ value: '9:10a', expect: 9 * 60 * 60 * 1000 + 10 * 60 * 1000 }, { value: '9:10a', expect: 9 * MILLIS_PER_HOUR + 10 * MILLIS_PER_MINUTE },
{ value: '9:10pm', expect: (12 + 9) * 60 * 60 * 1000 + 10 * 60 * 1000 }, { value: '9:10pm', expect: (12 + 9) * MILLIS_PER_HOUR + 10 * MILLIS_PER_MINUTE },
{ value: '9:10p', expect: (12 + 9) * 60 * 60 * 1000 + 10 * 60 * 1000 }, { value: '9:10p', expect: (12 + 9) * MILLIS_PER_HOUR + 10 * MILLIS_PER_MINUTE },
{ value: '9am', expect: 9 * 60 * 60 * 1000 }, { value: '9am', expect: 9 * MILLIS_PER_HOUR },
{ value: '9a', expect: 9 * 60 * 60 * 1000 }, { value: '9a', expect: 9 * MILLIS_PER_HOUR },
{ value: '9pm', expect: (12 + 9) * 60 * 60 * 1000 }, { value: '9pm', expect: (12 + 9) * MILLIS_PER_HOUR },
{ value: '9p', expect: (12 + 9) * 60 * 60 * 1000 }, { value: '9p', expect: (12 + 9) * MILLIS_PER_HOUR },
{ value: '12am', expect: 0 }, { value: '12am', expect: 0 },
{ value: '12pm', expect: 12 * 60 * 60 * 1000 }, { value: '12pm', expect: 12 * MILLIS_PER_HOUR },
]; ];
for (const s of ampm) { for (const s of ampm) {
@@ -90,12 +92,12 @@ describe('test forgivingStringToMillis()', () => {
describe('it infers separators when non existent', () => { describe('it infers separators when non existent', () => {
const testCases = [ const testCases = [
{ value: '1', expect: 1000 * 60 }, // 00:01:00 { value: '1', expect: MILLIS_PER_MINUTE }, // 00:01:00
{ value: '12', expect: 1000 * 60 * 12 }, // 00:12:00 { value: '12', expect: 12 * MILLIS_PER_MINUTE }, // 00:12:00
{ value: '123', expect: 1000 * 60 * 23 + 1000 * 60 * 60 }, // 01:23:00 { value: '123', expect: MILLIS_PER_HOUR + 23 * MILLIS_PER_MINUTE }, // 01:23:00
{ value: '1234', expect: 1000 * 60 * 34 + 1000 * 60 * 60 * 12 }, // 12:34:00 { value: '1234', expect: 12 * MILLIS_PER_HOUR + 34 * MILLIS_PER_MINUTE }, // 12:34:00
{ value: '12345', expect: 1000 * 60 * 34 + 1000 * 60 * 60 * 12 + 5 * 1000 }, // 12:34:05 { value: '12345', expect: 12 * MILLIS_PER_HOUR + 34 * MILLIS_PER_MINUTE + 5 * MILLIS_PER_SECOND }, // 12:34:05
{ value: '123456', expect: 1000 * 60 * 34 + 1000 * 60 * 60 * 12 + 56 * 1000 }, // 12:34:56 { value: '123456', expect: 12 * MILLIS_PER_HOUR + 34 * MILLIS_PER_MINUTE + 56 * MILLIS_PER_SECOND }, // 12:34:56
]; ];
for (const s of testCases) { for (const s of testCases) {
@@ -105,12 +107,12 @@ describe('test forgivingStringToMillis()', () => {
} }
const sixDigits = [ const sixDigits = [
{ value: '000000', expect: 0 }, { value: '000000', expect: 0 }, // 00:00:00
{ value: '000001', expect: 1000 }, { value: '000001', expect: MILLIS_PER_SECOND }, // 00:00:01
{ value: '000100', expect: 1000 * 60 }, { value: '000100', expect: MILLIS_PER_MINUTE }, // 00:01:00
{ value: '010000', expect: 1000 * 60 * 60 }, { value: '010000', expect: MILLIS_PER_HOUR }, // 01:00:00
{ value: '230000', expect: 1000 * 60 * 60 * 23 }, { value: '230000', expect: MILLIS_PER_HOUR * 23 }, // 23:00:00
{ value: '121212', expect: 12 * 1000 + 12 * 60 * 1000 + 12 * 1000 * 60 * 60 }, { value: '121212', expect: 12 * MILLIS_PER_HOUR + 12 * MILLIS_PER_MINUTE + 12 * MILLIS_PER_SECOND }, // 12:12:12
]; ];
for (const s of sixDigits) { for (const s of sixDigits) {
@@ -121,13 +123,13 @@ describe('test forgivingStringToMillis()', () => {
const fiveDigits = [ const fiveDigits = [
{ value: '00000', expect: 0 }, { value: '00000', expect: 0 },
{ value: '00001', expect: 1000 }, // 00:00:01 { value: '00001', expect: MILLIS_PER_SECOND }, // 00:00:01
{ value: '00010', expect: 1000 * 60 }, // 00:01:00 { value: '00010', expect: MILLIS_PER_MINUTE }, // 00:01:00
{ value: '00100', expect: 1000 * 60 * 10 }, // 00:10:00 { value: '00100', expect: 10 * MILLIS_PER_MINUTE }, // 00:10:00
{ value: '01000', expect: 1000 * 60 * 60 }, // 01:00:00 { value: '01000', expect: MILLIS_PER_HOUR }, // 01:00:00
{ value: '10000', expect: 1000 * 60 * 60 * 10 }, // 10:00:00 { value: '10000', expect: 10 * MILLIS_PER_HOUR }, // 10:00:00
{ value: '23000', expect: 1000 * 60 * 60 * 23 }, // 23:00:00 { value: '23000', expect: 23 * MILLIS_PER_HOUR }, // 23:00:00
{ value: '12121', expect: 1000 + 12 * 60 * 1000 + 12 * 1000 * 60 * 60 }, // 12:12:01 { value: '12121', expect: 12 * MILLIS_PER_HOUR + 12 * MILLIS_PER_MINUTE + 1 * MILLIS_PER_SECOND }, // 12:12:01
]; ];
for (const s of fiveDigits) { for (const s of fiveDigits) {
@@ -138,12 +140,12 @@ describe('test forgivingStringToMillis()', () => {
const fourDigits = [ const fourDigits = [
{ value: '0000', expect: 0 }, { value: '0000', expect: 0 },
{ value: '0001', expect: 1000 * 60 }, // 00:01:00 { value: '0001', expect: MILLIS_PER_MINUTE }, // 00:01:00
{ value: '0010', expect: 1000 * 60 * 10 }, // 00:10:00 { value: '0010', expect: 10 * MILLIS_PER_MINUTE }, // 00:10:00
{ value: '0100', expect: 1000 * 60 * 60 }, // 01:00:00 { value: '0100', expect: MILLIS_PER_HOUR }, // 01:00:00
{ value: '1000', expect: 1000 * 60 * 60 * 10 }, // 10:00:00 { value: '1000', expect: 10 * MILLIS_PER_HOUR }, // 10:00:00
{ value: '2300', expect: 1000 * 60 * 60 * 23 }, // 23:00:00 { value: '2300', expect: 23 * MILLIS_PER_HOUR }, // 23:00:00
{ value: '1212', expect: 12 * 60 * 1000 + 12 * 1000 * 60 * 60 }, // 12:12:00 { value: '1212', expect: 12 * MILLIS_PER_HOUR + 12 * MILLIS_PER_MINUTE }, // 12:12:00
]; ];
for (const s of fourDigits) { for (const s of fourDigits) {
@@ -154,11 +156,11 @@ describe('test forgivingStringToMillis()', () => {
const threeDigits = [ const threeDigits = [
{ value: '000', expect: 0 }, { value: '000', expect: 0 },
{ value: '001', expect: 1000 * 60 }, // 00:01:00 { value: '001', expect: MILLIS_PER_MINUTE }, // 00:01:00
{ value: '010', expect: 1000 * 60 * 10 }, // 00:10:00 { value: '010', expect: 10 * MILLIS_PER_MINUTE }, // 00:10:00
{ value: '100', expect: 1000 * 60 * 60 }, // 01:00:00 { value: '100', expect: MILLIS_PER_HOUR }, // 01:00:00
{ value: '230', expect: 2 * 1000 * 60 * 60 + 30 * 1000 * 60 }, // 02:30:00 { value: '230', expect: 2 * MILLIS_PER_HOUR + 30 * MILLIS_PER_MINUTE }, // 02:30:00
{ value: '121', expect: 21 * 60 * 1000 + 1000 * 60 * 60 }, // 01:21:00 { value: '121', expect: MILLIS_PER_HOUR + 21 * MILLIS_PER_MINUTE }, // 01:21:00
]; ];
for (const s of threeDigits) { for (const s of threeDigits) {
@@ -169,9 +171,9 @@ describe('test forgivingStringToMillis()', () => {
const twoDigits = [ const twoDigits = [
{ value: '00', expect: 0 }, { value: '00', expect: 0 },
{ value: '01', expect: 1000 * 60 }, // 00:01:00 { value: '01', expect: MILLIS_PER_MINUTE }, // 00:01:00
{ value: '10', expect: 1000 * 60 * 10 }, // 00:10:00 { value: '10', expect: 10 * MILLIS_PER_MINUTE }, // 00:10:00
{ value: '23', expect: 1000 * 60 * 23 }, // 00:23:00 { value: '23', expect: 23 * MILLIS_PER_MINUTE }, // 00:23:00
]; ];
for (const s of twoDigits) { for (const s of twoDigits) {
@@ -192,18 +194,17 @@ describe('test forgivingStringToMillis()', () => {
describe('handles overflows', () => { describe('handles overflows', () => {
const ts = [ const ts = [
// minutes overflow // minutes overflow
{ value: '2.0.0', expect: 1000 * 60 * 120 }, { value: '99', expect: 99 * MILLIS_PER_MINUTE },
{ value: '99', expect: 1000 * 60 * 99 }, { value: '1.39.0', expect: 99 * MILLIS_PER_MINUTE },
{ value: '1.39.0', expect: 1000 * 60 * 99 },
// seconds overflow // seconds overflow
{ value: '0.0.120', expect: 120 * 1000 }, { value: '0.0.120', expect: 120 * MILLIS_PER_SECOND },
{ value: '0.2.0', expect: 120 * 1000 }, { value: '0.2.0', expect: 120 * MILLIS_PER_SECOND },
{ value: '0.0.99', expect: 99 * 1000 }, { value: '0.0.99', expect: 99 * MILLIS_PER_SECOND },
{ value: '0.1.39', expect: 99 * 1000 }, { value: '0.1.39', expect: 99 * MILLIS_PER_SECOND },
// hours overflow // hours overflow
{ value: '25.0.0', expect: 1000 * 60 * 60 * 25 }, { value: '25.0.0', expect: 25 * MILLIS_PER_HOUR },
// hours overflow // hours overflow
{ value: '50.0.0', expect: 1000 * 60 * 60 * 50 }, { value: '50.0.0', expect: 50 * MILLIS_PER_HOUR },
]; ];
for (const s of ts) { for (const s of ts) {
@@ -216,10 +217,10 @@ describe('test forgivingStringToMillis()', () => {
describe('test fillLeft', () => { describe('test fillLeft', () => {
describe('function handles separators', () => { describe('function handles separators', () => {
const testData = [ const testData = [
{ value: '1:2:3:10', expect: 3723000 }, { value: '1:2:3:10', expect: MILLIS_PER_HOUR + 2 * MILLIS_PER_MINUTE + 3 * MILLIS_PER_SECOND }, // 01:02:03
{ value: '2,10', expect: 2 * 60 * 60 * 1000 + 60 * 10 * 1000 }, { value: '2,10', expect: 2 * MILLIS_PER_HOUR + 10 * MILLIS_PER_MINUTE },
{ value: '2.10', expect: 2 * 60 * 60 * 1000 + 60 * 10 * 1000 }, { value: '2.10', expect: 2 * MILLIS_PER_HOUR + 10 * MILLIS_PER_MINUTE },
{ value: '2 10', expect: 2 * 60 * 60 * 1000 + 60 * 10 * 1000 }, { value: '2 10', expect: 2 * MILLIS_PER_HOUR + 10 * MILLIS_PER_MINUTE },
]; ];
for (const s of testData) { for (const s of testData) {
@@ -232,8 +233,8 @@ describe('test forgivingStringToMillis()', () => {
describe('parses strings correctly', () => { describe('parses strings correctly', () => {
const ts = [ const ts = [
{ value: '1.2', expect: 60 * 60 * 1000 + 2 * 60 * 1000 }, { value: '1.2', expect: MILLIS_PER_HOUR + 2 * MILLIS_PER_MINUTE },
{ value: '1.70', expect: 60 * 60 * 1000 + 70 * 60 * 1000 }, { value: '1.70', expect: MILLIS_PER_HOUR + 70 * MILLIS_PER_MINUTE },
]; ];
for (const s of ts) { for (const s of ts) {
@@ -246,8 +247,8 @@ describe('test forgivingStringToMillis()', () => {
describe('handles overflows', () => { describe('handles overflows', () => {
const ts = [ const ts = [
// minutes overflow // minutes overflow
{ value: '0.120', expect: 120 * 60 * 1000 }, { value: '0.120', expect: 120 * MILLIS_PER_MINUTE },
{ value: '0.99', expect: 99 * 60 * 1000 }, { value: '0.99', expect: 99 * MILLIS_PER_MINUTE },
]; ];
for (const s of ts) { for (const s of ts) {
@@ -93,10 +93,10 @@ export default function UrlPresetsForm() {
<Panel.SubHeader> <Panel.SubHeader>
URL presets URL presets
<div className={style.actionButtons}> <div className={style.actionButtons}>
<Button variant='ontime-ghosted' size='md' onClick={onReset} isDisabled={!canSubmit}> <Button variant='ontime-ghosted' size='sm' onClick={onReset} isDisabled={!canSubmit}>
Revert to saved Revert to saved
</Button> </Button>
<Button variant='ontime-filled' size='md' type='submit' isDisabled={!canSubmit} isLoading={isSubmitting}> <Button variant='ontime-filled' size='sm' type='submit' isDisabled={!canSubmit} isLoading={isSubmitting}>
Save Save
</Button> </Button>
</div> </div>
@@ -169,7 +169,7 @@ function ActionMenu({
<MenuItem onClick={handleRename}>Rename</MenuItem> <MenuItem onClick={handleRename}>Rename</MenuItem>
<MenuItem onClick={handleDuplicate}>Duplicate</MenuItem> <MenuItem onClick={handleDuplicate}>Duplicate</MenuItem>
<MenuItem onClick={handleDownload}>Download</MenuItem> <MenuItem onClick={handleDownload}>Download</MenuItem>
{current && <MenuItem onClick={handleExportCSV}>Export CSV Rundown</MenuItem>} <MenuItem onClick={handleExportCSV}>Export CSV Rundown</MenuItem>
<MenuItem isDisabled={current} onClick={handleDelete}> <MenuItem isDisabled={current} onClick={handleDelete}>
Delete Delete
</MenuItem> </MenuItem>
@@ -95,7 +95,7 @@ function TitlesOverview() {
} }
function TimerOverview() { function TimerOverview() {
const {current} = useTimer(); const { current } = useTimer();
const display = millisToString(current); const display = millisToString(current);
@@ -0,0 +1,22 @@
import { dayInMs } from 'ontime-utils';
import { calculateEndAndDaySpan } from '../overviewUtils';
describe('calculateEndAndDaySpan', () => {
it('should return [null, 0] when end is null', () => {
const result = calculateEndAndDaySpan(null);
expect(result).toEqual([null, 0]);
});
it('should return [end, 0] when end is less than or equal to dayInMs', () => {
const end = dayInMs / 2;
const result = calculateEndAndDaySpan(end);
expect(result).toEqual([end, 0]);
});
it('should return [end % dayInMs, Math.floor(end / dayInMs)] when end is greater than dayInMs', () => {
const end = dayInMs * 1.5;
const result = calculateEndAndDaySpan(end);
expect(result).toEqual([end % dayInMs, Math.floor(end / dayInMs)]);
});
});
@@ -13,20 +13,14 @@ export function formatedTime(time: MaybeNumber) {
} }
/** /**
* Calculates a day span from a number range * Calculates how long a time is and how many days it spans
* @param end
* @returns
*/ */
export function calculateEndAndDaySpan(end: MaybeNumber): [MaybeNumber, number] { export function calculateEndAndDaySpan(end: MaybeNumber): [MaybeNumber, number] {
let maybeEnd = end; if (end !== null && end > dayInMs) {
let maybeDaySpan = 0; return [end % dayInMs, Math.floor(end / dayInMs)];
if (end !== null) {
if (end > dayInMs) {
maybeEnd = end % dayInMs;
maybeDaySpan = Math.floor(end / dayInMs);
}
} }
return [maybeEnd, maybeDaySpan];
return [end, 0];
} }
/** /**
@@ -242,13 +242,7 @@ export default function EventBlock(props: EventBlockProps) {
onContextMenu={onContextMenu} onContextMenu={onContextMenu}
id='event-block' id='event-block'
> >
<RundownIndicators <RundownIndicators timeStart={timeStart} previousStart={previousStart} previousEnd={previousEnd} delay={delay} />
timeStart={timeStart}
timeEnd={timeEnd}
previousStart={previousStart}
previousEnd={previousEnd}
delay={delay}
/>
<div className={style.binder} style={{ ...binderColours }} tabIndex={-1}> <div className={style.binder} style={{ ...binderColours }} tabIndex={-1}>
<span className={style.drag} ref={handleRef} {...dragAttributes} {...dragListeners}> <span className={style.drag} ref={handleRef} {...dragAttributes} {...dragListeners}>
@@ -10,24 +10,41 @@ export function formatDelay(timeStart: number, delay: number): string | undefine
return `New start ${timeTag}`; return `New start ${timeTag}`;
} }
/**
* Utility function checks whether a given event is the day after from its predecessor
* We consider an event to be the day after, if it begins before the start of the previous
* @example day after
* // 09:00 - 10:00
* // 08:00 - 10:30 <--- day after
* @example same day
* // 09:00 - 10:00
* // 09:30 - 10:30 <--- same day
*/
function checkIsNextDay(previousStart: number, timeStart: number): boolean {
return timeStart < previousStart;
}
export function formatOverlap( export function formatOverlap(
previousStart: MaybeNumber, previousStart: MaybeNumber,
previousEnd: MaybeNumber, previousEnd: MaybeNumber,
timeStart: number, timeStart: number,
timeEnd: number,
): string | undefined { ): string | undefined {
if (previousEnd === null) return; const noPreviousElement = previousEnd === null || previousStart === null;
if (noPreviousElement) return;
const overlap = previousEnd - timeStart; const overlap = previousEnd - timeStart;
if (overlap === 0) return; if (overlap === 0) return;
if (previousStart && timeStart < previousEnd) { const previousCrossMidnight = previousStart > previousEnd;
const overlap = timeEnd - previousStart; const isNextDay = previousCrossMidnight
if (overlap > 0) { ? checkIsNextDay(previousEnd, timeStart) || previousEnd == 0 // exception for when previousEnd is precisely midnight
const overlapString = removeLeadingZero(millisToString(Math.abs(overlap))); : checkIsNextDay(previousStart, timeStart);
return `Overlap ${overlapString}`;
} const correctedPreviousEnd = previousCrossMidnight ? previousEnd + dayInMs : previousEnd;
const gap = timeStart + dayInMs - previousEnd;
if (isNextDay) {
const gap = dayInMs - correctedPreviousEnd + timeStart;
if (gap === 0) return;
const gapString = removeLeadingZero(millisToString(Math.abs(gap))); const gapString = removeLeadingZero(millisToString(Math.abs(gap)));
return `Gap ${gapString} (next day)`; return `Gap ${gapString} (next day)`;
} }
@@ -6,16 +6,15 @@ import style from './RundownIndicators.module.scss';
interface RundownIndicatorProps { interface RundownIndicatorProps {
timeStart: number; timeStart: number;
timeEnd: number;
previousStart: MaybeNumber; previousStart: MaybeNumber;
previousEnd: MaybeNumber; previousEnd: MaybeNumber;
delay: number; delay: number;
} }
export default function RundownIndicators(props: RundownIndicatorProps) { export default function RundownIndicators(props: RundownIndicatorProps) {
const { timeStart, timeEnd, previousStart, previousEnd, delay } = props; const { timeStart, previousStart, previousEnd, delay } = props;
const hasOverlap = formatOverlap(previousStart, previousEnd, timeStart, timeEnd); const hasOverlap = formatOverlap(previousStart, previousEnd, timeStart);
const hasDelay = formatDelay(timeStart, delay); const hasDelay = formatDelay(timeStart, delay);
return ( return (
@@ -1,3 +1,5 @@
import { MILLIS_PER_HOUR } from 'ontime-utils';
import { formatDelay, formatOverlap } from '../EventBlock.utils'; import { formatDelay, formatOverlap } from '../EventBlock.utils';
describe('formatDelay()', () => { describe('formatDelay()', () => {
@@ -14,35 +16,47 @@ describe('formatOverlap()', () => {
const previousStart = 0; const previousStart = 0;
const previousEnd = 60000; // 1 min const previousEnd = 60000; // 1 min
const timeStart = 30000; // 30 sec const timeStart = 30000; // 30 sec
const timeEnd = 90000; // 1:30 min const result = formatOverlap(previousStart, previousEnd, timeStart);
const result = formatOverlap(previousStart, previousEnd, timeStart, timeEnd);
expect(result).toEqual('Overlap 0:30'); expect(result).toEqual('Overlap 0:30');
}); });
it('bug #949 recognises an overlap between two times', () => {
const previousStart = 46800000; // 13:00:00
const previousEnd = 48600000; // 13:30:00
const timeStart = 48300000; // 13:25:00
const result = formatOverlap(previousStart, previousEnd, timeStart);
expect(result).toEqual('Overlap 5:00');
});
it('handles events the day after, without overlap', () => { it('handles events the day after, without overlap', () => {
const previousStart = new Date(0).setUTCHours(11); const previousStart = 11 * MILLIS_PER_HOUR;
const previousEnd = new Date(0).setUTCHours(12); const previousEnd = 12 * MILLIS_PER_HOUR;
const timeStart = new Date(0).setUTCHours(6); const timeStart = 6 * MILLIS_PER_HOUR;
const timeEnd = new Date(0).setUTCHours(10); const result = formatOverlap(previousStart, previousEnd, timeStart);
const result = formatOverlap(previousStart, previousEnd, timeStart, timeEnd);
expect(result).toBe('Gap 18:00:00 (next day)'); expect(result).toBe('Gap 18:00:00 (next day)');
}); });
it('handles events the day after, with overlap', () => {
const previousStart = new Date(0).setUTCHours(9);
const previousEnd = new Date(0).setUTCHours(10);
const timeStart = new Date(0).setUTCHours(6);
const timeEnd = new Date(0).setUTCHours(11);
const result = formatOverlap(previousStart, previousEnd, timeStart, timeEnd);
expect(result).toBe('Overlap 02:00:00');
});
it('handles events the day after, with gap', () => { it('handles events the day after, with gap', () => {
const previousStart = new Date(0).setUTCHours(17); const previousStart = 17 * MILLIS_PER_HOUR;
const previousEnd = new Date(0).setUTCHours(23); const previousEnd = 23 * MILLIS_PER_HOUR;
const timeStart = new Date(0).setUTCHours(9); const timeStart = 9 * MILLIS_PER_HOUR;
const timeEnd = new Date(0).setUTCHours(11); const result = formatOverlap(previousStart, previousEnd, timeStart);
const result = formatOverlap(previousStart, previousEnd, timeStart, timeEnd);
expect(result).toBe('Gap 10:00:00 (next day)'); expect(result).toBe('Gap 10:00:00 (next day)');
}); });
it('handles events the day after, with previous ending at midnight', () => {
const previousStart = 23 * MILLIS_PER_HOUR; // 23:00:00
const previousEnd = 0; // 00:00:00
const timeStart = 1 * MILLIS_PER_HOUR; // 01:00:00
const result = formatOverlap(previousStart, previousEnd, timeStart);
expect(result).toBe('Gap 01:00:00 (next day)');
});
it('handles events the day after, with previous ending over midnight', () => {
const previousStart = 23 * MILLIS_PER_HOUR;
const previousEnd = 1 * MILLIS_PER_HOUR;
const timeStart = 2 * MILLIS_PER_HOUR;
const result = formatOverlap(previousStart, previousEnd, timeStart);
expect(result).toBe('Gap 01:00:00');
});
}); });
@@ -32,3 +32,9 @@
.prompt { .prompt {
font-size: 1rem; font-size: 1rem;
} }
.divider {
display: inline-block;
text-align: center;
width: 1em;
}
@@ -18,49 +18,77 @@ function EventEditorEmpty() {
<tbody> <tbody>
<tr> <tr>
<td> <td>
<Kbd>{deviceAlt}</Kbd> + <Kbd></Kbd> / <Kbd></Kbd> <Kbd>{deviceAlt}</Kbd>
<AuxKey>+</AuxKey>
<Kbd></Kbd>
<AuxKey>/</AuxKey>
<Kbd></Kbd>
</td> </td>
<td>Select entry</td> <td>Select entry</td>
</tr> </tr>
<tr> <tr>
<td> <td>
<Kbd>{deviceAlt}</Kbd> + <Kbd>{deviceMod}</Kbd> + <Kbd></Kbd> / <Kbd></Kbd> <Kbd>{deviceAlt}</Kbd>
<AuxKey>+</AuxKey>
<Kbd>{deviceMod}</Kbd>
<AuxKey>+</AuxKey>
<Kbd></Kbd>
<AuxKey>/</AuxKey>
<Kbd></Kbd>
</td> </td>
<td>Reorder selected entry</td> <td>Reorder selected entry</td>
</tr> </tr>
<tr> <tr>
<td> <td>
<Kbd>{deviceAlt}</Kbd> + <Kbd>E</Kbd> <Kbd>{deviceAlt}</Kbd>
<AuxKey>+</AuxKey>
<Kbd>E</Kbd>
</td> </td>
<td>Add event below</td> <td>Add event below</td>
</tr> </tr>
<tr> <tr>
<td> <td>
<Kbd>{deviceAlt}</Kbd> + <Kbd></Kbd> + <Kbd>E</Kbd> <Kbd>{deviceAlt}</Kbd>
<AuxKey>+</AuxKey>
<Kbd></Kbd>
<AuxKey>+</AuxKey>
<Kbd>E</Kbd>
</td> </td>
<td>Add event above</td> <td>Add event above</td>
</tr> </tr>
<tr> <tr>
<td> <td>
<Kbd>{deviceAlt}</Kbd> + <Kbd>B</Kbd> <Kbd>{deviceAlt}</Kbd>
<AuxKey>+</AuxKey>
<Kbd>B</Kbd>
</td> </td>
<td>Add block below</td> <td>Add block below</td>
</tr> </tr>
<tr> <tr>
<td> <td>
<Kbd>{deviceAlt}</Kbd> + <Kbd></Kbd> + <Kbd>B</Kbd> <Kbd>{deviceAlt}</Kbd>
<AuxKey>+</AuxKey>
<Kbd></Kbd>
<AuxKey>+</AuxKey>
<Kbd>B</Kbd>
</td> </td>
<td>Add block above</td> <td>Add block above</td>
</tr> </tr>
<tr> <tr>
<td> <td>
<Kbd>{deviceAlt}</Kbd> + <Kbd>D</Kbd> <Kbd>{deviceAlt}</Kbd>
<AuxKey>+</AuxKey>
<Kbd>D</Kbd>
</td> </td>
<td>Add delay below</td> <td>Add delay below</td>
</tr> </tr>
<tr> <tr>
<td> <td>
<Kbd>{deviceAlt}</Kbd> + <Kbd></Kbd> + <Kbd>D</Kbd> <Kbd>{deviceAlt}</Kbd>
<AuxKey>+</AuxKey>
<Kbd></Kbd>
<AuxKey>+</AuxKey>
<Kbd>D</Kbd>
</td> </td>
<td>Add delay above</td> <td>Add delay above</td>
</tr> </tr>
@@ -72,25 +100,35 @@ function EventEditorEmpty() {
</tr> </tr>
<tr> <tr>
<td> <td>
<Kbd>{deviceMod}</Kbd> + <Kbd></Kbd> <Kbd>{deviceMod}</Kbd>
<AuxKey>+</AuxKey>
<Kbd></Kbd>
</td> </td>
<td>Delete selected entry</td> <td>Delete selected entry</td>
</tr> </tr>
<tr> <tr>
<td> <td>
<Kbd>{deviceMod}</Kbd> + <Kbd>C</Kbd> <Kbd>{deviceMod}</Kbd>
<AuxKey>+</AuxKey>
<Kbd>C</Kbd>
</td> </td>
<td>Copy selected entry</td> <td>Copy selected entry</td>
</tr> </tr>
<tr> <tr>
<td> <td>
<Kbd>{deviceMod}</Kbd> + <Kbd></Kbd> + <Kbd>V</Kbd> <Kbd>{deviceMod}</Kbd>
<AuxKey>+</AuxKey>
<Kbd></Kbd>
<AuxKey>+</AuxKey>
<Kbd>V</Kbd>
</td> </td>
<td>Paste above</td> <td>Paste above</td>
</tr> </tr>
<tr> <tr>
<td> <td>
<Kbd>{deviceMod}</Kbd> + <Kbd>V</Kbd> <Kbd>{deviceMod}</Kbd>
<AuxKey>+</AuxKey>
<Kbd>V</Kbd>
</td> </td>
<td>Paste below</td> <td>Paste below</td>
</tr> </tr>
@@ -100,3 +138,7 @@ function EventEditorEmpty() {
</div> </div>
); );
} }
function AuxKey({ children }: { children: React.ReactNode }) {
return <span className={style.divider}>{children}</span>;
}
+6 -2
View File
@@ -83,9 +83,13 @@ const theme = extendTheme({
Kbd: { Kbd: {
baseStyle: { baseStyle: {
borderRadius: '2px', borderRadius: '2px',
border: '1px solid rgba(255, 255, 255, 0.20)', border: 'none',
background: 'rgba(255, 255, 255, 0.10)', background: 'rgba(255, 255, 255, 0.10)',
color: 'white', padding: '0.125rem 0.5rem',
color: '#f6f6f6', // $ui-white
fontWeight: 400,
boxShadow: '0px 0px 3px 0px rgba(0,0,0,0.4)',
fontSize: 'calc(1rem - 2px)',
}, },
}, },
Menu: { Menu: {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "ontime", "name": "ontime",
"version": "3.0.0", "version": "3.0.2",
"author": "Carlos Valente", "author": "Carlos Valente",
"description": "Time keeping for live events", "description": "Time keeping for live events",
"repository": "https://github.com/cpvalente/ontime", "repository": "https://github.com/cpvalente/ontime",
+2 -3
View File
@@ -2,7 +2,7 @@
"name": "ontime-server", "name": "ontime-server",
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
"version": "3.0.0", "version": "3.0.2",
"exports": "./src/index.js", "exports": "./src/index.js",
"dependencies": { "dependencies": {
"@googleapis/sheets": "^5.0.5", "@googleapis/sheets": "^5.0.5",
@@ -10,9 +10,8 @@
"cors": "^2.8.5", "cors": "^2.8.5",
"dotenv": "^16.0.1", "dotenv": "^16.0.1",
"express": "^4.18.2", "express": "^4.18.2",
"express-session": "^1.17.3",
"express-static-gzip": "^2.1.7", "express-static-gzip": "^2.1.7",
"express-validator": "^6.14.2", "express-validator": "^7.0.1",
"fast-equals": "^5.0.1", "fast-equals": "^5.0.1",
"google-auth-library": "^9.4.2", "google-auth-library": "^9.4.2",
"got": "^14.0.0", "got": "^14.0.0",
+28 -5
View File
@@ -6,17 +6,19 @@ import {
ProjectData, ProjectData,
ProjectFileListResponse, ProjectFileListResponse,
} from 'ontime-types'; } from 'ontime-types';
import { getErrorMessage } from 'ontime-utils';
import { join } from 'path';
import { existsSync } from 'fs';
import type { Request, Response } from 'express'; import type { Request, Response } from 'express';
import { failEmptyObjects } from '../../utils/routerUtils.js'; import { failEmptyObjects } from '../../utils/routerUtils.js';
import { resolveDbPath, resolveProjectsDirectory } from '../../setup/index.js'; import { resolveDbDirectory, resolveProjectsDirectory } from '../../setup/index.js';
import * as projectService from '../../services/project-service/ProjectService.js'; import * as projectService from '../../services/project-service/ProjectService.js';
import { ensureJsonExtension } from '../../utils/fileManagement.js'; import { ensureJsonExtension } from '../../utils/fileManagement.js';
import { generateUniqueFileName } from '../../utils/generateUniqueFilename.js'; import { generateUniqueFileName } from '../../utils/generateUniqueFilename.js';
import { appStateService } from '../../services/app-state-service/AppStateService.js'; import { appStateService } from '../../services/app-state-service/AppStateService.js';
import { getErrorMessage } from 'ontime-utils';
export async function patchPartialProjectFile(req: Request, res: Response<DatabaseModel | ErrorResponse>) { export async function patchPartialProjectFile(req: Request, res: Response<DatabaseModel | ErrorResponse>) {
// all fields are optional in validation // all fields are optional in validation
@@ -76,9 +78,30 @@ export async function createProjectFile(req: Request, res: Response<{ filename:
} }
} }
export async function projectDownload(_req: Request, res: Response) { /**
const fileTitle = projectService.getProjectTitle(); * Utility function finds the correct project file to download
res.download(resolveDbPath, `${fileTitle}.json`, (error) => { */
function selectProjectFile(fileName?: string) {
const projectsDirectory = resolveDbDirectory;
const fileToDownload = fileName ? ensureJsonExtension(fileName) : projectService.getProjectTitle();
const pathToFile = join(projectsDirectory, fileToDownload);
return { pathToFile, name: fileToDownload };
}
/**
* Allows downloading of a optionally given project files
* If no {filename} is provided, loaded file will be served
*/
export async function projectDownload(req: Request, res: Response) {
const { pathToFile, name } = selectProjectFile(req.body?.fileName);
// Check if the file exists before attempting to download
if (!existsSync(pathToFile)) {
return res.status(404).send({ message: `Project ${name} not found.` });
}
res.download(pathToFile, name, (error) => {
if (error) { if (error) {
const message = getErrorMessage(error); const message = getErrorMessage(error);
res.status(500).send({ message }); res.status(500).send({ message });
+2 -1
View File
@@ -16,6 +16,7 @@ import { uploadProjectFile } from './db.middleware.js';
import { import {
projectSanitiser, projectSanitiser,
sanitizeProjectFilename, sanitizeProjectFilename,
validateDownloadProject,
validateLoadProjectFile, validateLoadProjectFile,
validatePatchProjectFile, validatePatchProjectFile,
validateProjectDuplicate, validateProjectDuplicate,
@@ -24,7 +25,7 @@ import {
export const router = express.Router(); export const router = express.Router();
router.get('/download', projectDownload); router.post('/download', validateDownloadProject, projectDownload);
router.post('/upload', uploadProjectFile, postProjectFile); router.post('/upload', uploadProjectFile, postProjectFile);
router.patch('/', validatePatchProjectFile, patchPartialProjectFile); router.patch('/', validatePatchProjectFile, patchPartialProjectFile);
@@ -104,3 +104,19 @@ export const validateProjectRename = [
next(); next();
}, },
]; ];
/**
* @description Validates a download request which can include an optional project name.
*/
export const validateDownloadProject = [
body('fileName').isString().optional(),
(req: Request, res: Response, next: NextFunction) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(422).json({ errors: errors.array() });
}
next();
},
];
+1 -1
View File
@@ -2,5 +2,5 @@ export const timerConfig = {
skipLimit: 1000, // threshold of skip for recalculating skipLimit: 1000, // threshold of skip for recalculating
updateRate: 32, // how often do we update the timer updateRate: 32, // how often do we update the timer
notificationRate: 1000, // how often do we notify clients and integrations notificationRate: 1000, // how often do we notify clients and integrations
triggerAhead: 16, // how far ahead do we trigger the end event triggerAhead: 10, // how far ahead do we trigger the end event
}; };
+2 -1
View File
@@ -2,6 +2,7 @@ import { OntimeEvent } from 'ontime-types';
import * as runtimeState from '../stores/runtimeState.js'; import * as runtimeState from '../stores/runtimeState.js';
import type { UpdateResult } from '../stores/runtimeState.js'; import type { UpdateResult } from '../stores/runtimeState.js';
import { timerConfig } from '../config/config.js';
/** /**
* Service manages Ontime's main timer * Service manages Ontime's main timer
@@ -41,7 +42,7 @@ export class TimerService {
} }
const state = runtimeState.getState(); const state = runtimeState.getState();
const endTime = state.timer.current - 10; const endTime = state.timer.current - timerConfig.triggerAhead;
this.endCallback = setTimeout(() => this.update(), endTime); this.endCallback = setTimeout(() => this.update(), endTime);
return true; return true;
} }
@@ -51,7 +51,24 @@ export class HttpIntegration implements IIntegration<HttpSubscription, HttpSetti
emit(path: string) { emit(path: string) {
got.get(path, { retry: { limit: 0 } }).catch((err) => { got.get(path, { retry: { limit: 0 } }).catch((err) => {
logger.error(LogOrigin.Tx, `HTTP Integration: ${err.code}`); logger.warning(LogOrigin.Tx, `HTTP Integration: ${err.message}`);
if (err.code === 'ECONNREFUSED') {
logger.warning(LogOrigin.Tx, `HTTP Integration: '${err.code}' The server refused the connection`);
return;
}
if (err.code === 'ENOTFOUND') {
logger.warning(LogOrigin.Tx, `HTTP Integration: '${err.code}' DNS lookup failed`);
return;
}
if (err.code === 'ETIMEDOUT') {
logger.warning(LogOrigin.Tx, `HTTP Integration: '${err.code}' The connection timed out`);
return;
}
logger.warning(LogOrigin.Tx, `HTTP Integration: ${err.code}`);
}); });
} }
@@ -80,6 +80,8 @@ export function generate(
links = {}; links = {};
firstStart = null; firstStart = null;
lastEnd = null; lastEnd = null;
totalDuration = 0;
totalDelay = 0;
let accumulatedDelay = 0; let accumulatedDelay = 0;
let daySpan = 0; let daySpan = 0;
@@ -1,4 +1,4 @@
import { EndAction, LogOrigin, OntimeEvent, Playback, RuntimeStore, TimerLifeCycle } from 'ontime-types'; import { EndAction, LogOrigin, MaybeNumber, OntimeEvent, Playback, RuntimeStore, TimerLifeCycle } from 'ontime-types';
import { millisToString, validatePlayback } from 'ontime-utils'; import { millisToString, validatePlayback } from 'ontime-utils';
import { deepEqual } from 'fast-equals'; import { deepEqual } from 'fast-equals';
@@ -20,6 +20,7 @@ import {
getPlayableEvents, getPlayableEvents,
} from '../rundown-service/rundownUtils.js'; } from '../rundown-service/rundownUtils.js';
import { integrationService } from '../integration-service/IntegrationService.js'; import { integrationService } from '../integration-service/IntegrationService.js';
import { getForceUpdate, getShouldClockUpdate, getShouldTimerUpdate } from './rundownService.utils.js';
/** /**
* Service manages runtime status of app * Service manages runtime status of app
@@ -27,15 +28,20 @@ import { integrationService } from '../integration-service/IntegrationService.js
*/ */
class RuntimeService { class RuntimeService {
private eventTimer: TimerService | null = null; private eventTimer: TimerService | null = null;
private lastOnUpdate = -1; private lastIntegrationClockUpdate = -1;
private lastIntegrationTimerValue = -1;
/** last time we updated the socket */ /** last time we updated the socket */
static previousUpdate: number; static previousTimerUpdate: number;
static previousTimerValue: MaybeNumber;
static previousClockUpdate: number;
/** last known state */ /** last known state */
static previousState: RuntimeState; static previousState: RuntimeState;
constructor() { constructor() {
RuntimeService.previousUpdate = -1; RuntimeService.previousTimerUpdate = -1;
RuntimeService.previousTimerValue = -1;
RuntimeService.previousClockUpdate = -1;
RuntimeService.previousState = {} as RuntimeState; RuntimeService.previousState = {} as RuntimeState;
} }
@@ -61,19 +67,25 @@ class RuntimeService {
} }
} }
// update normal cycle const hasRunningTimer = Boolean(newState.eventNow) && newState.timer.playback === Playback.Play;
if (newState.clock - this.lastOnUpdate >= timerConfig.notificationRate) { const shouldUpdateTimer =
const hasRunningTimer = Boolean(newState.eventNow) && newState.timer.playback === Playback.Play; hasRunningTimer && getShouldTimerUpdate(this.lastIntegrationTimerValue, newState.timer.current);
if (hasRunningTimer) {
process.nextTick(() => { if (shouldUpdateTimer) {
integrationService.dispatch(TimerLifeCycle.onUpdate); process.nextTick(() => {
}); integrationService.dispatch(TimerLifeCycle.onUpdate);
} });
this.lastIntegrationTimerValue = newState.timer.current;
}
const shouldUpdateClock = getShouldClockUpdate(this.lastIntegrationClockUpdate, newState.clock);
if (shouldUpdateClock) {
process.nextTick(() => { process.nextTick(() => {
integrationService.dispatch(TimerLifeCycle.onClock); integrationService.dispatch(TimerLifeCycle.onClock);
}); });
this.lastOnUpdate = newState.clock; this.lastIntegrationClockUpdate = newState.clock;
} }
if (shouldCallRoll) { if (shouldCallRoll) {
@@ -516,29 +528,30 @@ function broadcastResult(_target: any, _propertyKey: string, descriptor: Propert
// we do the comparison by explicitly for each property // we do the comparison by explicitly for each property
// to apply custom logic for different datasets // to apply custom logic for different datasets
// some of the data, we only update at intervals const shouldUpdateClock = getShouldClockUpdate(RuntimeService.previousClockUpdate, state.clock);
const isTimeToUpdate = const shouldForceTimerUpdate = getForceUpdate(RuntimeService.previousTimerUpdate, state.clock);
state.clock < RuntimeService.previousUpdate || const shouldUpdateTimer =
state.clock - RuntimeService.previousUpdate >= timerConfig.notificationRate; shouldForceTimerUpdate || getShouldTimerUpdate(RuntimeService.previousTimerValue, state.timer.current);
// some changes need an immediate update // some changes need an immediate update
const hasNewLoaded = state.eventNow?.id !== RuntimeService.previousState?.eventNow?.id; const hasNewLoaded = state.eventNow?.id !== RuntimeService.previousState?.eventNow?.id;
const hasSkippedBack = state.clock < RuntimeService.previousUpdate;
const justStarted = !RuntimeService.previousState?.timer; const justStarted = !RuntimeService.previousState?.timer;
const hasChangedPlayback = RuntimeService.previousState.timer?.playback !== state.timer.playback; const hasChangedPlayback = RuntimeService.previousState.timer?.playback !== state.timer.playback;
const hasImmediateChanges = hasNewLoaded || hasSkippedBack || justStarted || hasChangedPlayback; const hasImmediateChanges = hasNewLoaded || justStarted || hasChangedPlayback;
if (hasChangedPlayback) { if (hasChangedPlayback) {
eventStore.set('onAir', state.timer.playback !== Playback.Stop); eventStore.set('onAir', state.timer.playback !== Playback.Stop);
} }
if (hasImmediateChanges || (isTimeToUpdate && !deepEqual(RuntimeService.previousState?.timer, state.timer))) { if (hasImmediateChanges || (shouldUpdateTimer && !deepEqual(RuntimeService.previousState?.timer, state.timer))) {
RuntimeService.previousTimerUpdate = state.clock;
RuntimeService.previousTimerValue = state.timer.current;
eventStore.set('timer', state.timer); eventStore.set('timer', state.timer);
RuntimeService.previousState.timer = { ...state.timer }; RuntimeService.previousState.timer = { ...state.timer };
} }
if (hasChangedPlayback || (isTimeToUpdate && !deepEqual(RuntimeService.previousState?.runtime, state.runtime))) { if (hasChangedPlayback || (shouldUpdateTimer && !deepEqual(RuntimeService.previousState?.runtime, state.runtime))) {
eventStore.set('runtime', state.runtime); eventStore.set('runtime', state.runtime);
RuntimeService.previousState.runtime = { ...state.runtime }; RuntimeService.previousState.runtime = { ...state.runtime };
} }
@@ -549,8 +562,8 @@ function broadcastResult(_target: any, _propertyKey: string, descriptor: Propert
updateEventIfChanged('eventNext', state); updateEventIfChanged('eventNext', state);
updateEventIfChanged('publicEventNext', state); updateEventIfChanged('publicEventNext', state);
if (isTimeToUpdate) { if (shouldUpdateClock) {
RuntimeService.previousUpdate = state.clock; RuntimeService.previousClockUpdate = state.clock;
eventStore.set('clock', state.clock); eventStore.set('clock', state.clock);
saveRestoreState(state); saveRestoreState(state);
} }
@@ -0,0 +1,62 @@
import { MILLIS_PER_MINUTE } from 'ontime-utils';
import { getShouldClockUpdate, getShouldTimerUpdate } from '../rundownService.utils.js';
describe('getShouldClockUpdate()', () => {
it('should return true when we slid forwards', () => {
const previousUpdate = Date.now() - 2000; // 2 seconds ago
const now = Date.now();
const result = getShouldClockUpdate(previousUpdate, now);
expect(result).toBe(true);
});
it('should return true when we slid backwards', () => {
const previousUpdate = Date.now();
const now = Date.now() - 2000; // 2 seconds ago
const result = getShouldClockUpdate(previousUpdate, now);
expect(result).toBe(true);
});
it('should return true when clock is a second ahead', () => {
const previousUpdate = MILLIS_PER_MINUTE - 100;
const now = MILLIS_PER_MINUTE;
const result = getShouldClockUpdate(previousUpdate, now);
expect(result).toBe(true);
});
it('should return false when clock is not a second ahead and force update is not required', () => {
const previousUpdate = Date.now() - 32;
const now = Date.now();
const result = getShouldClockUpdate(previousUpdate, now);
expect(result).toBe(false);
});
});
describe('getShouldTimerUpdate', () => {
it('should return false when currentValue is null', () => {
const previousValue = 0;
const currentValue = null;
const result = getShouldTimerUpdate(previousValue, currentValue);
expect(result).toBe(false);
});
it('should return true when timer is a second ahead', () => {
const previousValue = 6500;
const currentValue = 5000;
const result = getShouldTimerUpdate(previousValue, currentValue);
expect(result).toBe(true);
});
it('should return false when timer is not a second ahead', () => {
const previousValue = 5500;
const currentValue = 5200;
const result = getShouldTimerUpdate(previousValue, currentValue);
expect(result).toBe(false);
});
it('timer value is ceiled', () => {
const previousValue = 5001; // 6
const currentValue = 4999; // 5
const result = getShouldTimerUpdate(previousValue, currentValue);
expect(result).toBe(true);
});
});
@@ -0,0 +1,43 @@
import { millisToSeconds } from 'ontime-utils';
import { timerConfig } from '../../config/config.js';
import { MaybeNumber } from 'ontime-types';
/**
* Checks whether we should update the clock value
* - clock has slid
* - we have rolled into a new seconds unit
*/
export function getShouldClockUpdate(previousUpdate: number, now: number): boolean {
const shouldForceUpdate = getForceUpdate(previousUpdate, now);
if (shouldForceUpdate) {
return true;
}
const isClockSecondAhead = millisToSeconds(now) !== millisToSeconds(previousUpdate + timerConfig.triggerAhead);
return isClockSecondAhead;
}
/**
* Checks whether we should update the timer value
* - we have rolled into a new seconds unit
*/
export function getShouldTimerUpdate(previousValue: number, currentValue: MaybeNumber): boolean {
if (currentValue === null) {
return false;
}
// we avoid trigger ahead since it can cause duplicate triggers
// we force the timer value to be negative because we need a ceiling reduction
const shouldUpdateTimer = millisToSeconds(-currentValue) !== millisToSeconds(-previousValue);
return shouldUpdateTimer;
}
/**
* In some cases we want to force an update to the timer
* - if the clock has slid back
* - if we have escaped the update rate (clock slid forward)
*/
export function getForceUpdate(previousUpdate: number, now: number): boolean {
const isClockBehind = now < previousUpdate;
const hasExceededRate = now - previousUpdate >= timerConfig.notificationRate;
return isClockBehind || hasExceededRate;
}
@@ -168,7 +168,7 @@ describe('mutation on runtimeState', () => {
load(event2, [event1, event2]); load(event2, [event1, event2]);
start(); start();
newState = getState(); newState = getState();
expect(newState.runtime.actualStart).toBe(firstStart); expect(newState.runtime.actualStart).toBeCloseTo(firstStart, 0);
// we are over-under, the difference between the schedule and the actual start // we are over-under, the difference between the schedule and the actual start
const delayBefore = event2.timeStart - newState.clock; const delayBefore = event2.timeStart - newState.clock;
expect(newState.runtime.offset).toBe(delayBefore); expect(newState.runtime.offset).toBe(delayBefore);
+8 -10
View File
@@ -23,7 +23,7 @@ const initialRuntime: Runtime = {
plannedEnd: 0, plannedEnd: 0,
actualStart: null, actualStart: null,
expectedEnd: null, expectedEnd: null,
}; } as const;
const initialTimer: TimerState = { const initialTimer: TimerState = {
addedTime: 0, addedTime: 0,
@@ -35,7 +35,7 @@ const initialTimer: TimerState = {
playback: Playback.Stop, playback: Playback.Stop,
secondaryTimer: null, secondaryTimer: null,
startedAt: null, startedAt: null,
}; } as const;
export type RuntimeState = { export type RuntimeState = {
clock: number; // realtime clock clock: number; // realtime clock
@@ -59,7 +59,7 @@ const runtimeState: RuntimeState = {
publicEventNow: null, publicEventNow: null,
eventNext: null, eventNext: null,
publicEventNext: null, publicEventNext: null,
runtime: initialRuntime, runtime: { ...initialRuntime },
timer: { ...initialTimer }, timer: { ...initialTimer },
_timer: { _timer: {
totalDelay: 0, totalDelay: 0,
@@ -78,12 +78,10 @@ export function clear() {
runtimeState.eventNext = null; runtimeState.eventNext = null;
runtimeState.publicEventNext = null; runtimeState.publicEventNext = null;
runtimeState.runtime = { runtimeState.runtime.offset = null;
...initialRuntime, runtimeState.runtime.actualStart = null;
// persist session stuff runtimeState.runtime.expectedEnd = null;
actualStart: runtimeState.runtime.actualStart, runtimeState.runtime.selectedEventIndex = null;
numEvents: runtimeState.runtime.numEvents,
};
runtimeState.timer.playback = Playback.Stop; runtimeState.timer.playback = Playback.Stop;
runtimeState.clock = clock.timeNow(); runtimeState.clock = clock.timeNow();
@@ -123,7 +121,7 @@ export function updateRundownData(rundownData: RundownData) {
runtimeState.runtime.numEvents = rundownData.numEvents; runtimeState.runtime.numEvents = rundownData.numEvents;
runtimeState.runtime.plannedStart = rundownData.firstStart; runtimeState.runtime.plannedStart = rundownData.firstStart;
runtimeState.runtime.plannedEnd = rundownData.firstStart + rundownData.totalDuration; runtimeState.runtime.plannedEnd = rundownData.lastEnd;
runtimeState.runtime.expectedEnd = getExpectedEnd(runtimeState); runtimeState.runtime.expectedEnd = getExpectedEnd(runtimeState);
} }
+2 -3
View File
@@ -19,10 +19,9 @@ export function ensureDirectory(directory: string): void {
/** /**
* Ensures that a filename ends with .json extension * Ensures that a filename ends with .json extension
*/ */
export function ensureJsonExtension(filename: string): string { export function ensureJsonExtension(filename: string | undefined): string | undefined {
if (!filename) return filename; if (!filename) return filename;
return filename.endsWith('.json') ? filename : `${filename}.json`;
return filename.includes('.json') ? filename : `${filename}.json`;
} }
/** /**
+1 -2
View File
@@ -1,9 +1,8 @@
{ {
"name": "ontime", "name": "ontime",
"version": "3.0.0", "version": "3.0.2",
"description": "Time keeping for live events", "description": "Time keeping for live events",
"keywords": [ "keywords": [
"lightdev",
"ontime", "ontime",
"timer", "timer",
"rundown" "rundown"
@@ -5,7 +5,7 @@ export const MILLIS_PER_MINUTE = 1000 * 60;
export const MILLIS_PER_HOUR = 1000 * 60 * 60; export const MILLIS_PER_HOUR = 1000 * 60 * 60;
function convertMillis(millis: MaybeNumber, conversion: number) { function convertMillis(millis: MaybeNumber, conversion: number) {
if (millis == null || millis === 0) { if (!millis) {
return 0; return 0;
} }
+210 -65
View File
@@ -146,7 +146,7 @@ importers:
version: 13.4.0(react-dom@18.2.0)(react@18.2.0) version: 13.4.0(react-dom@18.2.0)(react@18.2.0)
'@testing-library/user-event': '@testing-library/user-event':
specifier: ^14.1.1 specifier: ^14.1.1
version: 14.4.3(@testing-library/dom@9.3.4) version: 14.4.3(@testing-library/dom@10.1.0)
'@types/color': '@types/color':
specifier: ^3.0.3 specifier: ^3.0.3
version: 3.0.3 version: 3.0.3
@@ -261,15 +261,12 @@ importers:
express: express:
specifier: ^4.18.2 specifier: ^4.18.2
version: 4.18.2 version: 4.18.2
express-session:
specifier: ^1.17.3
version: 1.17.3
express-static-gzip: express-static-gzip:
specifier: ^2.1.7 specifier: ^2.1.7
version: 2.1.7 version: 2.1.7
express-validator: express-validator:
specifier: ^6.14.2 specifier: ^7.0.1
version: 6.14.2 version: 7.0.1
fast-equals: fast-equals:
specifier: ^5.0.1 specifier: ^5.0.1
version: 5.0.1 version: 5.0.1
@@ -345,7 +342,7 @@ importers:
version: 8.53.0 version: 8.53.0
eslint-plugin-prettier: eslint-plugin-prettier:
specifier: ^5.0.1 specifier: ^5.0.1
version: 5.0.1(eslint-config-prettier@9.0.0)(eslint@8.53.0)(prettier@3.0.3) version: 5.0.1(eslint@8.53.0)(prettier@3.0.3)
nodemon: nodemon:
specifier: ^2.0.20 specifier: ^2.0.20
version: 2.0.20 version: 2.0.20
@@ -369,7 +366,7 @@ importers:
version: 5.4.3 version: 5.4.3
vitest: vitest:
specifier: ^1.2.2 specifier: ^1.2.2
version: 1.2.2(@types/node@18.11.18)(jsdom@21.1.0)(sass@1.57.1) version: 1.2.2(@types/node@18.11.18)
packages/types: packages/types:
devDependencies: devDependencies:
@@ -463,6 +460,14 @@ packages:
'@babel/highlight': 7.23.4 '@babel/highlight': 7.23.4
chalk: 2.4.2 chalk: 2.4.2
/@babel/code-frame@7.24.2:
resolution: {integrity: sha512-y5+tLQyV8pg3fsiln67BVLD1P13Eg4lh5RW9mF0zUuvLrv9uIQ4MCL+CRT+FTsBlBjcIan6PGsLcBN0m3ClUyQ==}
engines: {node: '>=6.9.0'}
dependencies:
'@babel/highlight': 7.24.5
picocolors: 1.0.0
dev: true
/@babel/compat-data@7.23.5: /@babel/compat-data@7.23.5:
resolution: {integrity: sha512-uU27kfDRlhfKl+w1U6vp16IuvSLtjAxdArVXPa9BvLkrr7CYIsxH5adpHObeAGY/41+syctUWOZ140a2Rvkgjw==} resolution: {integrity: sha512-uU27kfDRlhfKl+w1U6vp16IuvSLtjAxdArVXPa9BvLkrr7CYIsxH5adpHObeAGY/41+syctUWOZ140a2Rvkgjw==}
engines: {node: '>=6.9.0'} engines: {node: '>=6.9.0'}
@@ -624,6 +629,11 @@ packages:
resolution: {integrity: sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==} resolution: {integrity: sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==}
engines: {node: '>=6.9.0'} engines: {node: '>=6.9.0'}
/@babel/helper-validator-identifier@7.24.5:
resolution: {integrity: sha512-3q93SSKX2TWCG30M2G2kwaKeTYgEUp5Snjuj8qm729SObL6nbtUldAi37qbxkD5gg3xnBio+f9nqpSepGZMvxA==}
engines: {node: '>=6.9.0'}
dev: true
/@babel/helper-validator-option@7.23.5: /@babel/helper-validator-option@7.23.5:
resolution: {integrity: sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw==} resolution: {integrity: sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw==}
engines: {node: '>=6.9.0'} engines: {node: '>=6.9.0'}
@@ -648,6 +658,16 @@ packages:
chalk: 2.4.2 chalk: 2.4.2
js-tokens: 4.0.0 js-tokens: 4.0.0
/@babel/highlight@7.24.5:
resolution: {integrity: sha512-8lLmua6AVh/8SLJRRVD6V8p73Hir9w5mJrhE+IPpILG31KKlI9iz5zmBYKcWPS59qSfgP9RaSBQSHHE81WKuEw==}
engines: {node: '>=6.9.0'}
dependencies:
'@babel/helper-validator-identifier': 7.24.5
chalk: 2.4.2
js-tokens: 4.0.0
picocolors: 1.0.0
dev: true
/@babel/parser@7.23.6: /@babel/parser@7.23.6:
resolution: {integrity: sha512-Z2uID7YJ7oNvAI20O9X0bblw7Qqs8Q2hFy0R9tAfnfLkp5MW0UH9eUvnDSnFwKZ0AvgS1ucqR4KzvVHgnke1VQ==} resolution: {integrity: sha512-Z2uID7YJ7oNvAI20O9X0bblw7Qqs8Q2hFy0R9tAfnfLkp5MW0UH9eUvnDSnFwKZ0AvgS1ucqR4KzvVHgnke1VQ==}
engines: {node: '>=6.0.0'} engines: {node: '>=6.0.0'}
@@ -697,8 +717,8 @@ packages:
regenerator-runtime: 0.13.11 regenerator-runtime: 0.13.11
dev: false dev: false
/@babel/runtime@7.24.0: /@babel/runtime@7.24.5:
resolution: {integrity: sha512-Chk32uHMg6TnQdvw2e9IlqPpFX/6NLuK0Ys2PqLb7/gL5uFn9mXvK715FGLlOLQrcO4qIkNHkvPGktzzXexsFw==} resolution: {integrity: sha512-Nms86NXrsaeU9vbBJKni6gXiEXZ4CVpYVzEjDH9Sb8vmZ3UljyA1GSOJl/6LGPO8EHLuSF9H+IxNXHPX8QHJ4g==}
engines: {node: '>=6.9.0'} engines: {node: '>=6.9.0'}
dependencies: dependencies:
regenerator-runtime: 0.14.1 regenerator-runtime: 0.14.1
@@ -3059,6 +3079,20 @@ packages:
engines: {node: '>=12'} engines: {node: '>=12'}
dev: false dev: false
/@testing-library/dom@10.1.0:
resolution: {integrity: sha512-wdsYKy5zupPyLCW2Je5DLHSxSfbIp6h80WoHOQc+RPtmPGA52O9x5MJEkv92Sjonpq+poOAtUKhh1kBGAXBrNA==}
engines: {node: '>=18'}
dependencies:
'@babel/code-frame': 7.24.2
'@babel/runtime': 7.24.5
'@types/aria-query': 5.0.4
aria-query: 5.3.0
chalk: 4.1.2
dom-accessibility-api: 0.5.16
lz-string: 1.5.0
pretty-format: 27.5.1
dev: true
/@testing-library/dom@8.19.1: /@testing-library/dom@8.19.1:
resolution: {integrity: sha512-P6iIPyYQ+qH8CvGauAqanhVnjrnRe0IZFSYCeGkSRW9q3u8bdVn2NPI+lasFyVsEQn1J/IFmp5Aax41+dAP9wg==} resolution: {integrity: sha512-P6iIPyYQ+qH8CvGauAqanhVnjrnRe0IZFSYCeGkSRW9q3u8bdVn2NPI+lasFyVsEQn1J/IFmp5Aax41+dAP9wg==}
engines: {node: '>=12'} engines: {node: '>=12'}
@@ -3073,20 +3107,6 @@ packages:
pretty-format: 27.5.1 pretty-format: 27.5.1
dev: true dev: true
/@testing-library/dom@9.3.4:
resolution: {integrity: sha512-FlS4ZWlp97iiNWig0Muq8p+3rVDjRiYE+YKGbAqXOu9nwJFFOdL00kFpz42M+4huzYi86vAK1sOOfyOG45muIQ==}
engines: {node: '>=14'}
dependencies:
'@babel/code-frame': 7.23.5
'@babel/runtime': 7.24.0
'@types/aria-query': 5.0.4
aria-query: 5.1.3
chalk: 4.1.2
dom-accessibility-api: 0.5.16
lz-string: 1.5.0
pretty-format: 27.5.1
dev: true
/@testing-library/jest-dom@5.16.5: /@testing-library/jest-dom@5.16.5:
resolution: {integrity: sha512-N5ixQ2qKpi5OLYfwQmUb/5mSV9LneAcaUfp32pn4yCnpb8r/Yz0pXFPck21dIicKmi+ta5WRAknkZCfA8refMA==} resolution: {integrity: sha512-N5ixQ2qKpi5OLYfwQmUb/5mSV9LneAcaUfp32pn4yCnpb8r/Yz0pXFPck21dIicKmi+ta5WRAknkZCfA8refMA==}
engines: {node: '>=8', npm: '>=6', yarn: '>=1'} engines: {node: '>=8', npm: '>=6', yarn: '>=1'}
@@ -3116,13 +3136,13 @@ packages:
react-dom: 18.2.0(react@18.2.0) react-dom: 18.2.0(react@18.2.0)
dev: true dev: true
/@testing-library/user-event@14.4.3(@testing-library/dom@9.3.4): /@testing-library/user-event@14.4.3(@testing-library/dom@10.1.0):
resolution: {integrity: sha512-kCUc5MEwaEMakkO5x7aoD+DLi02ehmEM2QCGWvNqAS1dV/fAvORWEjnjsEIvml59M7Y5kCkWN6fCCyPOe8OL6Q==} resolution: {integrity: sha512-kCUc5MEwaEMakkO5x7aoD+DLi02ehmEM2QCGWvNqAS1dV/fAvORWEjnjsEIvml59M7Y5kCkWN6fCCyPOe8OL6Q==}
engines: {node: '>=12', npm: '>=6'} engines: {node: '>=12', npm: '>=6'}
peerDependencies: peerDependencies:
'@testing-library/dom': '>=7.21.4' '@testing-library/dom': '>=7.21.4'
dependencies: dependencies:
'@testing-library/dom': 9.3.4 '@testing-library/dom': 10.1.0
dev: true dev: true
/@tootallnate/once@2.0.0: /@tootallnate/once@2.0.0:
@@ -3975,7 +3995,7 @@ packages:
lodash.isplainobject: 4.0.6 lodash.isplainobject: 4.0.6
lodash.union: 4.6.0 lodash.union: 4.6.0
normalize-path: 3.0.0 normalize-path: 3.0.0
readable-stream: 2.3.7 readable-stream: 2.3.8
dev: true dev: true
/archiver-utils@3.0.4: /archiver-utils@3.0.4:
@@ -4053,6 +4073,12 @@ packages:
deep-equal: 2.2.0 deep-equal: 2.2.0
dev: true dev: true
/aria-query@5.3.0:
resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==}
dependencies:
dequal: 2.0.3
dev: true
/array-flatten@1.1.1: /array-flatten@1.1.1:
resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==} resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==}
dev: false dev: false
@@ -4655,11 +4681,6 @@ packages:
resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==} resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==}
dev: false dev: false
/cookie@0.4.2:
resolution: {integrity: sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==}
engines: {node: '>= 0.6'}
dev: false
/cookie@0.5.0: /cookie@0.5.0:
resolution: {integrity: sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==} resolution: {integrity: sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==}
engines: {node: '>= 0.6'} engines: {node: '>= 0.6'}
@@ -4955,6 +4976,11 @@ packages:
engines: {node: '>= 0.8'} engines: {node: '>= 0.8'}
dev: false dev: false
/dequal@2.0.3:
resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==}
engines: {node: '>=6'}
dev: true
/destroy@1.2.0: /destroy@1.2.0:
resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==}
engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16}
@@ -5426,6 +5452,26 @@ packages:
synckit: 0.8.5 synckit: 0.8.5
dev: true dev: true
/eslint-plugin-prettier@5.0.1(eslint@8.53.0)(prettier@3.0.3):
resolution: {integrity: sha512-m3u5RnR56asrwV/lDC4GHorlW75DsFfmUcjfCYylTUs85dBRnB7VM6xG8eCMJdeDRnppzmxZVf1GEPJvl1JmNg==}
engines: {node: ^14.18.0 || >=16.0.0}
peerDependencies:
'@types/eslint': '>=8.0.0'
eslint: '>=8.0.0'
eslint-config-prettier: '*'
prettier: '>=3.0.0'
peerDependenciesMeta:
'@types/eslint':
optional: true
eslint-config-prettier:
optional: true
dependencies:
eslint: 8.53.0
prettier: 3.0.3
prettier-linter-helpers: 1.0.0
synckit: 0.8.5
dev: true
/eslint-plugin-react-hooks@4.6.0(eslint@8.53.0): /eslint-plugin-react-hooks@4.6.0(eslint@8.53.0):
resolution: {integrity: sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g==} resolution: {integrity: sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g==}
engines: {node: '>=10'} engines: {node: '>=10'}
@@ -5682,22 +5728,6 @@ packages:
jest-util: 29.3.1 jest-util: 29.3.1
dev: true dev: true
/express-session@1.17.3:
resolution: {integrity: sha512-4+otWXlShYlG1Ma+2Jnn+xgKUZTMJ5QD3YvfilX3AcocOAbIkVylSWEklzALe/+Pu4qV6TYBj5GwOBFfdKqLBw==}
engines: {node: '>= 0.8.0'}
dependencies:
cookie: 0.4.2
cookie-signature: 1.0.6
debug: 2.6.9
depd: 2.0.0
on-headers: 1.0.2
parseurl: 1.3.3
safe-buffer: 5.2.1
uid-safe: 2.1.5
transitivePeerDependencies:
- supports-color
dev: false
/express-static-gzip@2.1.7: /express-static-gzip@2.1.7:
resolution: {integrity: sha512-QOCZUC+lhPPCjIJKpQGu1Oa61Axg9Mq09Qvit8Of7kzpMuwDeMSqjjQteQS3OVw/GkENBoSBheuQDWPlngImvw==} resolution: {integrity: sha512-QOCZUC+lhPPCjIJKpQGu1Oa61Axg9Mq09Qvit8Of7kzpMuwDeMSqjjQteQS3OVw/GkENBoSBheuQDWPlngImvw==}
dependencies: dependencies:
@@ -5706,12 +5736,12 @@ packages:
- supports-color - supports-color
dev: false dev: false
/express-validator@6.14.2: /express-validator@7.0.1:
resolution: {integrity: sha512-8XfAUrQ6Y7dIIuy9KcUPCfG/uCbvREctrxf5EeeME+ulanJ4iiW71lWmm9r4YcKKYOCBMan0WpVg7FtHu4Z4Wg==} resolution: {integrity: sha512-oB+z9QOzQIE8FnlINqyIFA8eIckahC6qc8KtqLdLJcU3/phVyuhXH3bA4qzcrhme+1RYaCSwrq+TlZ/kAKIARA==}
engines: {node: '>= 8.0.0'} engines: {node: '>= 8.0.0'}
dependencies: dependencies:
lodash: 4.17.21 lodash: 4.17.21
validator: 13.7.0 validator: 13.12.0
dev: false dev: false
/express@4.18.2: /express@4.18.2:
@@ -7577,6 +7607,7 @@ packages:
/on-headers@1.0.2: /on-headers@1.0.2:
resolution: {integrity: sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==} resolution: {integrity: sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==}
engines: {node: '>= 0.8'} engines: {node: '>= 0.8'}
dev: true
/once@1.4.0: /once@1.4.0:
resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
@@ -7960,11 +7991,6 @@ packages:
resolution: {integrity: sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==} resolution: {integrity: sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==}
engines: {node: '>=10'} engines: {node: '>=10'}
/random-bytes@1.0.0:
resolution: {integrity: sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==}
engines: {node: '>= 0.8'}
dev: false
/range-parser@1.2.1: /range-parser@1.2.1:
resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==}
engines: {node: '>= 0.6'} engines: {node: '>= 0.6'}
@@ -8178,6 +8204,18 @@ packages:
string_decoder: 1.1.1 string_decoder: 1.1.1
util-deprecate: 1.0.2 util-deprecate: 1.0.2
/readable-stream@2.3.8:
resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==}
dependencies:
core-util-is: 1.0.3
inherits: 2.0.4
isarray: 1.0.0
process-nextick-args: 2.0.1
safe-buffer: 5.1.2
string_decoder: 1.1.1
util-deprecate: 1.0.2
dev: true
/readable-stream@3.6.2: /readable-stream@3.6.2:
resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==}
engines: {node: '>= 6'} engines: {node: '>= 6'}
@@ -9179,13 +9217,6 @@ packages:
resolution: {integrity: sha512-o+ORpgGwaYQXgqGDwd+hkS4PuZ3QnmqMMxRuajK/a38L6fTpcE5GPIfrf+L/KemFzfUpeUQc1rRS1iDBozvnFA==} resolution: {integrity: sha512-o+ORpgGwaYQXgqGDwd+hkS4PuZ3QnmqMMxRuajK/a38L6fTpcE5GPIfrf+L/KemFzfUpeUQc1rRS1iDBozvnFA==}
dev: true dev: true
/uid-safe@2.1.5:
resolution: {integrity: sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==}
engines: {node: '>= 0.8'}
dependencies:
random-bytes: 1.0.0
dev: false
/unbox-primitive@1.0.2: /unbox-primitive@1.0.2:
resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==} resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==}
dependencies: dependencies:
@@ -9325,8 +9356,8 @@ packages:
resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==}
dev: true dev: true
/validator@13.7.0: /validator@13.12.0:
resolution: {integrity: sha512-nYXQLCBkpJ8X6ltALua9dRrZDHVYxjJ1wgskNt1lH9fzGjs3tgojGSCBjmEPwkWS1y29+DrizMTW19Pr9uB2nw==} resolution: {integrity: sha512-c1Q0mCiPlgdTVVVIJIrBuxNicYE+t/7oKeI9MWLj3fh/uq2Pxh/3eeWbVZ4OcGW1TUf53At0njHw5SMdA3tmMg==}
engines: {node: '>= 0.10'} engines: {node: '>= 0.10'}
dev: false dev: false
@@ -9346,6 +9377,27 @@ packages:
dev: true dev: true
optional: true optional: true
/vite-node@1.2.2(@types/node@18.11.18):
resolution: {integrity: sha512-1as4rDTgVWJO3n1uHmUYqq7nsFgINQ9u+mRcXpjeOMJUmviqNKjcZB7UfRZrlM7MjYXMKpuWp5oGkjaFLnjawg==}
engines: {node: ^18.0.0 || >=20.0.0}
hasBin: true
dependencies:
cac: 6.7.14
debug: 4.3.4
pathe: 1.1.1
picocolors: 1.0.0
vite: 5.1.0(@types/node@18.11.18)
transitivePeerDependencies:
- '@types/node'
- less
- lightningcss
- sass
- stylus
- sugarss
- supports-color
- terser
dev: true
/vite-node@1.2.2(@types/node@18.11.18)(sass@1.57.1): /vite-node@1.2.2(@types/node@18.11.18)(sass@1.57.1):
resolution: {integrity: sha512-1as4rDTgVWJO3n1uHmUYqq7nsFgINQ9u+mRcXpjeOMJUmviqNKjcZB7UfRZrlM7MjYXMKpuWp5oGkjaFLnjawg==} resolution: {integrity: sha512-1as4rDTgVWJO3n1uHmUYqq7nsFgINQ9u+mRcXpjeOMJUmviqNKjcZB7UfRZrlM7MjYXMKpuWp5oGkjaFLnjawg==}
engines: {node: ^18.0.0 || >=20.0.0} engines: {node: ^18.0.0 || >=20.0.0}
@@ -9408,6 +9460,42 @@ packages:
- typescript - typescript
dev: true dev: true
/vite@5.1.0(@types/node@18.11.18):
resolution: {integrity: sha512-STmSFzhY4ljuhz14bg9LkMTk3d98IO6DIArnTY6MeBwiD1Za2StcQtz7fzOUnRCqrHSD5+OS2reg4HOz1eoLnw==}
engines: {node: ^18.0.0 || >=20.0.0}
hasBin: true
peerDependencies:
'@types/node': ^18.0.0 || >=20.0.0
less: '*'
lightningcss: ^1.21.0
sass: '*'
stylus: '*'
sugarss: '*'
terser: ^5.4.0
peerDependenciesMeta:
'@types/node':
optional: true
less:
optional: true
lightningcss:
optional: true
sass:
optional: true
stylus:
optional: true
sugarss:
optional: true
terser:
optional: true
dependencies:
'@types/node': 18.11.18
esbuild: 0.19.10
postcss: 8.4.35
rollup: 4.9.1
optionalDependencies:
fsevents: 2.3.3
dev: true
/vite@5.1.0(@types/node@18.11.18)(sass@1.57.1): /vite@5.1.0(@types/node@18.11.18)(sass@1.57.1):
resolution: {integrity: sha512-STmSFzhY4ljuhz14bg9LkMTk3d98IO6DIArnTY6MeBwiD1Za2StcQtz7fzOUnRCqrHSD5+OS2reg4HOz1eoLnw==} resolution: {integrity: sha512-STmSFzhY4ljuhz14bg9LkMTk3d98IO6DIArnTY6MeBwiD1Za2StcQtz7fzOUnRCqrHSD5+OS2reg4HOz1eoLnw==}
engines: {node: ^18.0.0 || >=20.0.0} engines: {node: ^18.0.0 || >=20.0.0}
@@ -9445,6 +9533,63 @@ packages:
fsevents: 2.3.3 fsevents: 2.3.3
dev: true dev: true
/vitest@1.2.2(@types/node@18.11.18):
resolution: {integrity: sha512-d5Ouvrnms3GD9USIK36KG8OZ5bEvKEkITFtnGv56HFaSlbItJuYr7hv2Lkn903+AvRAgSixiamozUVfORUekjw==}
engines: {node: ^18.0.0 || >=20.0.0}
hasBin: true
peerDependencies:
'@edge-runtime/vm': '*'
'@types/node': ^18.0.0 || >=20.0.0
'@vitest/browser': ^1.0.0
'@vitest/ui': ^1.0.0
happy-dom: '*'
jsdom: '*'
peerDependenciesMeta:
'@edge-runtime/vm':
optional: true
'@types/node':
optional: true
'@vitest/browser':
optional: true
'@vitest/ui':
optional: true
happy-dom:
optional: true
jsdom:
optional: true
dependencies:
'@types/node': 18.11.18
'@vitest/expect': 1.2.2
'@vitest/runner': 1.2.2
'@vitest/snapshot': 1.2.2
'@vitest/spy': 1.2.2
'@vitest/utils': 1.2.2
acorn-walk: 8.3.2
cac: 6.7.14
chai: 4.3.10
debug: 4.3.4
execa: 8.0.1
local-pkg: 0.5.0
magic-string: 0.30.5
pathe: 1.1.1
picocolors: 1.0.0
std-env: 3.6.0
strip-literal: 1.3.0
tinybench: 2.5.1
tinypool: 0.8.2
vite: 5.1.0(@types/node@18.11.18)
vite-node: 1.2.2(@types/node@18.11.18)
why-is-node-running: 2.2.2
transitivePeerDependencies:
- less
- lightningcss
- sass
- stylus
- sugarss
- supports-color
- terser
dev: true
/vitest@1.2.2(@types/node@18.11.18)(jsdom@21.1.0)(sass@1.57.1): /vitest@1.2.2(@types/node@18.11.18)(jsdom@21.1.0)(sass@1.57.1):
resolution: {integrity: sha512-d5Ouvrnms3GD9USIK36KG8OZ5bEvKEkITFtnGv56HFaSlbItJuYr7hv2Lkn903+AvRAgSixiamozUVfORUekjw==} resolution: {integrity: sha512-d5Ouvrnms3GD9USIK36KG8OZ5bEvKEkITFtnGv56HFaSlbItJuYr7hv2Lkn903+AvRAgSixiamozUVfORUekjw==}
engines: {node: ^18.0.0 || >=20.0.0} engines: {node: ^18.0.0 || >=20.0.0}
+6 -5
View File
@@ -10,7 +10,7 @@
}, },
"settings": { "settings": {
"app": "ontime", "app": "ontime",
"version": "3.0.0", "version": "3.0.0-beta.3",
"serverPort": 4001, "serverPort": 4001,
"editorKey": null, "editorKey": null,
"operatorKey": null, "operatorKey": null,
@@ -18,11 +18,12 @@
"language": "en" "language": "en"
}, },
"viewSettings": { "viewSettings": {
"overrideStyles": false,
"normalColor": "#ffffffcc",
"warningColor": "#FFAB33",
"dangerColor": "#ED3333", "dangerColor": "#ED3333",
"endMessage": "" "endMessage": "",
"freezeEnd": false,
"normalColor": "#ffffffcc",
"overrideStyles": false,
"warningColor": "#FFAB33"
}, },
"urlPresets": [], "urlPresets": [],
"customFields": {}, "customFields": {},