From 7198a880d1b431319c64da3c93df3f1c59e5b53f Mon Sep 17 00:00:00 2001 From: johnc Date: Sat, 14 Dec 2024 16:52:48 -0800 Subject: [PATCH 001/141] i1027: added contest timers to app-header files in "shared" module. --- .../components/app-header/app-header.component.html | 13 +++++++++++++ .../components/app-header/app-header.component.scss | 10 ++++++++++ .../components/app-header/app-header.component.ts | 3 +++ .../WTI-UI/src/app/modules/shared/shared.module.ts | 3 +++ 4 files changed, 29 insertions(+) diff --git a/projects/WTI-UI/src/app/modules/shared/components/app-header/app-header.component.html b/projects/WTI-UI/src/app/modules/shared/components/app-header/app-header.component.html index 470818851..d35e4c331 100644 --- a/projects/WTI-UI/src/app/modules/shared/components/app-header/app-header.component.html +++ b/projects/WTI-UI/src/app/modules/shared/components/app-header/app-header.component.html @@ -4,9 +4,17 @@ VerticalBar ICPC Programming Contest Logo + +
+ Elapsed: + {{time.days}}d {{time.hours}}h {{time.minutes}}m {{time.seconds}}s + +
+

TEAM {{teamId}}

+ + +
+ Remaining: +
+ diff --git a/projects/WTI-UI/src/app/modules/shared/components/app-header/app-header.component.scss b/projects/WTI-UI/src/app/modules/shared/components/app-header/app-header.component.scss index 5f354d056..163b99a76 100644 --- a/projects/WTI-UI/src/app/modules/shared/components/app-header/app-header.component.scss +++ b/projects/WTI-UI/src/app/modules/shared/components/app-header/app-header.component.scss @@ -14,6 +14,16 @@ header { .teamid-container { color: white; font-size: 1.5rem; + padding-left: 8px; + padding-right: 8px; + } + + /* Set the container holding the contest clock value(s) to medium, white font */ + .clock-container { + color: white; + font-size: 1.0rem; + padding-left: 8px; + padding-right: 8px; } nav a { diff --git a/projects/WTI-UI/src/app/modules/shared/components/app-header/app-header.component.ts b/projects/WTI-UI/src/app/modules/shared/components/app-header/app-header.component.ts index 912435306..dae851204 100644 --- a/projects/WTI-UI/src/app/modules/shared/components/app-header/app-header.component.ts +++ b/projects/WTI-UI/src/app/modules/shared/components/app-header/app-header.component.ts @@ -14,6 +14,9 @@ export class AppHeaderComponent { //Return a boolean indicating whether or not to show a teamId in the header get showTeamId(): boolean { return this._authService.isLoggedIn; } + //Return a boolean indicating whether or not to show the contest clock(s) in the header + get showClocks(): boolean { return this._authService.isLoggedIn; } + /* Return a string containing the "team id" -- that is, the PC2 team account number with the leading "team" removed */ get teamId(): string { diff --git a/projects/WTI-UI/src/app/modules/shared/shared.module.ts b/projects/WTI-UI/src/app/modules/shared/shared.module.ts index 434aae915..c4d736029 100644 --- a/projects/WTI-UI/src/app/modules/shared/shared.module.ts +++ b/projects/WTI-UI/src/app/modules/shared/shared.module.ts @@ -13,6 +13,7 @@ import {MatInputModule} from '@angular/material/input'; import {MatSelectModule} from '@angular/material/select'; import {MatSnackBarModule} from '@angular/material/snack-bar'; import { AboutWtiComponent } from './components/about-wti/about-wti.component'; +import { CountdownModule } from 'ngx-countdown'; @NgModule({ declarations: [ @@ -32,6 +33,7 @@ import { AboutWtiComponent } from './components/about-wti/about-wti.component'; MatInputModule, MatSelectModule, MatSnackBarModule, + CountdownModule ], exports: [ AppHeaderComponent, @@ -44,6 +46,7 @@ import { AboutWtiComponent } from './components/about-wti/about-wti.component'; MatInputModule, MatSelectModule, MatSnackBarModule, + CountdownModule ] }) export class SharedModule { } From 28836d8a6538bfd77f5ec402abc4b93a178a71d2 Mon Sep 17 00:00:00 2001 From: johnc Date: Sat, 14 Dec 2024 16:53:39 -0800 Subject: [PATCH 002/141] i1027: added ngx-countdown@v16.0 to package.json --- projects/WTI-UI/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/projects/WTI-UI/package.json b/projects/WTI-UI/package.json index 86a8a1dd7..d85d926f5 100644 --- a/projects/WTI-UI/package.json +++ b/projects/WTI-UI/package.json @@ -22,6 +22,7 @@ "@angular/platform-browser-dynamic": "^16.2.11", "@angular/router": "^16.2.11", "core-js": "^3.33.1", + "ngx-countdown": "16.0", "rxjs": "^6.5.3 || ^7.4.0", "tslib": "^1.9.0", "zone.js": "^0.13.X" From 7df2b2194531df9eae73c8bb47b553307e6540fa Mon Sep 17 00:00:00 2001 From: johnc Date: Tue, 17 Dec 2024 14:56:48 -0800 Subject: [PATCH 003/141] i1027: added various attempts to get HTML properly displaying clocks. --- .../app-header/app-header.component.html | 28 +++++++++++++++++-- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/projects/WTI-UI/src/app/modules/shared/components/app-header/app-header.component.html b/projects/WTI-UI/src/app/modules/shared/components/app-header/app-header.component.html index d35e4c331..ddb2acfa7 100644 --- a/projects/WTI-UI/src/app/modules/shared/components/app-header/app-header.component.html +++ b/projects/WTI-UI/src/app/modules/shared/components/app-header/app-header.component.html @@ -6,9 +6,30 @@
- Elapsed: + Elapsed:
+ +
{{ elapsedSecs }}
+ + +
{{ getElapsedTimeAsDate() | elapsedTime }}
+ + + + + + + +
@@ -24,7 +45,8 @@
- Remaining: + Remaining:
+
From 198fe1c4a7267a82073c8aa480a0f3d0805e3277 Mon Sep 17 00:00:00 2001 From: johnc Date: Tue, 17 Dec 2024 14:58:17 -0800 Subject: [PATCH 004/141] i1027: added setInterval object to gen timer ticks; added date func. --- .../app-header/app-header.component.ts | 24 ++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/projects/WTI-UI/src/app/modules/shared/components/app-header/app-header.component.ts b/projects/WTI-UI/src/app/modules/shared/components/app-header/app-header.component.ts index dae851204..4fcca263a 100644 --- a/projects/WTI-UI/src/app/modules/shared/components/app-header/app-header.component.ts +++ b/projects/WTI-UI/src/app/modules/shared/components/app-header/app-header.component.ts @@ -1,5 +1,6 @@ import { Component } from '@angular/core'; import { AuthService } from 'src/app/modules/core/auth/auth.service'; +import { ElapsedTimePipe } from 'src/app/modules/core/services/elapsedTimePipe.service'; @Component({ selector: 'app-header', @@ -24,6 +25,27 @@ export class AppHeaderComponent { let teamId = acctId.substr(4); return teamId; } + + elapsedSecs = 0 ; + + getElapsedTimeAsDate(): Date { + let newDate = new Date(this.elapsedSecs * 1000); + console.log(newDate.toString()); + return newDate ; + } - constructor(private _authService: AuthService) { } + + constructor(private _authService: AuthService) { + + setInterval( + //execute this function at the specified interval: + () => { + //add 1 second to the counter since 1000msec have passed + this.elapsedSecs += 1 ; + console.log(`Seconds: ${this.elapsedSecs}`); + }, + 1000 // 1000 milliseconds = 1 second interval + ); + + } } From 0d612ab52d3250c390cc8bf3175826af2af8765b Mon Sep 17 00:00:00 2001 From: johnc Date: Tue, 17 Dec 2024 14:59:40 -0800 Subject: [PATCH 005/141] i1027: added attempts to access new ElapsedTimePipe from core module. --- projects/WTI-UI/src/app/modules/core/core.module.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/projects/WTI-UI/src/app/modules/core/core.module.ts b/projects/WTI-UI/src/app/modules/core/core.module.ts index 304c557ef..7d827f7b8 100644 --- a/projects/WTI-UI/src/app/modules/core/core.module.ts +++ b/projects/WTI-UI/src/app/modules/core/core.module.ts @@ -15,6 +15,8 @@ import { WebsocketMockService } from './services/websocket.mock.service'; import { IWebsocketService } from './abstract-services/i-websocket.service'; import { UiHelperService } from './services/ui-helper.service'; import { SharedModule } from '../shared/shared.module'; +import { CommonModule } from '@angular/common'; +import { ElapsedTimePipe } from './services/elapsedTimePipe.service'; export function TeamsServiceFactory(http: HttpClient) { if (environment.useMock) { return new TeamsMockService(); } @@ -41,10 +43,16 @@ export function WebsocketServiceFactory(injector: Injector, authService: AuthSer AuthGuard, UiHelperService ], + declarations: [ + ElapsedTimePipe + ], imports: [ HttpClientModule, - SharedModule + SharedModule, + CommonModule + ], + exports: [ + ElapsedTimePipe ], - exports: [], }) export class CoreModule { } From ac3ea6cf091cde3300da2504ee18c311cae85c5a Mon Sep 17 00:00:00 2001 From: johnc Date: Tue, 17 Dec 2024 15:01:27 -0800 Subject: [PATCH 006/141] i1027: added attempts to access ElapsedTimePipe from shared module. --- .../WTI-UI/src/app/modules/shared/shared.module.ts | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/projects/WTI-UI/src/app/modules/shared/shared.module.ts b/projects/WTI-UI/src/app/modules/shared/shared.module.ts index c4d736029..3e5f52b71 100644 --- a/projects/WTI-UI/src/app/modules/shared/shared.module.ts +++ b/projects/WTI-UI/src/app/modules/shared/shared.module.ts @@ -8,12 +8,14 @@ import { AppFooterComponent } from './components/app-footer/app-footer.component import { RouterModule } from '@angular/router'; import { JudgementSelectorComponent } from './components/judgement-selector/judgement-selector.component'; import { MatDialogModule } from '@angular/material/dialog'; -import {MatFormFieldModule} from '@angular/material/form-field'; -import {MatInputModule} from '@angular/material/input'; -import {MatSelectModule} from '@angular/material/select'; -import {MatSnackBarModule} from '@angular/material/snack-bar'; +import { MatFormFieldModule } from '@angular/material/form-field'; +import { MatInputModule } from '@angular/material/input'; +import { MatSelectModule } from '@angular/material/select'; +import { MatSnackBarModule } from '@angular/material/snack-bar'; import { AboutWtiComponent } from './components/about-wti/about-wti.component'; import { CountdownModule } from 'ngx-countdown'; +import { BrowserModule } from '@angular/platform-browser'; +import { ElapsedTimePipe } from 'src/app/modules/core/services/elapsedTimePipe.service'; @NgModule({ declarations: [ @@ -33,7 +35,9 @@ import { CountdownModule } from 'ngx-countdown'; MatInputModule, MatSelectModule, MatSnackBarModule, - CountdownModule + CountdownModule, + BrowserModule, + ElapsedTimePipe ], exports: [ AppHeaderComponent, From 36ce45fcf1e43ddd3c464b8c0554f4b1357a8e0d Mon Sep 17 00:00:00 2001 From: johnc Date: Tue, 17 Dec 2024 15:02:37 -0800 Subject: [PATCH 007/141] i1027: added new ElapsedTimePipe for formatting dates into dd:hh:mm:ss. --- .../core/services/elapsedTimePipe.service.ts | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 projects/WTI-UI/src/app/modules/core/services/elapsedTimePipe.service.ts diff --git a/projects/WTI-UI/src/app/modules/core/services/elapsedTimePipe.service.ts b/projects/WTI-UI/src/app/modules/core/services/elapsedTimePipe.service.ts new file mode 100644 index 000000000..ddaee036b --- /dev/null +++ b/projects/WTI-UI/src/app/modules/core/services/elapsedTimePipe.service.ts @@ -0,0 +1,28 @@ +import { Pipe, PipeTransform } from '@angular/core'; + +//coded generated by Google AI +//Explanation: +//The pipe takes a Date or a timestamp as input. +//It calculates the difference in milliseconds between the given date and the current time. +//Then, it extracts days, hours, minutes, and seconds from the difference. +//Finally, it formats the output string. + +@Pipe({ + name: 'elapsedTime' +}) +export class ElapsedTimePipe implements PipeTransform { + + transform(value: Date | number): string { + const now = new Date().getTime(); + const then = new Date(value).getTime(); + + const diff = now - then; + + const days = Math.floor(diff / (1000 * 60 * 60 * 24)); + const hours = Math.floor((diff % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)); + const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60)); + const seconds = Math.floor((diff % (1000 * 60)) / 1000); + + return `${days}d ${hours}h ${minutes}m ${seconds}s`; + } +} \ No newline at end of file From 39786ef6a1f8631f7b53fdb888528da58a1b7ded Mon Sep 17 00:00:00 2001 From: John Clevenger Date: Wed, 18 Dec 2024 14:18:20 -0800 Subject: [PATCH 008/141] i1027: copy 'constant.ts' into i1027 branch from PR871 branch. --- projects/WTI-UI/src/constants.ts | 58 ++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 projects/WTI-UI/src/constants.ts diff --git a/projects/WTI-UI/src/constants.ts b/projects/WTI-UI/src/constants.ts new file mode 100644 index 000000000..2e055ede0 --- /dev/null +++ b/projects/WTI-UI/src/constants.ts @@ -0,0 +1,58 @@ +//App-wide constants for the PC2 WTI-UI project + +/** + * Global flag indicating whether to display debug messages on the browser console. + * Set to null to disable debugging output; any non-null value causes debugging output. + * NOTE: this flag should probably be set to null when creating a production release. + */ +export let DEBUG_MODE = true ; + +/** + * The key under which the currently-active WTI page is stored in sessionStorage. + */ +export const CURRENT_PAGE_KEY = 'curPageKey'; + +/** + * The storageSession value indicating the RUNS page is "current". + */ +export const RUNS_PAGE = 'runs'; + +/** + * The storageSession value indicating the OPTIONS page is "current". + */ +export const OPTIONS_PAGE = 'options'; + +/** + * The key under which the OPTIONS page details (that is, current option values) are stored in sessionStorage. + */ +export const OPTIONS_DETAILS_KEY = 'optionsDetails'; + +/** + * The optionsDetails key for the clarifications-notifications-enabled option + */ +export const CLARS_ENABLED_OPTIONS_KEY = 'clarsNotificationsEnabled'; + +/** + * The optionsDetails key for the runs-notifications-enabled option + */ +export const RUNS_ENABLED_OPTIONS_KEY = 'runsNotificationsEnabled'; + +/** + * The storageSession value indicating the CLARIFICATIONS page is "current". + */ +export const CLARIFICATIONS_PAGE = 'clarifications'; + +/** + * The storageSession value indicating the SCOREBOARD page is "current". + */ +export const SCOREBOARD_PAGE = 'scoreboard'; + +/** + * The key under which the "connection token" for websocket messages is stored in sessionStorage. + */ +export const CONNECTION_TOKEN_KEY = 'token'; + +/** + * The key under which the user name for the current websocket connection is stored in sessionStorage. + */ +export const CONNECTION_USERNAME_KEY = 'username'; \ No newline at end of file From 9ac819e8b6f964f399e44a0ea4ec075134f6dbd5 Mon Sep 17 00:00:00 2001 From: John Clevenger Date: Wed, 18 Dec 2024 14:26:06 -0800 Subject: [PATCH 009/141] i1027: added method getContestClock() to ContestServices (to both... the abstract and concrete versions) --- .../modules/core/abstract-services/i-contest.service.ts | 6 +++++- .../src/app/modules/core/services/contest.service.ts | 8 ++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/projects/WTI-UI/src/app/modules/core/abstract-services/i-contest.service.ts b/projects/WTI-UI/src/app/modules/core/abstract-services/i-contest.service.ts index 253e18741..73f262c5e 100644 --- a/projects/WTI-UI/src/app/modules/core/abstract-services/i-contest.service.ts +++ b/projects/WTI-UI/src/app/modules/core/abstract-services/i-contest.service.ts @@ -1,11 +1,13 @@ import { Observable, Subject } from 'rxjs'; import { ContestLanguage } from '../models/contest-language'; import { ContestProblem } from '../models/contest-problem'; +import { ContestClock } from '../models/contest-clock'; import { Clarification } from '../models/clarification'; export abstract class IContestService { clarificationsUpdated = new Subject(); - contestClock = new Subject(); + contestClock = new Subject(); //note that this is a WTI-UI-specific object used in conjunction with Websocket "clock" messages; + //it is NOT a "ContestClock" object in the sense of classes defined in core/models. standingsUpdated = new Subject(); isContestRunning = false; @@ -18,6 +20,8 @@ export abstract class IContestService { abstract getClarifications(): Observable; abstract getIsContestRunning(): Observable; + + abstract getContestClock(): Observable; //note that this refers to a "ContestClock" model object, not the "contestClock" subject above abstract getStandings(): Observable; diff --git a/projects/WTI-UI/src/app/modules/core/services/contest.service.ts b/projects/WTI-UI/src/app/modules/core/services/contest.service.ts index 3888498cf..e1888991c 100644 --- a/projects/WTI-UI/src/app/modules/core/services/contest.service.ts +++ b/projects/WTI-UI/src/app/modules/core/services/contest.service.ts @@ -5,6 +5,7 @@ import { ContestLanguage } from '../models/contest-language'; import { HttpClient } from '@angular/common/http'; import { environment } from 'src/environments/environment'; import { ContestProblem } from '../models/contest-problem'; +import { ContestClock } from '../models/contest-clock'; import { Clarification } from '../models/clarification'; @Injectable() @@ -38,6 +39,13 @@ export class ContestService extends IContestService { return this._httpClient.get(`${environment.baseUrl}/contest/isRunning`); } + /** This method returns an Observable "ContestClock" object -- a WTI-UI model corresponding to the PC2 "ContestTime" class, + * which itself encapsulates the "contest clock" on the PC2 server. + */ + getContestClock(): Observable { + return this._httpClient.get(`${environment.baseUrl}/contest/contestclock`); + } + getStandings(): Observable { console.log("ContestService.getStandings():") if (!this.standingsAreCurrent) { From 2335da071076f2ce3ab785cea945a8fc8abe745a Mon Sep 17 00:00:00 2001 From: John Clevenger Date: Wed, 18 Dec 2024 14:30:07 -0800 Subject: [PATCH 010/141] i1027: added ContestClockModel to hold the WTI-API rep of ContestTime. --- .../src/main/models/ContestClockModel.java | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 projects/WTI-API/src/main/models/ContestClockModel.java diff --git a/projects/WTI-API/src/main/models/ContestClockModel.java b/projects/WTI-API/src/main/models/ContestClockModel.java new file mode 100644 index 000000000..260c1c9f5 --- /dev/null +++ b/projects/WTI-API/src/main/models/ContestClockModel.java @@ -0,0 +1,43 @@ +package models; + +import edu.csus.ecs.pc2.core.model.ContestTime; + +/** + * This class encapsulates the features of the PC2 "contest clock" (class {@link ContestTime}) which need to be able to be passed + * to the WTI-UI. + * + * @author JohnC + * + */ +public class ContestClockModel { + + private boolean isRunning; + private long contestLengthInSecs; + private long elapsedSecs; //total time in seconds that the contest has been running -- does NOT include time during any "pauses" + private long wallClockStartTime; //unix timestamp when the contest actually started -- msec since the Epoch. Does not change due to "pauses" + + public ContestClockModel(boolean isRunning, long contestLengthInSecs, long elapsedSecs, long wallClockStartTime) { + this.isRunning = isRunning; + this.contestLengthInSecs = contestLengthInSecs; + this.elapsedSecs = elapsedSecs; + this.wallClockStartTime = wallClockStartTime; + } + + public ContestClockModel() { } + + public boolean isRunning() { + return isRunning; + } + + public long getContestLengthInSecs() { + return contestLengthInSecs; + } + + public long getElapsedSecs() { + return elapsedSecs; + } + + public long getWallClockStartTime() { + return wallClockStartTime; + } +} From d189a1cabfdaf1c3548c5b1d0e00ac6384e269f6 Mon Sep 17 00:00:00 2001 From: John Clevenger Date: Wed, 18 Dec 2024 15:19:28 -0800 Subject: [PATCH 011/141] i1027: added support for getting contest start time via the PC2 API. --- src/edu/csus/ecs/pc2/api/IContestClock.java | 18 +++++++++++++++++- .../ContestTimeImplementation.java | 11 +++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/src/edu/csus/ecs/pc2/api/IContestClock.java b/src/edu/csus/ecs/pc2/api/IContestClock.java index f7e3239b8..89ec83d5f 100644 --- a/src/edu/csus/ecs/pc2/api/IContestClock.java +++ b/src/edu/csus/ecs/pc2/api/IContestClock.java @@ -1,10 +1,13 @@ // Copyright (C) 1989-2019 PC2 Development Team: John Clevenger, Douglas Lane, Samir Ashoo, and Troy Boudreau. package edu.csus.ecs.pc2.api; +import java.util.Calendar; + /** * This interface describes the PC2 API view of Contest Time information. * It provides methods for accessing various time-related aspects of the contest, including - * how much time has elapsed, how much time remains, and how long the contest is scheduled to last. + * how much time has elapsed, how much time remains, how long the contest is scheduled to last, + * and (once the contest has been started) the date/time at which it was started. *

* Note that under the current implementation, an {@link IContestClock} object is static once it * is obtained; to get a current copy of the contest time information a new {@link IContestClock} object @@ -62,6 +65,19 @@ public interface IContestClock { * @return true if the contest clock is currently running; false otherwise. */ boolean isContestClockRunning (); + + /** + * Returns a {@link Calendar} containing the date/time that the contest actually started, + * or null if the contest has not ever been started. + * Note that the returned value is independent of (has nothing to do with) the "Scheduled Start Time"; + * the latter is only relevant before the contest actually starts. + * Note also that once the contest is started, the value returned by this method never changes; + * specifically, it is not affected by any subsequent "pause" (a.k.a. "stop contest") operations, + * nor by any "start contest" operations following a pause; the returned value always indicates + * when the contest was FIRST started. + * @return a {@link Calendar} object indicating the date/time that the contest actually started. + */ + public Calendar getContestStartTime(); /** * Check whether this ContestClock object is the same as some other ContestClock. diff --git a/src/edu/csus/ecs/pc2/api/implementation/ContestTimeImplementation.java b/src/edu/csus/ecs/pc2/api/implementation/ContestTimeImplementation.java index f50a105bb..46d3c09de 100644 --- a/src/edu/csus/ecs/pc2/api/implementation/ContestTimeImplementation.java +++ b/src/edu/csus/ecs/pc2/api/implementation/ContestTimeImplementation.java @@ -1,6 +1,8 @@ // Copyright (C) 1989-2019 PC2 Development Team: John Clevenger, Douglas Lane, Samir Ashoo, and Troy Boudreau. package edu.csus.ecs.pc2.api.implementation; +import java.util.Calendar; + import edu.csus.ecs.pc2.api.IContestClock; import edu.csus.ecs.pc2.core.model.ContestTime; import edu.csus.ecs.pc2.core.model.ElementId; @@ -36,6 +38,15 @@ public long getElapsedSecs() { return contestTime.getElapsedSecs(); } + /** + * Returns the time at which the contest was first started, or null if the contest has never + * been started. Note that the returned time is never affected by any "pause" operations + * which occur after the contest has been first started. + */ + public Calendar getContestStartTime() { + return contestTime.getContestStartTime(); + } + public boolean isContestClockRunning() { return contestTime.isContestRunning(); } From 912fb68dda3386e98a15b6dd37fd80f050fe8510 Mon Sep 17 00:00:00 2001 From: John Clevenger Date: Wed, 18 Dec 2024 15:38:29 -0800 Subject: [PATCH 012/141] i1027: add support for URL /contestclock to WTI-API server. --- .../main/controllers/ContestController.java | 76 +++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/projects/WTI-API/src/main/controllers/ContestController.java b/projects/WTI-API/src/main/controllers/ContestController.java index 4b6d3d011..fefdfad09 100644 --- a/projects/WTI-API/src/main/controllers/ContestController.java +++ b/projects/WTI-API/src/main/controllers/ContestController.java @@ -25,6 +25,7 @@ import config.ServerInit; import edu.csus.ecs.pc2.api.IClarification; import edu.csus.ecs.pc2.api.IClient; +import edu.csus.ecs.pc2.api.IContestClock; import edu.csus.ecs.pc2.api.IJudgement; import edu.csus.ecs.pc2.api.ILanguage; import edu.csus.ecs.pc2.api.IProblem; @@ -48,6 +49,7 @@ import io.swagger.annotations.ApiResponses; import io.swagger.annotations.Authorization; import models.ClarificationModel; +import models.ContestClockModel; import models.LanguageModel; import models.ProblemModel; import models.ServerErrorResponseModel; @@ -411,6 +413,80 @@ public Response isRunning( .type(MediaType.APPLICATION_JSON).build(); } + + /*** + * This method returns a "ContestClock" object encapsulating the current state of the PC2 contest clock + * as maintained by the PC2 class "ContestTime". + * It first checks to see that the user (team) specified by the received "key" is currently logged in. If so, the method + * returns the current contest time information, obtained from the PC2 {@link ServerConnection} for the team. + * + * @param key a String containing a key which uniquely identifies the team making the request. + * The value of "key" is obtained from the HTTP header parameter "team_id". + * + * @return Response of: + * 401 (unauthorized) if team's credentials are incorrect (i.e. the team is not logged in or is not allowed to make such a request); + * 500 (INTERNAL_SERVER_ERROR) if an error occurs in fetching the requested data from the PC2 server; + * otherwise 200 (OK) and a JSON string containing a {@link ContestClock} is returned. + */ + @Path("/contestclock") + @GET + @ApiOperation(value = "contestclock", + notes = "Gets the PC2 contest clock (time info).") + @ApiResponses({ + @ApiResponse(code = 200, message = "Returns a contest clock object.", response = ContestClockModel.class), + @ApiResponse(code = 401, message = "Returned if invalid credentials are supplied", response = ServerErrorResponseModel.class) + }) + + public Response contestClock(@ApiParam(value="token used by logged in users to access teams information", + required = true) @HeaderParam("team_id")String key) { + + ServerConnection userInformation = connections.get(key); + + //verify the user is logged in + try { + // make sure we have connection information for this user (i.e. that the user is logged in) + if (userInformation == null) { + throw new NotLoggedInException(); + } + } catch (NotLoggedInException e1) { + return Response.status(Response.Status.UNAUTHORIZED) + .entity(new ServerErrorResponseModel(Response.Status.UNAUTHORIZED, "Unauthorized user request")) + .type(MediaType.APPLICATION_JSON).build(); + } + + //the ContestClock object to be returned in the response to the HTTP request + ContestClockModel returnableContestClock ; + + try { + //get the contest clock from the PC2 Server via the PC2 API ServerConnection + IContestClock contestClock = userInformation.getContest().getContestClock(); + + //retrieve the relevant fields from the PC2 contest clock + boolean isRunning = contestClock.isContestClockRunning(); + long contestLengthInSecs = contestClock.getContestLengthSecs(); + long elapsedSecs = contestClock.getElapsedSecs(); + long wallClockStartTime = contestClock.getContestStartTime().getTimeInMillis(); + + returnableContestClock = new ContestClockModel(isRunning,contestLengthInSecs, elapsedSecs, wallClockStartTime); + + } + catch(NotLoggedInException e) { + return Response.status(Response.Status.UNAUTHORIZED) + .entity(new ServerErrorResponseModel(Response.Status.UNAUTHORIZED, "Unauthorized user request")) + .type(MediaType.APPLICATION_JSON).build(); + } + catch(NullPointerException e) { + logger.severe(e.getMessage()); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity(new ServerErrorResponseModel(Response.Status.INTERNAL_SERVER_ERROR, "NullPointerException in ContestController.clarifications()")) + .type(MediaType.APPLICATION_JSON).build(); + } + + return Response.ok() + .entity(returnableContestClock) + .type(MediaType.APPLICATION_JSON).build(); + } + /*** * This method returns a list of all the clarifications in the PC^2 contest submitted by the specified team. * It first checks to see that the user (team) specified by the received "key" is currently logged in and that the From 9705890d4cc2df83c52ecdbe9fe4f0d9d620e0ed Mon Sep 17 00:00:00 2001 From: John Clevenger Date: Wed, 18 Dec 2024 15:42:03 -0800 Subject: [PATCH 013/141] i1027: fix minor typo in javadoc comment. --- src/edu/csus/ecs/pc2/api/IContest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/edu/csus/ecs/pc2/api/IContest.java b/src/edu/csus/ecs/pc2/api/IContest.java index 7748ef0db..c920cd73a 100644 --- a/src/edu/csus/ecs/pc2/api/IContest.java +++ b/src/edu/csus/ecs/pc2/api/IContest.java @@ -299,7 +299,7 @@ public interface IContest { /** * Get an {@link IContestClock} object containing contest time-related information. * The {@link IContestClock} object can be queried for values such as the amount of - * time elasped so far in the contest, the amount of time remaining in the contest, and + * time elapsed so far in the contest, the amount of time remaining in the contest, and * whether the contest clock is currently "paused" or not. *

* Note that the {@link IContestClock} object returned by the current implementation of From 6e386d87ebe4b92baff5dfb6937736dff6f48cd6 Mon Sep 17 00:00:00 2001 From: John Clevenger Date: Wed, 18 Dec 2024 15:51:06 -0800 Subject: [PATCH 014/141] i1027: add elapsedTimePipe to core/services -- an Angular "Pipe" that... can be used to transform Date objects into strings displaying day/hours/minutes/seconds. --- .../src/app/modules/core/core.module.ts | 7 ++-- .../core/services/elapsedTimePipe.service.ts | 35 ++++++++++++++----- 2 files changed, 28 insertions(+), 14 deletions(-) diff --git a/projects/WTI-UI/src/app/modules/core/core.module.ts b/projects/WTI-UI/src/app/modules/core/core.module.ts index 7d827f7b8..fe56e7900 100644 --- a/projects/WTI-UI/src/app/modules/core/core.module.ts +++ b/projects/WTI-UI/src/app/modules/core/core.module.ts @@ -15,7 +15,6 @@ import { WebsocketMockService } from './services/websocket.mock.service'; import { IWebsocketService } from './abstract-services/i-websocket.service'; import { UiHelperService } from './services/ui-helper.service'; import { SharedModule } from '../shared/shared.module'; -import { CommonModule } from '@angular/common'; import { ElapsedTimePipe } from './services/elapsedTimePipe.service'; export function TeamsServiceFactory(http: HttpClient) { @@ -41,15 +40,13 @@ export function WebsocketServiceFactory(injector: Injector, authService: AuthSer { provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true }, { provide: IWebsocketService, useFactory: WebsocketServiceFactory, deps: [Injector, AuthService] }, AuthGuard, - UiHelperService - ], - declarations: [ + UiHelperService, ElapsedTimePipe ], imports: [ HttpClientModule, SharedModule, - CommonModule + ElapsedTimePipe ], exports: [ ElapsedTimePipe diff --git a/projects/WTI-UI/src/app/modules/core/services/elapsedTimePipe.service.ts b/projects/WTI-UI/src/app/modules/core/services/elapsedTimePipe.service.ts index ddaee036b..ad3dc7287 100644 --- a/projects/WTI-UI/src/app/modules/core/services/elapsedTimePipe.service.ts +++ b/projects/WTI-UI/src/app/modules/core/services/elapsedTimePipe.service.ts @@ -1,20 +1,27 @@ import { Pipe, PipeTransform } from '@angular/core'; -//coded generated by Google AI -//Explanation: -//The pipe takes a Date or a timestamp as input. +//This class defines a "pipe" which accepts a "Date" object and returns +// a formatted string representing the number of days/hours/minutes/seconds that have +// elapsed between the specified Date and "now". +//The "pipe" is suitable for use, for example, in an Angular HTML template (see app-header.component.html). +//It is based on code generated by Google AI. + //It calculates the difference in milliseconds between the given date and the current time. -//Then, it extracts days, hours, minutes, and seconds from the difference. -//Finally, it formats the output string. +//Then it extracts days, hours, minutes, and seconds from the difference. +//Finally, it formats the output string. "Days" are only included in the output if they are greater than zero. @Pipe({ - name: 'elapsedTime' + name: 'elapsedTime', + standalone: true }) export class ElapsedTimePipe implements PipeTransform { - transform(value: Date | number): string { + /** + Returns a string representing the elapsed time between the input Date and "now". + */ + transform(startDate: Date): string { const now = new Date().getTime(); - const then = new Date(value).getTime(); + const then = startDate.getTime(); const diff = now - then; @@ -23,6 +30,16 @@ export class ElapsedTimePipe implements PipeTransform { const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60)); const seconds = Math.floor((diff % (1000 * 60)) / 1000); - return `${days}d ${hours}h ${minutes}m ${seconds}s`; + const formattedHours = hours.toString().padStart(2, '0'); + const formattedMinutes = minutes.toString().padStart(2, '0'); + const formattedSeconds = seconds.toString().padStart(2, '0'); + + + if (days > 0) { + return `${days}d ${formattedHours}:${formattedMinutes}:${formattedSeconds}`; + } else { + return `${formattedHours}:${formattedMinutes}:${formattedSeconds}`; + } + } } \ No newline at end of file From 443238fe46774fdcc17815a7024be7290df411da Mon Sep 17 00:00:00 2001 From: John Clevenger Date: Wed, 18 Dec 2024 15:56:45 -0800 Subject: [PATCH 015/141] i1027: clean up app-header HTML to display elapsed time in two ways: - a count of the number of elapsed seconds (planned for removal), and - a string showing days plus hh:mm:ss (the real plan). --- .../app-header/app-header.component.html | 27 +++---------------- 1 file changed, 4 insertions(+), 23 deletions(-) diff --git a/projects/WTI-UI/src/app/modules/shared/components/app-header/app-header.component.html b/projects/WTI-UI/src/app/modules/shared/components/app-header/app-header.component.html index ddb2acfa7..132082b19 100644 --- a/projects/WTI-UI/src/app/modules/shared/components/app-header/app-header.component.html +++ b/projects/WTI-UI/src/app/modules/shared/components/app-header/app-header.component.html @@ -7,29 +7,10 @@

Elapsed:
- -
{{ elapsedSecs }}
- - -
{{ getElapsedTimeAsDate() | elapsedTime }}
- - - - - - - - + {{ elapsedSecs }}
+ + {{ getStartDate() | elapsedTime }}
From 95b5e7356c34523818773720e3bfd7892a82b64d Mon Sep 17 00:00:00 2001 From: John Clevenger Date: Fri, 20 Dec 2024 18:16:28 -0800 Subject: [PATCH 016/141] i1027: minor doc (comments) added. --- projects/WTI-API/src/main/controllers/ContestController.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/projects/WTI-API/src/main/controllers/ContestController.java b/projects/WTI-API/src/main/controllers/ContestController.java index fefdfad09..a5de70b5d 100644 --- a/projects/WTI-API/src/main/controllers/ContestController.java +++ b/projects/WTI-API/src/main/controllers/ContestController.java @@ -467,8 +467,9 @@ public Response contestClock(@ApiParam(value="token used by logged in users to a long elapsedSecs = contestClock.getElapsedSecs(); long wallClockStartTime = contestClock.getContestStartTime().getTimeInMillis(); + //construct a ContestClock containing the PC2 Server clock values returnableContestClock = new ContestClockModel(isRunning,contestLengthInSecs, elapsedSecs, wallClockStartTime); - + } catch(NotLoggedInException e) { return Response.status(Response.Status.UNAUTHORIZED) @@ -482,6 +483,7 @@ public Response contestClock(@ApiParam(value="token used by logged in users to a .type(MediaType.APPLICATION_JSON).build(); } + //return the PC2 ContestClock, mapped into JSON return Response.ok() .entity(returnableContestClock) .type(MediaType.APPLICATION_JSON).build(); From 6ed9b97d78204ff1f8ed6013d149cffe7477ec68 Mon Sep 17 00:00:00 2001 From: John Clevenger Date: Fri, 20 Dec 2024 18:18:14 -0800 Subject: [PATCH 017/141] i1027: added newly-required getContestClock() method to mock service. --- .../src/app/modules/core/services/contest.mock.service.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/projects/WTI-UI/src/app/modules/core/services/contest.mock.service.ts b/projects/WTI-UI/src/app/modules/core/services/contest.mock.service.ts index a3e1b22dc..a543aac9b 100644 --- a/projects/WTI-UI/src/app/modules/core/services/contest.mock.service.ts +++ b/projects/WTI-UI/src/app/modules/core/services/contest.mock.service.ts @@ -4,6 +4,7 @@ import { Observable, of } from 'rxjs'; import { ContestLanguage } from '../models/contest-language'; import { ContestProblem } from '../models/contest-problem'; import { Clarification } from '../models/clarification'; +import { ContestClock } from '../models/contest-clock'; @Injectable() export class ContestMockService extends IContestService { @@ -78,6 +79,12 @@ export class ContestMockService extends IContestService { return of(true); } + getContestClock(): Observable { + return of( + //return a contest that is running, last 5 hours, has been running one hour, and started on December 18, 2024 + {isRunning:'true', contestLengthSecs:'18000', elapsedSecs:'3600', wallClockStartTime:'1734593847'}); + } + getStandings(): Observable { //TODO: this method needs to return a legitimate (mock) team standing array! From 23567657923aae33e3fbb6e7e773f125c162b188 Mon Sep 17 00:00:00 2001 From: John Clevenger Date: Fri, 20 Dec 2024 18:23:54 -0800 Subject: [PATCH 018/141] i1027: added fetch of PC2 contest clock upon successful login. --- .../login-page/login-page.component.ts | 51 +++++++++++++++++-- 1 file changed, 47 insertions(+), 4 deletions(-) diff --git a/projects/WTI-UI/src/app/modules/login/components/login-page/login-page.component.ts b/projects/WTI-UI/src/app/modules/login/components/login-page/login-page.component.ts index b628dbd15..c3f09a67f 100644 --- a/projects/WTI-UI/src/app/modules/login/components/login-page/login-page.component.ts +++ b/projects/WTI-UI/src/app/modules/login/components/login-page/login-page.component.ts @@ -9,6 +9,8 @@ import { IWebsocketService } from 'src/app/modules/core/abstract-services/i-webs import { Router } from '@angular/router'; import { IContestService } from 'src/app/modules/core/abstract-services/i-contest.service'; import { AppTitleService } from 'src/app/modules/core/services/app-title.service'; +import { DEBUG_MODE } from 'src/constants' +import { ContestClock } from 'src/app/modules/core/models/contest-clock'; @Component({ templateUrl: './login-page.component.html', @@ -19,6 +21,7 @@ export class LoginPageComponent implements OnInit, OnDestroy { formGroup: FormGroup; invalidCreds = false; loginStarted = false; + contestClock: ContestClock = new ContestClock(); constructor(private _formBuilder: FormBuilder, private _authService: AuthService, @@ -55,10 +58,50 @@ export class LoginPageComponent implements OnInit, OnDestroy { this._contestService.isContestRunning = val; this._contestService.contestClock.next(); }); - }, (error: any) => { - this.invalidCreds = true; - this.loginStarted = false; - }); + + //get the actual contest clock info from the PC2 server via the Contest Service (which gets it via the WTI Server and its PC2 API) + if (DEBUG_MODE) { + console.log ("LoginPageComponent.onSubmit(): invoking ContestService.getContestClock() and subscribing to wait for response.") + } + this._contestService.getContestClock() + .subscribe( + (data: any) => { + if (DEBUG_MODE) { + console.log ("LoginPageComponent.onSubmit(): got subscription callback from getContestClock()"); + } + if (!data) { + console.error ("Unable to get ContestClock from PC2 API via ContestService!"); + } else { + if (DEBUG_MODE) { + console.log ("LoginPageComponent.onSubmit(): data object returned from getContestClock():"); + console.log (data); + } + + //copy the data fields received from the PC2 Server (via the WTI-API) into the local ContestClock object + this.contestClock.isRunning = data.running ; + this.contestClock.contestLengthSecs = data.contestLengthInSecs ; + this.contestClock.elapsedSecs = data.elapsedSecs ; + this.contestClock.wallClockStartTime = data.wallClockStartTime ; + + if (DEBUG_MODE) { + console.log (" Contest Clock values:"); + console.log (" isRunning = ", this.contestClock.isRunning); + console.log (" contestLengthSecs = ", this.contestClock.contestLengthSecs); + console.log (" elapsedSecs = ", this.contestClock.elapsedSecs); + console.log (" wallClockStartTime = ", this.contestClock.wallClockStartTime); + } + } + }, + (error: any) => { + console.error("LoginPageComponent.onSubmit(): getContestClock() subscription callback error:"); + console.error (error); + } + ); + + }, (error: any) => { + this.invalidCreds = true; + this.loginStarted = false; + }); } private buildForm(): void { From d55fa7f50241c2fab626a43bd8b8512fb19a0cc5 Mon Sep 17 00:00:00 2001 From: John Clevenger Date: Sat, 21 Dec 2024 15:13:42 -0800 Subject: [PATCH 019/141] i1027: removed no-longer-needed debugging; reformat code for readability --- .../login-page/login-page.component.ts | 32 ++++++------------- 1 file changed, 9 insertions(+), 23 deletions(-) diff --git a/projects/WTI-UI/src/app/modules/login/components/login-page/login-page.component.ts b/projects/WTI-UI/src/app/modules/login/components/login-page/login-page.component.ts index c3f09a67f..cb7ead227 100644 --- a/projects/WTI-UI/src/app/modules/login/components/login-page/login-page.component.ts +++ b/projects/WTI-UI/src/app/modules/login/components/login-page/login-page.component.ts @@ -51,8 +51,11 @@ export class LoginPageComponent implements OnInit, OnDestroy { this._authService.login(loginCreds) .pipe(takeUntil(this._unsubscribe)) .subscribe((result: TeamsLoginResponse) => { + this._authService.completeLogin(result.teamId, result.teamName); + this._websocketService.startWebsocket(); + this._contestService.getIsContestRunning() .subscribe((val: boolean) => { this._contestService.isContestRunning = val; @@ -60,47 +63,30 @@ export class LoginPageComponent implements OnInit, OnDestroy { }); //get the actual contest clock info from the PC2 server via the Contest Service (which gets it via the WTI Server and its PC2 API) - if (DEBUG_MODE) { - console.log ("LoginPageComponent.onSubmit(): invoking ContestService.getContestClock() and subscribing to wait for response.") - } this._contestService.getContestClock() .subscribe( (data: any) => { - if (DEBUG_MODE) { - console.log ("LoginPageComponent.onSubmit(): got subscription callback from getContestClock()"); - } if (!data) { console.error ("Unable to get ContestClock from PC2 API via ContestService!"); - } else { - if (DEBUG_MODE) { - console.log ("LoginPageComponent.onSubmit(): data object returned from getContestClock():"); - console.log (data); - } - + } else { //copy the data fields received from the PC2 Server (via the WTI-API) into the local ContestClock object this.contestClock.isRunning = data.running ; this.contestClock.contestLengthSecs = data.contestLengthInSecs ; this.contestClock.elapsedSecs = data.elapsedSecs ; this.contestClock.wallClockStartTime = data.wallClockStartTime ; - - if (DEBUG_MODE) { - console.log (" Contest Clock values:"); - console.log (" isRunning = ", this.contestClock.isRunning); - console.log (" contestLengthSecs = ", this.contestClock.contestLengthSecs); - console.log (" elapsedSecs = ", this.contestClock.elapsedSecs); - console.log (" wallClockStartTime = ", this.contestClock.wallClockStartTime); - } } }, (error: any) => { - console.error("LoginPageComponent.onSubmit(): getContestClock() subscription callback error:"); + console.error("LoginPageComponent.onSubmit(): getContestClock() subscription callback error: "); console.error (error); } ); }, (error: any) => { - this.invalidCreds = true; - this.loginStarted = false; + console.error ("LoginPageComponent.onSubmit(): AuthService.login() subscription callback error: "); + console.error(error); + this.invalidCreds = true; + this.loginStarted = false; }); } From 5111f9c7826a3efdfeb78e577fb072710481051b Mon Sep 17 00:00:00 2001 From: John Clevenger Date: Sat, 21 Dec 2024 16:17:21 -0800 Subject: [PATCH 020/141] i1027: rename time display pipe, rework to accept numSeconds --- .../core/services/displayTimePipe.service.ts | 42 +++++++++++++++++ .../core/services/elapsedTimePipe.service.ts | 45 ------------------- 2 files changed, 42 insertions(+), 45 deletions(-) create mode 100644 projects/WTI-UI/src/app/modules/core/services/displayTimePipe.service.ts delete mode 100644 projects/WTI-UI/src/app/modules/core/services/elapsedTimePipe.service.ts diff --git a/projects/WTI-UI/src/app/modules/core/services/displayTimePipe.service.ts b/projects/WTI-UI/src/app/modules/core/services/displayTimePipe.service.ts new file mode 100644 index 000000000..5082f3ac9 --- /dev/null +++ b/projects/WTI-UI/src/app/modules/core/services/displayTimePipe.service.ts @@ -0,0 +1,42 @@ +import { Pipe, PipeTransform } from '@angular/core'; +import { DEBUG_MODE } from 'src/constants' + +//This class defines a "pipe" which accepts a time, represented as a number of seconds, and returns +// a formatted string representing the number of days/hours/minutes/seconds represented by the input number of seconds. +//"Days" are only included in the output if they are greater than zero. +//The "pipe" is suitable for use, for example, in an Angular HTML template wanting to display an elapsed number of seconds +// in terms of hours/mins/secs (see app-header.component.html). + +@Pipe({ + name: 'displayTime', + standalone: true +}) +export class DisplayTimePipe implements PipeTransform { + + /** + Returns a formatted string representing the specified elapsed time (in seconds). + */ + transform(numSecs: number): string { + + const days = Math.floor(numSecs / (60 * 60 * 24)); + const hours = Math.floor((numSecs % (60 * 60 * 24)) / (60 * 60)); + const minutes = Math.floor((numSecs % (60 * 60)) / (60)); + const seconds = Math.floor(numSecs % 60); + + if (DEBUG_MODE) { + console.log("DisplayTimePipe.transform(): computed ", numSecs, " as ", days, "d ", hours, "h ", minutes, "m ", seconds, "s"); + } + + const formattedHours = hours.toString().padStart(2, '0'); + const formattedMinutes = minutes.toString().padStart(2, '0'); + const formattedSeconds = seconds.toString().padStart(2, '0'); + + + if (days > 0) { + return `${days}d ${formattedHours}:${formattedMinutes}:${formattedSeconds}`; + } else { + return `${formattedHours}:${formattedMinutes}:${formattedSeconds}`; + } + + } +} \ No newline at end of file diff --git a/projects/WTI-UI/src/app/modules/core/services/elapsedTimePipe.service.ts b/projects/WTI-UI/src/app/modules/core/services/elapsedTimePipe.service.ts deleted file mode 100644 index ad3dc7287..000000000 --- a/projects/WTI-UI/src/app/modules/core/services/elapsedTimePipe.service.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { Pipe, PipeTransform } from '@angular/core'; - -//This class defines a "pipe" which accepts a "Date" object and returns -// a formatted string representing the number of days/hours/minutes/seconds that have -// elapsed between the specified Date and "now". -//The "pipe" is suitable for use, for example, in an Angular HTML template (see app-header.component.html). -//It is based on code generated by Google AI. - -//It calculates the difference in milliseconds between the given date and the current time. -//Then it extracts days, hours, minutes, and seconds from the difference. -//Finally, it formats the output string. "Days" are only included in the output if they are greater than zero. - -@Pipe({ - name: 'elapsedTime', - standalone: true -}) -export class ElapsedTimePipe implements PipeTransform { - - /** - Returns a string representing the elapsed time between the input Date and "now". - */ - transform(startDate: Date): string { - const now = new Date().getTime(); - const then = startDate.getTime(); - - const diff = now - then; - - const days = Math.floor(diff / (1000 * 60 * 60 * 24)); - const hours = Math.floor((diff % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)); - const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60)); - const seconds = Math.floor((diff % (1000 * 60)) / 1000); - - const formattedHours = hours.toString().padStart(2, '0'); - const formattedMinutes = minutes.toString().padStart(2, '0'); - const formattedSeconds = seconds.toString().padStart(2, '0'); - - - if (days > 0) { - return `${days}d ${formattedHours}:${formattedMinutes}:${formattedSeconds}`; - } else { - return `${formattedHours}:${formattedMinutes}:${formattedSeconds}`; - } - - } -} \ No newline at end of file From 41051a58f88a52a208ddd4c200469b3310aa170a Mon Sep 17 00:00:00 2001 From: John Clevenger Date: Mon, 23 Dec 2024 17:34:32 -0800 Subject: [PATCH 021/141] i1027: add WTI-UI model for contest clock. --- .../src/app/modules/core/models/contest-clock.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 projects/WTI-UI/src/app/modules/core/models/contest-clock.ts diff --git a/projects/WTI-UI/src/app/modules/core/models/contest-clock.ts b/projects/WTI-UI/src/app/modules/core/models/contest-clock.ts new file mode 100644 index 000000000..474f7c012 --- /dev/null +++ b/projects/WTI-UI/src/app/modules/core/models/contest-clock.ts @@ -0,0 +1,12 @@ +/** This class encapsulates the PC2 notion of a "Contest Clock" -- a collection of items which together provide + * information on the state of state of time in the contest -- what time the contest started, how long the contest runs, + * whether the contest is running or paused right now, and how much time has elapsed on the contest clock since the + * contest started (note that this latter value does NOT include any real time which passed while the contest was "paused"). + * Essentially, this model class corresponds to the PC2 server class "ContestTime". + */ +export class ContestClock { + isRunning: string = ''; //string 'true' or 'false' + contestLengthSecs: string = ''; //string representing an integer number of seconds + elapsedSecs: string = ''; //string representing total time in seconds that the contest has been running -- does NOT include time during any "pauses" + wallClockStartTime: string = ''; //string representing unix timestamp when the contest actually started -- msec since the Epoch. Does not change due to "pauses" +} \ No newline at end of file From a93102a02bfd78aff064a1769ceaaa66a2e4439f Mon Sep 17 00:00:00 2001 From: John Clevenger Date: Mon, 23 Dec 2024 17:35:58 -0800 Subject: [PATCH 022/141] i1027: added Timer class to enable/disable incr of display times. --- .../core/services/contestTimer.service.ts | 80 +++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 projects/WTI-UI/src/app/modules/core/services/contestTimer.service.ts diff --git a/projects/WTI-UI/src/app/modules/core/services/contestTimer.service.ts b/projects/WTI-UI/src/app/modules/core/services/contestTimer.service.ts new file mode 100644 index 000000000..368978acf --- /dev/null +++ b/projects/WTI-UI/src/app/modules/core/services/contestTimer.service.ts @@ -0,0 +1,80 @@ +import { DEBUG_MODE } from 'src/constants' + +export class ContestTimerService { + + elapsedSecs = 0 ; //how many seconds the contest has been running (doesn't include any 'paused' time) + remainingSecs = 18000 ; //how many seconds remain in the contest (default initial value: 5 hours = 18,000 secs) + intervalId: ReturnType ; //the id of the JavaScript "interval" used to generate timer ticks + isTimerRunning: boolean = false; + + constructor() { + if (DEBUG_MODE) { + console.log ("Executing ContestTimerService constructor"); + } + } + + getElapsedSecs(): number { + return this.elapsedSecs; + } + + getRemainingSecs(): number { + return this.remainingSecs; + } + + setElapsedSecs(newElapsedSecs: number) { + if (this.intervalId) { + console.error ("ContestTimerService.setElapsedSecs(): cannot set elapsed seconds while Timer is running; stop the timer first.") + return; + } else { + this.elapsedSecs = newElapsedSecs; + } + } + + setRemainingSecs(newRemainingSecs: number) { + if (this.intervalId) { + console.error ("ContestTimerService.setRemainingSecs(): cannot set remaining seconds while Timer is running; stop the timer first.") + return; + } else { + this.remainingSecs = newRemainingSecs>=0 ? newRemainingSecs : 0; + } + } + + startTimer() { + if (this.intervalId) { + console.error("ContestTimerService.startTimer(): call to startTimer() when timer is already running"); + return; + } else { + if (DEBUG_MODE) { + console.log ("ContestTimerService.startTimer(): starting 1-second interval timer"); + } + //Start a 1-second interval timer going, save the interval timer Id + this.intervalId = setInterval( + //execute this function at the specified interval: + () => { + //bump the elapsed & remaining counters since 1000msec (one sec) has passed + this.elapsedSecs += 1 ; + this.remainingSecs -= 1 + console.log(`Elapsed secs: ${this.elapsedSecs}; remaining secs: ${this.remainingSecs}`); + }, + 1000 // 1000 milliseconds = 1 second interval + ); + this.isTimerRunning = true; + } + } + + stopTimer() { + if (!this.intervalId) { + console.error("ContestTimerService.stopTimer(): call to stopTimer() when timer is not running"); + return; + } else { + if (DEBUG_MODE) { + console.log ("ContestTimerService.stopTimer(): stopping timer"); + } + //dispose of the existing interval timer (thus stopping it) + clearInterval(this.intervalId); + this.intervalId = undefined; + this.isTimerRunning = false; + } + } + +} From ea7490afecd75aefc83c02240b95da702478de92 Mon Sep 17 00:00:00 2001 From: John Clevenger Date: Wed, 25 Dec 2024 14:45:30 -0800 Subject: [PATCH 023/141] i1027: remove no-longer-used ngx-countdown. --- projects/WTI-UI/package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/projects/WTI-UI/package.json b/projects/WTI-UI/package.json index d85d926f5..86a8a1dd7 100644 --- a/projects/WTI-UI/package.json +++ b/projects/WTI-UI/package.json @@ -22,7 +22,6 @@ "@angular/platform-browser-dynamic": "^16.2.11", "@angular/router": "^16.2.11", "core-js": "^3.33.1", - "ngx-countdown": "16.0", "rxjs": "^6.5.3 || ^7.4.0", "tslib": "^1.9.0", "zone.js": "^0.13.X" From 1645b45b7156a3b19e2bc81e96417420d8138889 Mon Sep 17 00:00:00 2001 From: John Clevenger Date: Wed, 25 Dec 2024 14:47:37 -0800 Subject: [PATCH 024/141] i1027: include title-setting in all routes, matching PR871. --- projects/WTI-UI/src/app/app-routing.module.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/projects/WTI-UI/src/app/app-routing.module.ts b/projects/WTI-UI/src/app/app-routing.module.ts index b2aa6f684..0c781a8f8 100644 --- a/projects/WTI-UI/src/app/app-routing.module.ts +++ b/projects/WTI-UI/src/app/app-routing.module.ts @@ -11,25 +11,31 @@ import { ScoreboardPageComponent } from './modules/scoreboard/components/scorebo const routes: Routes = [ { path: 'login', + title: 'Login to PC2', component: LoginPageComponent }, { path: 'runs', + title: 'Submit Runs', component: RunsPageComponent, canActivate: [AuthGuard] }, { path: 'options', + title: 'Configure Options', component: OptionsPageComponent, canActivate: [AuthGuard] }, { path: 'logout', + title: 'Logout of PC2', component: LogoutComponent, canActivate: [AuthGuard] }, { path: 'clarifications', + title: 'Submit Clarification Request', component: ClarificationsPageComponent, canActivate: [AuthGuard] }, { path: 'scoreboard', + title: 'View Scoreboard', component: ScoreboardPageComponent, canActivate: [AuthGuard] }, { From 7a71e77099305cd129d76eab0bf3485c338f9510 Mon Sep 17 00:00:00 2001 From: John Clevenger Date: Wed, 25 Dec 2024 16:00:34 -0800 Subject: [PATCH 025/141] i1027: update for compatibility with PR for i871. --- .../clarifications-page.component.ts | 7 +- .../core/abstract-services/i-teams.service.ts | 11 +++ .../src/app/modules/core/auth/auth.guard.ts | 11 ++- .../src/app/modules/core/auth/auth.service.ts | 43 +++++++++-- .../src/app/modules/core/core.module.ts | 77 ++++++++++++++++--- .../core/services/contest.mock.service.ts | 8 ++ .../core/services/teams.mock.service.ts | 17 +++- .../modules/core/services/teams.service.ts | 9 ++- .../core/services/ui-helper.service.ts | 11 ++- .../core/services/websocket.mock.service.ts | 14 +++- .../core/services/websocket.service.ts | 31 ++++++-- .../components/logout/logout.component.ts | 16 +++- .../options-page/options-page.component.ts | 54 +++++++++++++ .../runs-page/runs-page.component.ts | 6 +- .../scoreboard-page.component.ts | 11 ++- 15 files changed, 288 insertions(+), 38 deletions(-) diff --git a/projects/WTI-UI/src/app/modules/clarifications/components/clarifications-page/clarifications-page.component.ts b/projects/WTI-UI/src/app/modules/clarifications/components/clarifications-page/clarifications-page.component.ts index dab398e9d..91abb04e8 100644 --- a/projects/WTI-UI/src/app/modules/clarifications/components/clarifications-page/clarifications-page.component.ts +++ b/projects/WTI-UI/src/app/modules/clarifications/components/clarifications-page/clarifications-page.component.ts @@ -8,6 +8,8 @@ import { MatDialog } from '@angular/material/dialog'; import { NewClarificationComponent } from '../new-clarification/new-clarification.component'; import { AuthService } from '../../../core/auth/auth.service'; import { AppTitleService } from 'src/app/modules/core/services/app-title.service'; +import { saveCurrentPage } from 'src/app/app.component'; +import * as Constants from 'src/constants'; @Component({ templateUrl: './clarifications-page.component.html', @@ -29,7 +31,10 @@ export class ClarificationsPageComponent implements OnInit, OnDestroy { ngOnInit(): void { this._appTitleService.setTitleWithTeamId("Clarifications"); - + + //indicate that this Clarifications page is the most recently accessed page + saveCurrentPage(Constants.CLARIFICATIONS_PAGE); + this.buildForm(); this.loadClars(); diff --git a/projects/WTI-UI/src/app/modules/core/abstract-services/i-teams.service.ts b/projects/WTI-UI/src/app/modules/core/abstract-services/i-teams.service.ts index e92dc8b5c..dfbba2061 100644 --- a/projects/WTI-UI/src/app/modules/core/abstract-services/i-teams.service.ts +++ b/projects/WTI-UI/src/app/modules/core/abstract-services/i-teams.service.ts @@ -1,12 +1,23 @@ +import { Injectable } from '@angular/core'; import { LoginCredentials } from '../models/login-credentials'; import { Observable, Subject } from 'rxjs'; import { TeamsLoginResponse } from '../models/teams-login-response'; import { Submission } from '../models/submission'; import { Run } from '../models/run'; import { NewClarification } from '../models/new-clarification'; +import { DEBUG_MODE } from 'src/constants'; +@Injectable({ + providedIn: 'root' //forces the service to be a singleton across all app components ('root' == "root injector") +}) export abstract class ITeamsService { runsUpdated = new Subject(); + + constructor () { + if (DEBUG_MODE) { + console.log ("Executing ITeamsService constructor") ; + } + } abstract login(loginCredentials: LoginCredentials): Observable; diff --git a/projects/WTI-UI/src/app/modules/core/auth/auth.guard.ts b/projects/WTI-UI/src/app/modules/core/auth/auth.guard.ts index 252a2f631..e1fb57006 100644 --- a/projects/WTI-UI/src/app/modules/core/auth/auth.guard.ts +++ b/projects/WTI-UI/src/app/modules/core/auth/auth.guard.ts @@ -1,10 +1,17 @@ import { Injectable } from '@angular/core'; import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, Router } from '@angular/router'; import { AuthService } from './auth.service'; +import { DEBUG_MODE } from 'src/constants'; -@Injectable() +@Injectable({ + providedIn: 'root' //forces the service to be a singleton across all app components ('root' == "root injector") +}) export class AuthGuard implements CanActivate { - constructor(private _authService: AuthService, private _router: Router) { } + constructor(private _authService: AuthService, private _router: Router) { + if (DEBUG_MODE) { + console.log ("Executing AuthGuard constructor...") ; + } + } canActivate(next: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean { if (this._authService.isLoggedIn) { diff --git a/projects/WTI-UI/src/app/modules/core/auth/auth.service.ts b/projects/WTI-UI/src/app/modules/core/auth/auth.service.ts index acd0f9659..fbf9500a0 100644 --- a/projects/WTI-UI/src/app/modules/core/auth/auth.service.ts +++ b/projects/WTI-UI/src/app/modules/core/auth/auth.service.ts @@ -4,38 +4,69 @@ import { LoginCredentials } from '../models/login-credentials'; import { Router } from '@angular/router'; import { ITeamsService } from '../abstract-services/i-teams.service'; import { TeamsLoginResponse } from '../models/teams-login-response'; +import { saveCurrentToken, saveCurrentUserName, clearSessionStorage } from 'src/app/app.component'; +import { DEBUG_MODE } from 'src/constants'; -@Injectable() +@Injectable({ + providedIn: 'root' //forces the service to be a singleton across all app components ('root' == root injector) +}) export class AuthService { private _token: string; private _userName: string; - get isLoggedIn(): boolean { return !!this.token; } private _defaultRoute = '/runs'; - get defaultRoute(): string { return this._defaultRoute; } redirectUrl: string; + get token(): string { return this._token; } get username(): string { return this._userName; } - + get isLoggedIn(): boolean { return !!this.token; } + get defaultRoute(): string { return this._defaultRoute; } + + set token(value) { this._token = value; } + set username(value) { this._userName = value; } + constructor(private _teamsService: ITeamsService, - private _router: Router) { } + private _router: Router) { + if (DEBUG_MODE) { + console.log ("Executing AuthService constructor..."); + } + } completeLogin(tokenValue: string, username: string) { + if (DEBUG_MODE) { + console.log ("Executing AuthService.completeLogin()..."); + } + this._token = tokenValue; this._userName = username; + //save values in sessionStorage to allow for recovery from F5 + saveCurrentToken(tokenValue); + saveCurrentUserName(username); + //switch to the main user page this._router.navigateByUrl(this.redirectUrl || this.defaultRoute); } login(loginCredentials: LoginCredentials): Observable { + if (DEBUG_MODE) { + console.log ("Executing AuthService.login() -- invoking TeamsService.login()") ; + } return this._teamsService.login(loginCredentials); } logout(): Observable { - return this._teamsService.logout(); + if (DEBUG_MODE) { + console.log ("Executing AuthService.logout() -- invoking TeamsService.logout()") ; + } + return this._teamsService.logout(); } completeLogout(): void { + if (DEBUG_MODE) { + console.log ("Executing AuthService.completeLogout()..."); + } + this._token = undefined; this._userName = undefined; + clearSessionStorage(); this._router.navigateByUrl('/login'); } } diff --git a/projects/WTI-UI/src/app/modules/core/core.module.ts b/projects/WTI-UI/src/app/modules/core/core.module.ts index fe56e7900..731dde7be 100644 --- a/projects/WTI-UI/src/app/modules/core/core.module.ts +++ b/projects/WTI-UI/src/app/modules/core/core.module.ts @@ -15,41 +15,96 @@ import { WebsocketMockService } from './services/websocket.mock.service'; import { IWebsocketService } from './abstract-services/i-websocket.service'; import { UiHelperService } from './services/ui-helper.service'; import { SharedModule } from '../shared/shared.module'; -import { ElapsedTimePipe } from './services/elapsedTimePipe.service'; +import { DEBUG_MODE } from 'src/constants'; +import { DisplayTimePipe } from './services/displayTimePipe.service'; export function TeamsServiceFactory(http: HttpClient) { - if (environment.useMock) { return new TeamsMockService(); } + if (DEBUG_MODE) { + console.log("Executing TeamsServiceFactory...") + } + + if (environment.useMock) { + if (DEBUG_MODE) { + console.log("...about to construct then return new TeamsMockService") + } + return new TeamsMockService(); + } + + //not using Mock + if (DEBUG_MODE) { + console.log("...about to construct then return new TeamsService") + } return new TeamsService(http); } export function ContestServiceFactory(http: HttpClient) { - if (environment.useMock) { return new ContestMockService(); } + if (DEBUG_MODE) { + console.log("Executing ContestServiceFactory...") + } + + if (environment.useMock) { + if (DEBUG_MODE) { + console.log("...about to construct then return new ContestMockService") + } + return new ContestMockService(); + } + + //not using Mock + if (DEBUG_MODE) { + console.log("...about to construct then return new ContestService") + } return new ContestService(http); } -export function WebsocketServiceFactory(injector: Injector, authService: AuthService) { - if (environment.useMock) { return new WebsocketMockService(injector); } - return new WebsocketService(injector, authService); +export function WebsocketServiceFactory(injector: Injector, + uiHelperService: UiHelperService, iContestService: IContestService, + iTeamsService: ITeamsService, authService: AuthService) { + if (DEBUG_MODE) { + console.log("Executing WebsocketServiceFactory...") + } + + if (environment.useMock) { + if (DEBUG_MODE) { + console.log("...about to construct then return new WebsocketMockService") + } + return new WebsocketMockService(injector); + } + + //not using Mock + if (DEBUG_MODE) { + console.log("...about to construct then return new WebsocketService") + } + + //original code: + //return new WebsocketService(injector, authService); + return new WebsocketService(uiHelperService, iContestService, iTeamsService, authService); } @NgModule({ providers: [ { provide: ITeamsService, useFactory: TeamsServiceFactory, deps: [HttpClient] }, { provide: IContestService, useFactory: ContestServiceFactory, deps: [HttpClient] }, + { provide: ContestService, useFactory: ContestServiceFactory, deps: [HttpClient] }, { provide: AuthService, useClass: AuthService }, { provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true }, - { provide: IWebsocketService, useFactory: WebsocketServiceFactory, deps: [Injector, AuthService] }, + { provide: IWebsocketService, useFactory: WebsocketServiceFactory, deps: [Injector, UiHelperService, IContestService, ITeamsService, AuthService] }, AuthGuard, UiHelperService, - ElapsedTimePipe + DisplayTimePipe, + //TODO: should the following two still be declared here since they are now listed in the above "deps" list? + UiHelperService, + ContestService, + //TODO: should the following two still be declared here since they are now listed in the above "deps" list? + UiHelperService, + ContestService ], imports: [ HttpClientModule, SharedModule, - ElapsedTimePipe + DisplayTimePipe ], exports: [ - ElapsedTimePipe - ], + DisplayTimePipe + ] }) export class CoreModule { } diff --git a/projects/WTI-UI/src/app/modules/core/services/contest.mock.service.ts b/projects/WTI-UI/src/app/modules/core/services/contest.mock.service.ts index a543aac9b..406ecd1f7 100644 --- a/projects/WTI-UI/src/app/modules/core/services/contest.mock.service.ts +++ b/projects/WTI-UI/src/app/modules/core/services/contest.mock.service.ts @@ -5,9 +5,17 @@ import { ContestLanguage } from '../models/contest-language'; import { ContestProblem } from '../models/contest-problem'; import { Clarification } from '../models/clarification'; import { ContestClock } from '../models/contest-clock'; +import { DEBUG_MODE } from 'src/constants'; @Injectable() export class ContestMockService extends IContestService { + + constructor() { + if (DEBUG_MODE) { + console.log ("Executing ContestMockService constructor; preparing to call IContestService.super()") ; + } + super(); + } getLanguages(): Observable { return of([ diff --git a/projects/WTI-UI/src/app/modules/core/services/teams.mock.service.ts b/projects/WTI-UI/src/app/modules/core/services/teams.mock.service.ts index 62d8c0514..a030f3435 100644 --- a/projects/WTI-UI/src/app/modules/core/services/teams.mock.service.ts +++ b/projects/WTI-UI/src/app/modules/core/services/teams.mock.service.ts @@ -6,9 +6,18 @@ import { TeamsLoginResponse } from '../models/teams-login-response'; import { Submission } from '../models/submission'; import { Run } from '../models/run'; import { NewClarification } from '../models/new-clarification'; +import { DEBUG_MODE } from 'src/constants'; @Injectable() export class TeamsMockService extends ITeamsService { + + constructor() { + if (DEBUG_MODE) { + console.log ("TeamsMockService constructor; preparing to invoke ITeamsService superclass constructor via 'super()'") ; + } + super() ; + } + login(loginCredentials: LoginCredentials): Observable { return of({ teamName: loginCredentials.teamName, @@ -21,7 +30,9 @@ export class TeamsMockService extends ITeamsService { } submitRun(submission: Submission): Observable { - console.log(submission); + if (DEBUG_MODE) { + console.log(submission); + } return of({}); } @@ -95,7 +106,9 @@ export class TeamsMockService extends ITeamsService { } postClarification(clarification: NewClarification): Observable { - console.log(clarification); + if (DEBUG_MODE) { + console.log(clarification); + } return of({}); } } diff --git a/projects/WTI-UI/src/app/modules/core/services/teams.service.ts b/projects/WTI-UI/src/app/modules/core/services/teams.service.ts index d86a066bc..6903fab0c 100644 --- a/projects/WTI-UI/src/app/modules/core/services/teams.service.ts +++ b/projects/WTI-UI/src/app/modules/core/services/teams.service.ts @@ -8,11 +8,18 @@ import { TeamsLoginResponse } from '../models/teams-login-response'; import { Submission } from '../models/submission'; import { Run } from '../models/run'; import { NewClarification } from '../models/new-clarification'; +import { DEBUG_MODE } from 'src/constants'; -@Injectable() +@Injectable({ + providedIn: 'root' //forces the service to be a singleton across all app components ('root' == "root injector") +}) export class TeamsService extends ITeamsService { + constructor(private _httpClient: HttpClient) { super(); + if (DEBUG_MODE) { + console.log ("Executing TeamsService constructor") ; + } } login(loginCredentials: LoginCredentials): Observable { diff --git a/projects/WTI-UI/src/app/modules/core/services/ui-helper.service.ts b/projects/WTI-UI/src/app/modules/core/services/ui-helper.service.ts index 45fcd9eb9..8798e6cc0 100644 --- a/projects/WTI-UI/src/app/modules/core/services/ui-helper.service.ts +++ b/projects/WTI-UI/src/app/modules/core/services/ui-helper.service.ts @@ -3,14 +3,21 @@ import { MatDialog } from '@angular/material/dialog'; import { NewClarificationAlertComponent } from '../../clarifications/components/new-clarification-alert/new-clarification-alert.component'; import { NewRunAlertComponent } from '../../runs/components/new-run-alert/new-run-alert.component'; import { MatSnackBar } from '@angular/material/snack-bar'; +import { DEBUG_MODE } from 'src/constants'; -@Injectable() +@Injectable({ + providedIn: 'root' //forces the service to be a singleton across all app components ('root' == "root injector") +}) export class UiHelperService { enableClarificationNotifications = true; enableRunsNotifications = true; constructor(private _dialogService: MatDialog, - private _matSnackBar: MatSnackBar) { } + private _matSnackBar: MatSnackBar) { + if (DEBUG_MODE) { + console.log ("Executing UiHelperService constructor.") ; + } + } incomingClarification(id: string): void { if (this.enableClarificationNotifications) { diff --git a/projects/WTI-UI/src/app/modules/core/services/websocket.mock.service.ts b/projects/WTI-UI/src/app/modules/core/services/websocket.mock.service.ts index 11e55e2cb..528c55883 100644 --- a/projects/WTI-UI/src/app/modules/core/services/websocket.mock.service.ts +++ b/projects/WTI-UI/src/app/modules/core/services/websocket.mock.service.ts @@ -4,23 +4,31 @@ import { UiHelperService } from './ui-helper.service'; import { IContestService } from '../abstract-services/i-contest.service'; import { ITeamsService } from '../abstract-services/i-teams.service'; import { AuthService } from '../auth/auth.service'; +import { DEBUG_MODE } from 'src/constants'; @Injectable() export class WebsocketMockService extends IWebsocketService { + constructor(private _injector: Injector) { // Manually get UiHelperService from angular DI to pass to abstract class // This avoids having two references to UiHelperService super(_injector.get(UiHelperService), _injector.get(IContestService), _injector.get(ITeamsService), _injector.get(AuthService)); - console.log('firing construcitor in websocket MOCK'); + if (DEBUG_MODE) { + console.log('Executing WebsocketMockService constructor'); + } } startWebsocket(): void { // no need for a socket here.... this is a mock - console.log('[Websocket] Start websocket called!'); + if (DEBUG_MODE) { + console.log('[Websocket] Start websocket called!'); + } } stopWebsocket(): void { // no need for a socket here.... this is a mock - console.log('[Websocket] Stop websocket called!'); + if (DEBUG_MODE) { + console.log('[Websocket] Stop websocket called!'); + } } } diff --git a/projects/WTI-UI/src/app/modules/core/services/websocket.service.ts b/projects/WTI-UI/src/app/modules/core/services/websocket.service.ts index b6e40e3d4..790c19108 100644 --- a/projects/WTI-UI/src/app/modules/core/services/websocket.service.ts +++ b/projects/WTI-UI/src/app/modules/core/services/websocket.service.ts @@ -1,31 +1,48 @@ import { Injectable, Injector } from '@angular/core'; import { environment } from 'src/environments/environment'; -import { UiHelperService } from './ui-helper.service'; +import { UiHelperService } from '../services/ui-helper.service'; import { IWebsocketService } from '../abstract-services/i-websocket.service'; import { AuthService } from '../auth/auth.service'; import { WebsocketMessage } from '../models/websocket-message'; import { IContestService } from '../abstract-services/i-contest.service'; import { ITeamsService } from '../abstract-services/i-teams.service'; +import { DEBUG_MODE } from 'src/constants'; -@Injectable() +@Injectable({ + providedIn: 'root' //forces the service to be a singleton across all app components ('root' == "root injector") +}) export class WebsocketService extends IWebsocketService { socket: WebSocket; - constructor(private _injector: Injector, _authService: AuthService) { + constructor(_uiHelperService: UiHelperService, + _iContestService: IContestService, + _iTeamsService: ITeamsService, + _authService: AuthService) { // Manually get UiHelperService from angular DI to pass to abstract class // This avoids having two references to UiHelperService - super(_injector.get(UiHelperService), _injector.get(IContestService), _injector.get(ITeamsService), _injector.get(AuthService)); - console.log('firing constructor in WebsocketService'); + //super(_injector.get(UiHelperService), _iContestService, _iTeamsService, _authService); + //The above was replaced with the following at the same time all services were specified as "providedIn: 'root'" + super(_uiHelperService, _iContestService, _iTeamsService, _authService); + if (DEBUG_MODE) { + console.log('Executing WebsocketService constructor'); + } } startWebsocket(): void { - console.log('starting websocket'); + if (DEBUG_MODE) { + console.log('Constructing websocket...'); + } this.socket = new WebSocket(`${environment.websocketUrl}/${this._authService.token}`); this.socket.addEventListener('message', this.handleIncomingMessage); + if (DEBUG_MODE) { + console.log('...websocket URL =' + this.socket.url); + } } stopWebsocket(): void { - console.log('ending websocket'); + if (DEBUG_MODE) { + console.log('Closing websocket'); + } if (this.socket) { this.socket.close(); this.socket = undefined; diff --git a/projects/WTI-UI/src/app/modules/login/components/logout/logout.component.ts b/projects/WTI-UI/src/app/modules/login/components/logout/logout.component.ts index eefad6468..adc912190 100644 --- a/projects/WTI-UI/src/app/modules/login/components/logout/logout.component.ts +++ b/projects/WTI-UI/src/app/modules/login/components/logout/logout.component.ts @@ -1,6 +1,7 @@ import { Component, OnInit } from '@angular/core'; import { AuthService } from 'src/app/modules/core/auth/auth.service'; import { IWebsocketService } from 'src/app/modules/core/abstract-services/i-websocket.service'; +import { DEBUG_MODE } from 'src/constants'; @Component({ templateUrl: './logout.component.html' @@ -10,12 +11,25 @@ export class LogoutComponent implements OnInit { private _websocketService: IWebsocketService) { } ngOnInit(): void { + if (DEBUG_MODE) { + console.log("Executing logout.component.ngOnInit()"); + console.log ("Calling AuthService.logout()"); + } this._authService.logout() .subscribe(_ => { + if (DEBUG_MODE) { + console.log ("Received 'subscribe()' callback from AuthService.logout(); calling WebsocketService.stopWebsocket()..."); + } this._websocketService.stopWebsocket(); + if (DEBUG_MODE) { + console.log ("Calling AuthService.completeLogout()"); + } this._authService.completeLogout(); }, (error: any) => { - this._authService.completeLogout(); + if (DEBUG_MODE) { + console.log ("AuthService.logout().subscribe() callback returned error; calling AuthService.completeLogout()"); + } + this._authService.completeLogout(); }); } } diff --git a/projects/WTI-UI/src/app/modules/options/components/options-page/options-page.component.ts b/projects/WTI-UI/src/app/modules/options/components/options-page/options-page.component.ts index d47b32fbf..443545792 100644 --- a/projects/WTI-UI/src/app/modules/options/components/options-page/options-page.component.ts +++ b/projects/WTI-UI/src/app/modules/options/components/options-page/options-page.component.ts @@ -1,9 +1,15 @@ +//options-page.component.ts + import { Component, OnInit } from '@angular/core'; import { MatDialog } from '@angular/material/dialog'; import { ChangePasswordComponent } from '../change-password/change-password.component'; import { UiHelperService } from 'src/app/modules/core/services/ui-helper.service'; import { environment } from 'src/environments/environment'; import { AppTitleService } from 'src/app/modules/core/services/app-title.service'; +import { saveCurrentPage } from 'src/app/app.component'; +import { saveOptions } from 'src/app/app.component'; +import * as Constants from 'src/constants'; +import { DEBUG_MODE } from 'src/constants'; @Component({ templateUrl: './options-page.component.html', @@ -20,7 +26,32 @@ export class OptionsPageComponent implements OnInit { private _appTitleService: AppTitleService) { } ngOnInit(): void { + if (DEBUG_MODE) { + console.log ("Executing OptionsPageComponent.ngOnInit()..."); + } this._appTitleService.setTitleWithTeamId("Options"); + + //indicate that this Options page is the most recently accessed page + if (DEBUG_MODE) { + console.log ("...saving OPTIONS_PAGE as 'current page'"); + } + saveCurrentPage(Constants.OPTIONS_PAGE); + + //save the current option values in sessionStorage so they can be restored on F5 refresh + let options = { + //TODO: the following field names (on the left) should use the names defined in constants.ts + clarsNotificationsEnabled : this.clarsNotificationsEnabled, + runsNotificationsEnabled: this.runsNotificationsEnabled + } + if (DEBUG_MODE) { + console.log ("...saving options in sessionStorage: clarsNotificationsEnabled = ", options.clarsNotificationsEnabled, + "; runsNotificationsEnabled = ", options.runsNotificationsEnabled ); + } + saveOptions(options) ; + + if (DEBUG_MODE) { + console.log ("OptionsPageComponent.ngOnInit() finished.") + } } showWebsocketDebug(): boolean { @@ -30,4 +61,27 @@ export class OptionsPageComponent implements OnInit { openChangePW(): void { this._dialogSvc.open(ChangePasswordComponent); } + + onRadioButtonChange(event: any) { + if (DEBUG_MODE) { + console.log ("Executing OptionsPageComponent.onRadioButtonClick()..."); + } + const name = event.target.name; + const value = event.target.value; + if (DEBUG_MODE) { + console.log ("...button name = '", name, "', button value = ", value) ; + } + let options = { + //TODO: the following field names (on the left) should use the names defined in constants.ts + clarsNotificationsEnabled : this.clarsNotificationsEnabled, + runsNotificationsEnabled: this.runsNotificationsEnabled + } + if (DEBUG_MODE) { + console.log ("Saving options to sessionStorage:"); + console.log ("...Clar popups enabled = ", options.clarsNotificationsEnabled); + console.log ("...Runs popups enabled = ", options.runsNotificationsEnabled); + } + saveOptions(options); + + } } diff --git a/projects/WTI-UI/src/app/modules/runs/components/runs-page/runs-page.component.ts b/projects/WTI-UI/src/app/modules/runs/components/runs-page/runs-page.component.ts index f8e8d1322..d057da2b4 100644 --- a/projects/WTI-UI/src/app/modules/runs/components/runs-page/runs-page.component.ts +++ b/projects/WTI-UI/src/app/modules/runs/components/runs-page/runs-page.component.ts @@ -8,7 +8,8 @@ import { MatDialog } from '@angular/material/dialog'; import { NewRunComponent } from '../new-run/new-run.component'; import { TestRunDetailComponent } from '../test-run-detail/test-run-detail.component'; import { AppTitleService } from 'src/app/modules/core/services/app-title.service'; - +import { saveCurrentPage } from 'src/app/app.component'; +import * as Constants from 'src/constants'; @Component({ templateUrl: './runs-page.component.html', @@ -31,6 +32,9 @@ export class RunsPageComponent implements OnInit, OnDestroy { this.buildForm(); this.loadRuns(); + + //indicate that this Runs page is the most recently accessed page + saveCurrentPage(Constants.RUNS_PAGE); this.filteredRuns = this.runs; diff --git a/projects/WTI-UI/src/app/modules/scoreboard/components/scoreboard-page/scoreboard-page.component.ts b/projects/WTI-UI/src/app/modules/scoreboard/components/scoreboard-page/scoreboard-page.component.ts index 28509e2ba..dbbaa06bf 100644 --- a/projects/WTI-UI/src/app/modules/scoreboard/components/scoreboard-page/scoreboard-page.component.ts +++ b/projects/WTI-UI/src/app/modules/scoreboard/components/scoreboard-page/scoreboard-page.component.ts @@ -3,6 +3,9 @@ import { IContestService } from 'src/app/modules/core/abstract-services/i-contes import { takeUntil } from 'rxjs/operators'; import { Subject } from 'rxjs'; import { AppTitleService } from 'src/app/modules/core/services/app-title.service'; +import { saveCurrentPage } from 'src/app/app.component'; +import * as Constants from 'src/constants'; +import { DEBUG_MODE } from 'src/constants'; @Component({ templateUrl: './scoreboard-page.component.html', @@ -19,10 +22,16 @@ export class ScoreboardPageComponent implements OnInit, OnDestroy, DoCheck { ) { } ngOnInit(): void { - //console.log("Scoreboard OnInit executed."); + + if (DEBUG_MODE) { + console.log("Executing ScoreboardPageComponent.ngOnInit"); + } this._appTitleService.setTitleWithTeamId("Scoreboard"); + //indicate that this Scoreboard page is the most recently accessed page + saveCurrentPage(Constants.SCOREBOARD_PAGE); + this.loadStandings(); // when standings are updated, trigger a reload From 732b89ce2b0cde877216c4db7f0439ed647cb93f Mon Sep 17 00:00:00 2001 From: John Clevenger Date: Thu, 26 Dec 2024 12:06:43 -0800 Subject: [PATCH 026/141] i1027: SharedModule: remove ngx-countdown; add ContestServices. --- .../src/app/modules/shared/shared.module.ts | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/projects/WTI-UI/src/app/modules/shared/shared.module.ts b/projects/WTI-UI/src/app/modules/shared/shared.module.ts index 3e5f52b71..711126b75 100644 --- a/projects/WTI-UI/src/app/modules/shared/shared.module.ts +++ b/projects/WTI-UI/src/app/modules/shared/shared.module.ts @@ -13,11 +13,18 @@ import { MatInputModule } from '@angular/material/input'; import { MatSelectModule } from '@angular/material/select'; import { MatSnackBarModule } from '@angular/material/snack-bar'; import { AboutWtiComponent } from './components/about-wti/about-wti.component'; -import { CountdownModule } from 'ngx-countdown'; import { BrowserModule } from '@angular/platform-browser'; -import { ElapsedTimePipe } from 'src/app/modules/core/services/elapsedTimePipe.service'; +import { DisplayTimePipe } from 'src/app/modules/core/services/displayTimePipe.service'; +import { IContestService } from 'src/app/modules/core/abstract-services/i-contest.service'; +import { ContestService } from 'src/app/modules/core/services/contest.service'; +import { ContestServiceFactory } from '../core/core.module'; +import { HttpClient } from '@angular/common/http'; @NgModule({ + providers: [ + { provide: IContestService, useFactory: ContestServiceFactory, deps: [HttpClient] }, + { provide: ContestService, useFactory: ContestServiceFactory, deps: [HttpClient] }, + ], declarations: [ AppHeaderComponent, AppFooterComponent, @@ -35,9 +42,8 @@ import { ElapsedTimePipe } from 'src/app/modules/core/services/elapsedTimePipe.s MatInputModule, MatSelectModule, MatSnackBarModule, - CountdownModule, BrowserModule, - ElapsedTimePipe + DisplayTimePipe ], exports: [ AppHeaderComponent, @@ -49,8 +55,7 @@ import { ElapsedTimePipe } from 'src/app/modules/core/services/elapsedTimePipe.s MatFormFieldModule, MatInputModule, MatSelectModule, - MatSnackBarModule, - CountdownModule + MatSnackBarModule ] }) export class SharedModule { } From b4a84cccaeabf5026b424e43b7a8658b635c98d1 Mon Sep 17 00:00:00 2001 From: John Clevenger Date: Thu, 26 Dec 2024 12:08:47 -0800 Subject: [PATCH 027/141] i1027: rename contestClock Subject to contestClockEvent; add debugging --- .../problem-selector.component.ts | 43 +++++++++++++++++-- 1 file changed, 40 insertions(+), 3 deletions(-) diff --git a/projects/WTI-UI/src/app/modules/shared/components/problem-selector/problem-selector.component.ts b/projects/WTI-UI/src/app/modules/shared/components/problem-selector/problem-selector.component.ts index 65902fe11..910b85650 100644 --- a/projects/WTI-UI/src/app/modules/shared/components/problem-selector/problem-selector.component.ts +++ b/projects/WTI-UI/src/app/modules/shared/components/problem-selector/problem-selector.component.ts @@ -4,6 +4,7 @@ import { ContestProblem } from 'src/app/modules/core/models/contest-problem'; import { IContestService } from 'src/app/modules/core/abstract-services/i-contest.service'; import { Subject } from 'rxjs'; import { takeUntil } from 'rxjs/operators'; +import { DEBUG_MODE } from 'src/constants'; @Component({ selector: 'app-problem-selector', @@ -24,16 +25,33 @@ export class ProblemSelectorComponent implements OnInit, OnDestroy, ControlValue onChange = (event: any) => { }; onTouched = (event: any) => { }; - constructor(private _contestService: IContestService) { } + constructor(private _contestService: IContestService) { + if (DEBUG_MODE) { + console.log ("Executing ProblemSelectorComponent constructor; IContestService id = ", this._contestService.uniqueId) ; + } + } ngOnInit(): void { + if (DEBUG_MODE) { + console.log ("Executing ProblemSelectorComponent.ngOnInit()") ; + } this.loadProblems(); if (this.allowGeneral) { this.writeValue('general'); } - // listen for contest start/stop to show/hide contest problems - this._contestService.contestClock + //Listen for (subscribe to) contest start/stop events to show/hide contest problems. + //The contestClockEvent "Subject" (defined in IContestService) is used as a toggle to indicate when + //contest clock-related events have occured. Subscribing to it means that this component will get a + //callback whenever the contestClockEvent's "next()" method is invoked. + //Currently the contestClockEvent's "next()" method is invoked in exactly one + //place: IWebsocketService.incomingMessage(), when a message of type "contest_clock" is received by the WTI-UI from the WTI-API + //(which in turn happens when the WTI-API receives a "contest clock configuration update" notice via the PC2 API). + //The effect of all this is that this Component gets notified when "some change" has occurred in the state of the + //PC2 contest clock. This causes the Component to execute its "loadProblems()" method; that method in turn checks + //whether the contest is currently RUNNING; if so, it loads the problem names; if not, it blanks out problem names. + + this._contestService.contestClockEvent .pipe(takeUntil(this._unsubscribe)) .subscribe(_ => this.loadProblems()); } @@ -56,15 +74,34 @@ export class ProblemSelectorComponent implements OnInit, OnDestroy, ControlValue } private loadProblems(): void { + if (DEBUG_MODE) { + console.log ("Executing ProblemSelectorComponent.loadProblems()") ; + } if (this._contestService.isContestRunning) { + if (DEBUG_MODE) { + console.log ("ProblemSelectorComponent.loadProblems(): ContestService.isContestRunning() returned positive Truthy value") ; + } this._contestService.getProblems() .pipe(takeUntil(this._unsubscribe)) .subscribe((data: ContestProblem[]) => { + if (DEBUG_MODE) { + console.log ("ProblemSelectorComponent.loadProblems(): subscription callback from ContestService.getProblems() returned data:"); + console.log (data) ; + } this.problems = data; }, (error: any) => { + if (DEBUG_MODE) { + console.log ("ProblemSelectorComponent.loadProblems(): subscription callback from ContestService.getProblems() returned error"); + console.log (" Setting contest problem list to empty array." ) ; + } this.problems = []; }); } else { + if (DEBUG_MODE) { + console.log ("ProblemSelectorComponent.loadProblems(): ContestService.isContestRunning() returned negative Truthy value") ; + console.log (" Setting contest problem list to empty array." ) ; + + } this.problems = []; } } From b06f0cb51ce5ba8fc9b0f3597d3aaa91b5e976b9 Mon Sep 17 00:00:00 2001 From: John Clevenger Date: Thu, 26 Dec 2024 12:09:55 -0800 Subject: [PATCH 028/141] i1027: make some console debug output conditional on DEBUG_MODE --- .../modules/runs/components/new-run/new-run.component.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/projects/WTI-UI/src/app/modules/runs/components/new-run/new-run.component.ts b/projects/WTI-UI/src/app/modules/runs/components/new-run/new-run.component.ts index 108dafa32..132a943ea 100644 --- a/projects/WTI-UI/src/app/modules/runs/components/new-run/new-run.component.ts +++ b/projects/WTI-UI/src/app/modules/runs/components/new-run/new-run.component.ts @@ -7,6 +7,7 @@ import { ITeamsService } from 'src/app/modules/core/abstract-services/i-teams.se import { Subject } from 'rxjs'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { UiHelperService } from '../../../core/services/ui-helper.service'; +import { DEBUG_MODE } from 'src/constants'; //See the comments in method getOSName() regarding these services //import * as os from 'os'; @@ -115,7 +116,9 @@ export class NewRunComponent implements OnInit, OnDestroy { model.osName = this.getOSName(); - console.log('getOSName() returned : ' + model.osName); + if (DEBUG_MODE) { + console.log('getOSName() returned : ' + model.osName); + } //make sure no file names contain blanks (the PC2 server chokes on such filenames) if (this.filenameContainsBlanks(this.mainFile, this.additionalFiles, this.testFiles)){ @@ -243,7 +246,9 @@ export class NewRunComponent implements OnInit, OnDestroy { //returns a string intended to identify the platform on which the browser is running private getOSName() : string { - console.log(navigator.userAgent); + if (DEBUG_MODE) { + console.log(navigator.userAgent); + } return navigator.userAgent.toString(); // The following were all attempts to get the Angular Typescript/Javascript code to invoke the underlying OS to obtain From 17053d1c51190492ab27f14bc205a4bffb507975 Mon Sep 17 00:00:00 2001 From: John Clevenger Date: Thu, 26 Dec 2024 12:16:17 -0800 Subject: [PATCH 029/141] i1027: LoginPage: move ContestClock to ContestService; add doc and debug --- .../login-page/login-page.component.ts | 101 +++++++++++++++--- 1 file changed, 87 insertions(+), 14 deletions(-) diff --git a/projects/WTI-UI/src/app/modules/login/components/login-page/login-page.component.ts b/projects/WTI-UI/src/app/modules/login/components/login-page/login-page.component.ts index cb7ead227..db0ab8fdd 100644 --- a/projects/WTI-UI/src/app/modules/login/components/login-page/login-page.component.ts +++ b/projects/WTI-UI/src/app/modules/login/components/login-page/login-page.component.ts @@ -9,9 +9,29 @@ import { IWebsocketService } from 'src/app/modules/core/abstract-services/i-webs import { Router } from '@angular/router'; import { IContestService } from 'src/app/modules/core/abstract-services/i-contest.service'; import { AppTitleService } from 'src/app/modules/core/services/app-title.service'; -import { DEBUG_MODE } from 'src/constants' import { ContestClock } from 'src/app/modules/core/models/contest-clock'; +import { DEBUG_MODE } from 'src/constants'; +/* +LoginPageComponent is the initial page displayed by the WTI-UI Single-Page-Application (SPA). +It gets created as part of the "AppModule" bootstrapping process, which invokes its constructor +causing instantiation of each of the classes listed in the constructor parameter list +(note however that IContestService and IWebsocketService get created via invocation of "factory" methods +which are defined in, and exported from, class CoreModule). + +When LoginPageComponent is created, its "ngOnInit()" method gets invoked and checks to see if there is +already a "token" defined in the AuthService class (which would indicate the user is already logged in). +If so, it uses the Router to navigate to the "default route" defined in AuthService, which is the "/runs" page. + +Otherwise, it builds a "form" for the user to enter team name and password. When the user clicks "Submit" on that +form, the LoginPageComponent's "onSubmit()" method is invoked. onSubmit() invokes the AuthService's "login()" method, +which in turn makes an HTTP request to the login() method of the WTI server. onSubmit() subscribes to (waits for) +the response to this request, and if login was successful then it completes the login (saving login information in +the AuthService class and using the Router to transfer to the default "/runs" page), opens a WebSocket to the WTI server, +and invokes the ContestService to determine whether the contest clock is running (using the result to cause an SPA-local +"clock tick", which in turn has the effect of notifying class ProblemSelectorComponent whether or not to display +the list of contest problems). +*/ @Component({ templateUrl: './login-page.component.html', styleUrls: ['./login-page.component.scss'] @@ -21,45 +41,96 @@ export class LoginPageComponent implements OnInit, OnDestroy { formGroup: FormGroup; invalidCreds = false; loginStarted = false; - contestClock: ContestClock = new ContestClock(); constructor(private _formBuilder: FormBuilder, private _authService: AuthService, private _websocketService: IWebsocketService, private _router: Router, private _contestService: IContestService, - private _appTitleService: AppTitleService) { } + private _appTitleService: AppTitleService) { + + if (DEBUG_MODE) { + console.log ("Executing LoginPageComponent constructor") ; + } + } ngOnInit(): void { + if (DEBUG_MODE) { + console.log ("Executing LoginPageComponent.ngOnInit()") ; + } + this._appTitleService.setTitleWithTeamId("Login"); - - if (this._authService.token) { this._router.navigateByUrl(this._authService.defaultRoute); } - this.buildForm(); + + if (this._authService.token) { + if (DEBUG_MODE) { + console.log (" AuthService.token returns positive Truthy value; invoking Router to navigate to ") ; + console.log (" AuthService.defaultRoute '", this._authService.defaultRoute, "'") ; + } + this._router.navigateByUrl(this._authService.defaultRoute); + } + + if (DEBUG_MODE) { + console.log (" Invoking buildForm() " ) ; + } + this.buildForm(); } ngOnDestroy(): void { - this._unsubscribe.next(); - this._unsubscribe.complete(); + if (DEBUG_MODE) { + console.log ("Executing LoginPageComponent.ngOnDestroy(); invoking _unsubscribe.next() and then _unsubscribe.complete()") ; + } + this._unsubscribe.next(); + this._unsubscribe.complete(); } onSubmit(): void { + if (DEBUG_MODE) { + console.log ("Executing LoginPageComponent.onSubmit()...") ; + } this.loginStarted = true; const loginCreds = new LoginCredentials(); loginCreds.teamName = this.formGroup.get('username').value; loginCreds.password = this.formGroup.get('password').value; + if (DEBUG_MODE) { + console.log ("Invoking AuthService.login()") ; + } this._authService.login(loginCreds) .pipe(takeUntil(this._unsubscribe)) .subscribe((result: TeamsLoginResponse) => { + if (DEBUG_MODE) { + console.log ("Received callback from subscribing to AuthService.login();" ) ; + console.log (" Invoking AuthService.completeLogin()") ; + } this._authService.completeLogin(result.teamId, result.teamName); + if (DEBUG_MODE) { + console.log (" Invoking WebsocketService.startWebsocket()") ; + } this._websocketService.startWebsocket(); + if (DEBUG_MODE) { + console.log (" invoking ContestService.getisContestRunning() and subscribing to the result") ; + } this._contestService.getIsContestRunning() .subscribe((val: boolean) => { + if (DEBUG_MODE) { + console.log (" Subscription callback from ContestService.getIsContestRunning() returned: ", val); + console.log (" ContestService object has uniqueId", this._contestService.uniqueId); + //JSON.stringify() can't handle recursive objects like Angular Subjects + //console.log (JSON.stringify(this._contestService,null,2)); //obj, replacerFunction, indent + console.log (" and contents:"); + console.log (this._contestService); + } + if (DEBUG_MODE) { + console.log ("Setting ContestService.isContestRunning to '", val, "'") ; + } this._contestService.isContestRunning = val; - this._contestService.contestClock.next(); + if (DEBUG_MODE) { + console.log ("Invoking ContestService.contestClockEvent.next()") ; + } + this._contestService.contestClockEvent.next(); }); //get the actual contest clock info from the PC2 server via the Contest Service (which gets it via the WTI Server and its PC2 API) @@ -69,11 +140,13 @@ export class LoginPageComponent implements OnInit, OnDestroy { if (!data) { console.error ("Unable to get ContestClock from PC2 API via ContestService!"); } else { - //copy the data fields received from the PC2 Server (via the WTI-API) into the local ContestClock object - this.contestClock.isRunning = data.running ; - this.contestClock.contestLengthSecs = data.contestLengthInSecs ; - this.contestClock.elapsedSecs = data.elapsedSecs ; - this.contestClock.wallClockStartTime = data.wallClockStartTime ; + //copy the data fields received from the PC2 Server (via the WTI-API) into the ContestService's ContestClock object + let newContestClock = new ContestClock(); + newContestClock.isRunning = data.running ; + newContestClock.contestLengthSecs = data.contestLengthInSecs ; + newContestClock.elapsedSecs = data.elapsedSecs ; + newContestClock.wallClockStartTime = data.wallClockStartTime ; + this._contestService.updateContestClock(newContestClock); } }, (error: any) => { From 5291696b057542162500ac452594aff9ef0da8ed Mon Sep 17 00:00:00 2001 From: John Clevenger Date: Thu, 26 Dec 2024 12:19:18 -0800 Subject: [PATCH 030/141] i1027: IWebSocketService: make root-injectable; add debug output. --- .../abstract-services/i-websocket.service.ts | 48 +++++++++++++++---- 1 file changed, 39 insertions(+), 9 deletions(-) diff --git a/projects/WTI-UI/src/app/modules/core/abstract-services/i-websocket.service.ts b/projects/WTI-UI/src/app/modules/core/abstract-services/i-websocket.service.ts index 1d395b01a..b0d66ea6c 100644 --- a/projects/WTI-UI/src/app/modules/core/abstract-services/i-websocket.service.ts +++ b/projects/WTI-UI/src/app/modules/core/abstract-services/i-websocket.service.ts @@ -1,14 +1,23 @@ +import { Injectable } from '@angular/core'; import { UiHelperService } from '../services/ui-helper.service'; import { WebsocketMessage } from '../models/websocket-message'; import { IContestService } from './i-contest.service'; import { ITeamsService } from './i-teams.service'; import { AuthService } from '../auth/auth.service'; +import { DEBUG_MODE } from 'src/constants'; +@Injectable({ + providedIn: 'root' //forces the service to be a singleton across all app components ('root' == "root injector") +}) export abstract class IWebsocketService { constructor(private _uiHelperService: UiHelperService, private _contestService: IContestService, private _teamsService: ITeamsService, - public _authService: AuthService) { } + public _authService: AuthService) { + if (DEBUG_MODE) { + console.log("Executing IWebsocketService constructor..."); + } + } abstract startWebsocket(): void; @@ -18,35 +27,56 @@ export abstract class IWebsocketService { switch (message.type) { case 'test': case 'judged': { + if (DEBUG_MODE) { + console.log ("Got '", message.type, "' websocket message in IWebsocketService.incomingMessage()"); + } this._uiHelperService.incomingRun(message.id); this._teamsService.runsUpdated.next(); break; } case 'clarification': { + if (DEBUG_MODE) { + console.log ("Got '", message.type, "' websocket message in IWebsocketService.incomingMessage()"); + } this._uiHelperService.incomingClarification(message.id); this._contestService.clarificationsUpdated.next(); break; } case 'contest_clock': { + if (DEBUG_MODE) { + console.log ("Got '", message.type, "' websocket message in IWebsocketService.incomingMessage()"); + } this._contestService.getIsContestRunning() .subscribe((val: any) => { + if (DEBUG_MODE) { + console.log ("IWebsocketService.incomingMessage(): callback from ContestService.getIsContestRunning() returned '", val, "'"); + console.log ("Setting ContestService.isContestRunning to '", val, "'") ; + console.log (" and invoking ContestService.contestClockEvent.next()") ; + } + this._contestService.isContestRunning = val; - this._contestService.contestClock.next(); + this._contestService.contestClockEvent.next(); }); break; } case 'standings': { - console.log("got a Standings websocket message; marking standings out of date"); + if (DEBUG_MODE) { + console.log("Got a 'Standings' websocket message; marking standings out of date"); + } this._contestService.markStandingsOutOfDate(); break; } case 'connection_dropped': { - console.log("Got a connection_dropped websocket message:"); - console.log(message); - this._uiHelperService.indefinitelyDisplayedAlert("PC2 Server connection lost"); - this._authService.logout(); //invokes teamsService.logout(); - this._authService.completeLogout(); //navigates to login page - break; + if (DEBUG_MODE) { + console.log("Got a connection_dropped websocket message:"); + console.log(message); + console.log ("Invoking UIHelperService.indefinitelyDisplayedAlert('connection lost'") ; + console.log (" and invoking AuthService.logout() then AuthService.completeLogout()") ; + } + this._uiHelperService.indefinitelyDisplayedAlert("PC2 Server connection lost"); + this._authService.logout(); //invokes teamsService.logout(); + this._authService.completeLogout(); //navigates to login page + break; } default: console.warn('unrecognized message on websocket:'); From a08950843b92d371f4fc15a3cb95fc9a7be2bcf3 Mon Sep 17 00:00:00 2001 From: John Clevenger Date: Thu, 26 Dec 2024 12:23:40 -0800 Subject: [PATCH 031/141] i1027: IContestService: make root-injectable; also: - renamed contestClock to contestClockEvent; - added support for unique instant ids; - added abstract accessors for contest clock times; --- .../abstract-services/i-contest.service.ts | 47 +++++++++++++++++-- 1 file changed, 44 insertions(+), 3 deletions(-) diff --git a/projects/WTI-UI/src/app/modules/core/abstract-services/i-contest.service.ts b/projects/WTI-UI/src/app/modules/core/abstract-services/i-contest.service.ts index 73f262c5e..e832b9571 100644 --- a/projects/WTI-UI/src/app/modules/core/abstract-services/i-contest.service.ts +++ b/projects/WTI-UI/src/app/modules/core/abstract-services/i-contest.service.ts @@ -1,15 +1,50 @@ +import { Injectable } from '@angular/core'; import { Observable, Subject } from 'rxjs'; import { ContestLanguage } from '../models/contest-language'; import { ContestProblem } from '../models/contest-problem'; import { ContestClock } from '../models/contest-clock'; import { Clarification } from '../models/clarification'; +import { DEBUG_MODE } from 'src/constants'; +@Injectable({ + providedIn: 'root' //forces the service to be a singleton across all app components ('root' == "root injector") +}) export abstract class IContestService { + + //need to document how the "clarificationsUpdated" Subject works (see the description of contestClockEvent, below; + //this operates similarly although it is used in more places) clarificationsUpdated = new Subject(); - contestClock = new Subject(); //note that this is a WTI-UI-specific object used in conjunction with Websocket "clock" messages; - //it is NOT a "ContestClock" object in the sense of classes defined in core/models. + + //The contestClockEvent "Subject" is used as a toggle to indicate when contest clock-related events have occured. + //It is "subscribed to" by the "ProblemSelectorComponent.ngOnInit()" method (meaning, that component will get a callback + //whenever the contestClockEvent's "next()" method is invoked). The contestClockEvent's "next()" method is invoked in exactly one + //place: IWebsocketService.incomingMessage(), when a message of type "contest_clock" is received by the WTI-UI from the WTI-API + //(which in turn happens when the WTI-API receives a "contest clock configuration update" notice via the PC2 API). + //The effect of all this is that the ProblemSelectorComponent gets notified when "some change" has occurred in the state of the + //PC2 contest clock. This causes the ProblemSelectorComponent to execute its "loadProblems()" method; that method in turn checks + //whether the contest is currently RUNNING; if so, it loads the problem names; if not, it blanks out problem names. + //note that contestClockEvent is a WTI-UI-specific object used to coordinate with Websocket "clock" messages; + //it is NOT a "ContestClock" object in the sense of classes defined in core/models, nor an "Event" it PC2 terms. + contestClockEvent = new Subject(); + standingsUpdated = new Subject(); isContestRunning = false; + + //the WTI-UI representation of the PC2 Server's ContestClock (PC2 class ContestTime) + //todo: where does this get updated when the PC2 clock changes? ; + contestClock: ContestClock = new ContestClock(); + + //give each instance of IContestService a unique ID for debugging purposes + private static nextId: number = 1; + public uniqueId: number; + + + constructor () { + this.uniqueId = IContestService.nextId++; + if (DEBUG_MODE) { + console.log ("Executing IContestService constructor for unique instance ", this.uniqueId) ; + } + } abstract getLanguages(): Observable; @@ -21,8 +56,14 @@ export abstract class IContestService { abstract getIsContestRunning(): Observable; - abstract getContestClock(): Observable; //note that this refers to a "ContestClock" model object, not the "contestClock" subject above + abstract getContestClock(): Observable; //note this refers to a "ContestClock" model object, not the "contestClockEvent" Subject above + abstract updateContestClock (newClock: ContestClock): void; //update the WTI-UI representation of the PC2 contest clock + + abstract getElapsedSecs(): number; + + abstract getRemainingSecs(): number; + abstract getStandings(): Observable; abstract markStandingsOutOfDate(): void; From d07f77cb76bfcfac4145e852fd674c2869cdc0f1 Mon Sep 17 00:00:00 2001 From: John Clevenger Date: Thu, 26 Dec 2024 12:24:49 -0800 Subject: [PATCH 032/141] i1027: DisplayTimePipe: added debug output. --- .../src/app/modules/core/services/displayTimePipe.service.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/projects/WTI-UI/src/app/modules/core/services/displayTimePipe.service.ts b/projects/WTI-UI/src/app/modules/core/services/displayTimePipe.service.ts index 5082f3ac9..8307df7ad 100644 --- a/projects/WTI-UI/src/app/modules/core/services/displayTimePipe.service.ts +++ b/projects/WTI-UI/src/app/modules/core/services/displayTimePipe.service.ts @@ -23,10 +23,6 @@ export class DisplayTimePipe implements PipeTransform { const minutes = Math.floor((numSecs % (60 * 60)) / (60)); const seconds = Math.floor(numSecs % 60); - if (DEBUG_MODE) { - console.log("DisplayTimePipe.transform(): computed ", numSecs, " as ", days, "d ", hours, "h ", minutes, "m ", seconds, "s"); - } - const formattedHours = hours.toString().padStart(2, '0'); const formattedMinutes = minutes.toString().padStart(2, '0'); const formattedSeconds = seconds.toString().padStart(2, '0'); From 0e822915b9c53bac715e936b18e23583a4fea728 Mon Sep 17 00:00:00 2001 From: John Clevenger Date: Thu, 26 Dec 2024 12:26:17 -0800 Subject: [PATCH 033/141] i1027: ContestTimerService: added error output when remainingTime<0 --- .../src/app/modules/core/services/contestTimer.service.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/projects/WTI-UI/src/app/modules/core/services/contestTimer.service.ts b/projects/WTI-UI/src/app/modules/core/services/contestTimer.service.ts index 368978acf..600828df7 100644 --- a/projects/WTI-UI/src/app/modules/core/services/contestTimer.service.ts +++ b/projects/WTI-UI/src/app/modules/core/services/contestTimer.service.ts @@ -35,7 +35,12 @@ export class ContestTimerService { console.error ("ContestTimerService.setRemainingSecs(): cannot set remaining seconds while Timer is running; stop the timer first.") return; } else { - this.remainingSecs = newRemainingSecs>=0 ? newRemainingSecs : 0; + if (newRemainingSecs>=0) { + this.remainingSecs = newRemainingSecs; + } else { + console.error("ContestTimerService.setRemainingSecs(): attempt to set Remaining Time to less than zero not allowed; setting to zero instead."); + this.remainingSecs = 0; + } } } From ab9b09ae58d797e2443ad0c64e05a40877ec5b72 Mon Sep 17 00:00:00 2001 From: John Clevenger Date: Thu, 26 Dec 2024 12:30:17 -0800 Subject: [PATCH 034/141] i1027: ContestService: make root-injectable; add ContestTimer; also: - add support for updating ContestClock - add debug output --- .../modules/core/services/contest.service.ts | 74 +++++++++++++++++-- 1 file changed, 68 insertions(+), 6 deletions(-) diff --git a/projects/WTI-UI/src/app/modules/core/services/contest.service.ts b/projects/WTI-UI/src/app/modules/core/services/contest.service.ts index e1888991c..20857642a 100644 --- a/projects/WTI-UI/src/app/modules/core/services/contest.service.ts +++ b/projects/WTI-UI/src/app/modules/core/services/contest.service.ts @@ -6,17 +6,27 @@ import { HttpClient } from '@angular/common/http'; import { environment } from 'src/environments/environment'; import { ContestProblem } from '../models/contest-problem'; import { ContestClock } from '../models/contest-clock'; +import { ContestTimerService } from './contestTimer.service' ; import { Clarification } from '../models/clarification'; +import { DEBUG_MODE } from 'src/constants'; -@Injectable() +@Injectable({ + providedIn: 'root' //forces the service to be a singleton across all app components ('root' == "root injector") +}) export class ContestService extends IContestService { standingsAreCurrent: boolean ; cachedStandings: Observable ; + //the WTI-UI timer service which updates elapsed and remaining time when started + contestTimer: ContestTimerService = new ContestTimerService() ; + constructor(private _httpClient: HttpClient) { super(); - this.standingsAreCurrent = false; + if (DEBUG_MODE) { + console.log ("Executing ContestService constructor; instance ID = ", this.uniqueId) ; + } + this.standingsAreCurrent = false; } getLanguages(): Observable { @@ -36,24 +46,34 @@ export class ContestService extends IContestService { } getIsContestRunning(): Observable { - return this._httpClient.get(`${environment.baseUrl}/contest/isRunning`); + if (DEBUG_MODE) { + console.log ("Executing ContestService.getIsContestRunning(): calling HTTP client get(.../contest.isRunning)") ; + } + return this._httpClient.get(`${environment.baseUrl}/contest/isRunning`); } /** This method returns an Observable "ContestClock" object -- a WTI-UI model corresponding to the PC2 "ContestTime" class, * which itself encapsulates the "contest clock" on the PC2 server. + * TODO: should this method return the local copy of the ContestClock? */ getContestClock(): Observable { return this._httpClient.get(`${environment.baseUrl}/contest/contestclock`); } getStandings(): Observable { - console.log("ContestService.getStandings():") + if (DEBUG_MODE) { + console.log("ContestService.getStandings():") + } if (!this.standingsAreCurrent) { - console.log ("Standings are out of date; fetching new standings"); + if (DEBUG_MODE) { + console.log ("Standings are out of date; fetching new standings"); + } this.cachedStandings = this._httpClient.get(`${environment.baseUrl}/contest/scoreboard`); this.standingsAreCurrent = true ; } else { - console.log("Returning cached standings"); + if (DEBUG_MODE) { + console.log("Returning cached standings"); + } } return this.cachedStandings ; } @@ -65,5 +85,47 @@ export class ContestService extends IContestService { getStandingsAreCurrentFlag() : boolean { return this.standingsAreCurrent ; } + + updateContestClock (newContestClock: ContestClock) { + //save the new clock + this.contestClock = newContestClock; + + //pull the relevant values out of the new clock + let timerShouldBeStarted = this.contestClock.isRunning === 'true'; + let elapsedSecs = parseInt(this.contestClock.elapsedSecs); + let contestLengthSecs = parseInt(this.contestClock.contestLengthSecs); + let remainingSecs = contestLengthSecs - elapsedSecs; + + //shut off timer if it is running (otherwise we can't update the elapsed/remaining time values) + if (this.contestTimer.isTimerRunning) { + this.contestTimer.stopTimer(); + } + + //store the new contest time values in the Timer + this.contestTimer.setElapsedSecs(elapsedSecs); + this.contestTimer.setRemainingSecs(remainingSecs); + + //restart the timer if the new contest clock values indicate it should be running + if (timerShouldBeStarted) { + this.contestTimer.startTimer(); + } + } + + enableContestTimerUpdates() { + this.contestTimer.startTimer(); + } + + disableContestTimerUpdates() { + this.contestTimer.stopTimer(); + } + + getElapsedSecs(): number { + return parseInt(this.contestClock.elapsedSecs) ; + } + + getRemainingSecs(): number { + let remainingSecs = parseInt(this.contestClock.contestLengthSecs) - parseInt(this.contestClock.elapsedSecs) ; + return remainingSecs ; + } } From b6125c48a7a289c3ef497a9966182a08a44623ae Mon Sep 17 00:00:00 2001 From: John Clevenger Date: Thu, 26 Dec 2024 12:31:10 -0800 Subject: [PATCH 035/141] i1027: ContestMockService: add mock support for updating contest clock --- .../modules/core/services/contest.mock.service.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/projects/WTI-UI/src/app/modules/core/services/contest.mock.service.ts b/projects/WTI-UI/src/app/modules/core/services/contest.mock.service.ts index 406ecd1f7..c9d387e28 100644 --- a/projects/WTI-UI/src/app/modules/core/services/contest.mock.service.ts +++ b/projects/WTI-UI/src/app/modules/core/services/contest.mock.service.ts @@ -93,6 +93,18 @@ export class ContestMockService extends IContestService { {isRunning:'true', contestLengthSecs:'18000', elapsedSecs:'3600', wallClockStartTime:'1734593847'}); } + updateContestClock (newContestClock: ContestClock) { + //nothing to do -- this is a mock service + } + + getElapsedSecs(): number { + return 3600; //mock: 1 hour has elapsed + } + + getRemainingSecs(): number { + return 18000 - 3600 ; //mock: 5 hour contest with one hour elapsed + } + getStandings(): Observable { //TODO: this method needs to return a legitimate (mock) team standing array! From c622bdd8f24b3e613a95349ec977de9d446bb700 Mon Sep 17 00:00:00 2001 From: John Clevenger Date: Thu, 26 Dec 2024 12:34:18 -0800 Subject: [PATCH 036/141] i1027: AppComponent: add support matching i871-F5-in-WTI. --- projects/WTI-UI/src/app/app.component.ts | 284 +++++++++++++++++++++-- 1 file changed, 270 insertions(+), 14 deletions(-) diff --git a/projects/WTI-UI/src/app/app.component.ts b/projects/WTI-UI/src/app/app.component.ts index dd09d3226..774cc5dcd 100644 --- a/projects/WTI-UI/src/app/app.component.ts +++ b/projects/WTI-UI/src/app/app.component.ts @@ -1,28 +1,284 @@ import { Component, OnInit } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { environment } from 'src/environments/environment'; +import * as Constants from 'src/constants'; +import { Router } from '@angular/router'; +import { AuthService } from 'src/app/modules/core/auth/auth.service' ; +import { IContestService } from 'src/app/modules/core/abstract-services/i-contest.service' ; +import { IWebsocketService } from 'src/app/modules/core/abstract-services/i-websocket.service' ; +import { UiHelperService } from 'src/app/modules/core/services/ui-helper.service' +import { DEBUG_MODE } from 'src/constants'; + +/* +This AppComponent class is the main starting point for the WTI-UI Angular Single-Page-Application (SPA). +(The overall SPA starts in main.ts, which invokes app.module.ts, which in turn bootstraps this AppComponent class.) + +When app.module.ts invokes this class's constructor, the constructor parameters cause TypeScript to automatically construct +local property variables (objects) of type HttpClient, Router, AuthService, IContestService, and IWebsocketService. +Construcing the AuthService object in turn causes creation of an ITeamsService object. + +The AuthService, IContestService, ITeamsService, and IWebsocketService classes are all listed in the "providers" array +of class CoreModule (core.module.ts), which means that CoreModule is responsible for providing those service classes. +All four service classes are marked as "injectable", which means they can be injected into other classes. All four classes +are marked as "providedIn: 'root'" in their "@Injectable" decorator, which means they are all defined as singletons provided +by (injected by) the "root injector" + +The latter three classes (IContestService, ITeamsService, and IWebsocketService) are listed in CoreModule with "provide" properties +indicating that they are to be "provided" by corresponding "factory methods" (also in CoreModule). These factory methods choose +between "real" and "mock" service providers depending on the value of an "environment flag" named "useMock" (see the files +under "environments"). + +Once construction is complete, this AppComponent then starts running at its "ngOnInit()" method. +ngOnInit() checks the browser's "sessionStorage" to see if there is a "current page" recorded there. +If not, AppComponent invokes "loadEnvironment()" to load the SPA configuration from file "assets/appconfig.json". + +If an existing "current page" IS found in "sessionStorage", AppComponent interprets it as indicating that a "browser refresh" +(F5) has occurred. In this case, AppComponent performs the same "loadEnvironment() operations, then loads additional state +information (userName and token value) from "sessionStorage" and restores it into the AuthService class. +It then startsa new WebSocket for communication with the WTI Server, checks the ContestService "isContestRunning" value +(using it to trigger updates to things like display of problem names), and finally uses the Router to navigate to the +previous SPA page. + +Meanwhile, as part of the AppModule bootstrap process, module "AppRoutingModule" sets up a list of "available routes" +(that is, pages which the SPA knows how to transfer to), with the first (default) route being the LoginPageComponent. +This causes the SPA to display the LoginPageComponent, which builds a "submission form" for the user to enter +team name and password. When this form's "Submit" button is clicked, +the LoginPageComponent.onSubmit() method invokes the AuthService's "login" method, which connects to +the WTI server and logs the user into the PC2 server, then (on successful login) uses the Router's +path list to transfer to the "/runs" page. + +*/ @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.scss'] }) + export class AppComponent implements OnInit { configLoaded = false; - constructor(private _httpClient: HttpClient) { } + constructor(private _httpClient: HttpClient, + public router: Router, + private _authService: AuthService, + private _contestService: IContestService, + private _websocketService: IWebsocketService, + private _uiHelperService: UiHelperService) { + if (DEBUG_MODE) { + console.log("Executing AppComponent constructor..."); + } + //this.router.events.subscribe(console.log); //shows router tracing on console + } ngOnInit(): void { - // Load appconfig.json from assets directory, overwrite environment.ts - // with these values - this._httpClient.get('assets/appconfig.json') - .subscribe((data: any) => { - this.configLoaded = true; - if (!data) { return; } - Object.keys(data).forEach((key: string) => environment[key] = data[key]); - }, (error: any) => { - console.log('could not find appconfig.json in assets directory. using default values!'); - this.configLoaded = true; - }); - } -} + + if (DEBUG_MODE) { + console.log ("Entering AppComponent.ngOnInit()..."); + console.log ("...sessionStorage.length = " + sessionStorage.length); + if (sessionStorage.length > 0) { + console.log ("...sessionStorage values:"); + for (let i = 0; i < sessionStorage.length; i++) { + let key = sessionStorage.key(i); + let value = sessionStorage.getItem(key); + console.log(" Key: ", key, " Value: ", value); + } + } + } + + //check if we're loading for the first time + if (!getCurrentPage()) { + if (DEBUG_MODE) { + console.log ('Starting Single-Page-Application from scratch...'); + } + //we have no current page so we must be starting from scratch; + // the following matches the sum total of what ngOnInit() used to do before "F5 handling" was added -- jlc + this.loadEnvironment(); + + } else { + //there is a current page stored; we must be reloading from (e.g.) an F5 refresh + if (DEBUG_MODE) { + console.log ('Restarting Single-Page-Application after refresh navigation...'); + } + + //restore former environment + if (DEBUG_MODE) { + console.log ("...Loading environment...") ; + } + this.loadEnvironment(); + + //The following was initially done by the login-page component's onSubmit() method during login; + // this F5 "restore state" code needs to accomplish the equivalent: + //this._authService.login(loginCreds) + // .pipe(takeUntil(this._unsubscribe)) + // .subscribe((result: TeamsLoginResponse) => { + // this._authService.completeLogin(result.teamId, result.teamName); //save 'teamId' returned from HTTP login "token" + // this._websocketService.startWebsocket(); //create a websocket connection to the WTI Server + // this._contestService.getIsContestRunning() //get contest isRunning state and save in ContestService + // .subscribe((val: boolean) => { + // this._contestService.isContestRunning = val; + // this._contestService.contestClock.next(); + // }); + // } + + //restore the connection token and username into the AuthService from browser sessionStorage + let token = getCurrentToken(); + let username = getCurrentUserName(); + if (DEBUG_MODE) { + if (!!token && !!username) { + console.log("...restoring token '" + token + "' and username '" + username + "' into AuthService..." ); + } + } + + //check whether we found non-null token and username from sessionStorage + if (! (!!token && !!username) ) { + console.warn ("Attempting to reload after F5 restart but found invalid state: token = ", token, " and username = ", username, " !!") ; + //TODO: what should happen if the above occurs?? + } else { + //put the token and username back into AuthService (accomplishing what was originally done + // by AuthService.completeLogin(teamId, teamName); teamId is the "token") + this._authService.token = token; + this._authService.username = username; + } + + //re-create websocket connection to the WTI Server + if (DEBUG_MODE) { + console.log ("...calling WebsocketService.startWebsocket()..."); + } + this._websocketService.startWebsocket(); + + //update the "is contest running" value in the IContestService object using the value returned from + // an HTTP call within ContestService.getIsContestRunning() + this._contestService.getIsContestRunning() + .subscribe((val: boolean) => { + if (DEBUG_MODE) { + console.log (" Subscription callback from ContestService.getIsContestRunning() returned: ", val); + console.log (" ContestService object has uniqueId", this._contestService.uniqueId); + //JSON.stringify() can't handle recursive objects like Angular Subjects + //console.log (JSON.stringify(this._contestService,null,2)); //obj, replacerFunction, indent + console.log (" and contents:"); + console.log (this._contestService); + } + this._contestService.isContestRunning = val; + this._contestService.contestClockEvent.next(); + }); + + //get the most recently saved Option values from sessionStorage + let options = getOptions(); + + //if options were obtained, store them back into the OptionsPageComponent radio buttons + if (!!options) { + if (DEBUG_MODE) { + console.log ("Calling 'restoreOptions' to restore Options Page settings; values retrieved from sessionStorage are:") ; + console.log (" ClarsNotificationsEnabled option = ", options.clarsNotificationsEnabled); + console.log (" RunsNotificationsEnabled option = ", options.runsNotificationsEnabled); + } + this.restoreOptions (options); + + } else { + if (DEBUG_MODE) { + console.log ("Got null options from sessionStorage; nothing to restore for Options page.") + } + } + + // transfer to the (former) "current page". + let page = getCurrentPage(); + if (DEBUG_MODE) { + console.log ('...navigating to previous page: ' + page); + } + + //navigate to the most recently saved page + // TODO: consider whether using history.pushState()/popState() is a better solution for this... + this.router.navigate([page]) + .then(nav => { + if (DEBUG_MODE) { + console.log ("...navigation complete."); + } + }, err => { + console.log("Call to router.navigate([" + page + "]) failed (returned '" + err + "')"); + }); + + } + + } + + // Load appconfig.json from assets directory, overwrite environment.ts with these values + loadEnvironment(): void { + if (DEBUG_MODE) { + console.log("...loading environment from 'assets/appconfig.json'..."); + } + this._httpClient.get('assets/appconfig.json') + .subscribe((data: any) => { + this.configLoaded = true; + if (!data) { return; } + Object.keys(data).forEach((key: string) => environment[key] = data[key]); + }, (error: any) => { + console.log('Could not find appconfig.json in assets directory. using default values!'); + this.configLoaded = true; + }); + }//end function loadEnvironment() + + /** + * Accept the received Options object and install the values therein into the OptionsPage Clarifications and Runs radio button options. + */ + restoreOptions(options: any) { + if (DEBUG_MODE) { + console.log ("Executing AppComponent.restoreOptions()..."); + console.log ("...received option values are: 'clarsNoficationsEnabled='", options.clarsNotificationsEnabled); + console.log ("...and 'runsNotificationsEnabled='", options.runsNotificationsEnabled); + } + this._uiHelperService.enableClarificationNotifications = options.clarsNotificationsEnabled; + this._uiHelperService.enableRunsNotifications = options.runsNotificationsEnabled; + + } +}//end class AppComponent + + //save the current page in sessionStorage so a subsequent F5 (refresh) can return to that page + export function saveCurrentPage(page:string) { + sessionStorage.setItem(Constants.CURRENT_PAGE_KEY, page) ; + } + + //return the most recently saved page + export function getCurrentPage() { + return (sessionStorage.getItem(Constants.CURRENT_PAGE_KEY)); + } + + //clear any record of a "current page" + export function clearCurrentPage() { + sessionStorage.removeItem(Constants.CURRENT_PAGE_KEY); + } + + //save the current token in sessionStorage so a subsequent F5 (refresh) can restore it + export function saveCurrentToken(token:string) { + sessionStorage.setItem(Constants.CONNECTION_TOKEN_KEY, token) ; + } + + //return the most recently saved token + export function getCurrentToken() { + return (sessionStorage.getItem(Constants.CONNECTION_TOKEN_KEY)); + } + + //save the current username in sessionStorage so a subsequent F5 (refresh) can restore it + export function saveCurrentUserName(username:string) { + sessionStorage.setItem(Constants.CONNECTION_USERNAME_KEY, username) ; + } + + //return the most recently saved username + export function getCurrentUserName() { + return (sessionStorage.getItem(Constants.CONNECTION_USERNAME_KEY)); + } + + //save the current options in sessionStorage so a subsequent F5 (refresh) can restore it + export function saveOptions(options: Object) { + return (sessionStorage.setItem(Constants.OPTIONS_DETAILS_KEY, JSON.stringify(options))); + } + + //return the most recently saved option values from sessionStorage + export function getOptions() { + const value = sessionStorage.getItem(Constants.OPTIONS_DETAILS_KEY); + return value ? JSON.parse(value) : null; + } + + //clear all data in sessionStorage + export function clearSessionStorage() { + sessionStorage.clear(); + } From 5fc8c5174458aa4c5454c602f345413759374dcb Mon Sep 17 00:00:00 2001 From: John Clevenger Date: Thu, 26 Dec 2024 12:37:53 -0800 Subject: [PATCH 037/141] i1027: moved timer support out of AppHeader (into ContestService); also: replaced use of ngx-countdown for remaining time with ContestService timers. --- .../app-header/app-header.component.html | 12 ++++--- .../app-header/app-header.component.ts | 33 ++++++++----------- 2 files changed, 21 insertions(+), 24 deletions(-) diff --git a/projects/WTI-UI/src/app/modules/shared/components/app-header/app-header.component.html b/projects/WTI-UI/src/app/modules/shared/components/app-header/app-header.component.html index 132082b19..4f3354b3a 100644 --- a/projects/WTI-UI/src/app/modules/shared/components/app-header/app-header.component.html +++ b/projects/WTI-UI/src/app/modules/shared/components/app-header/app-header.component.html @@ -7,10 +7,10 @@
Elapsed:
- {{ elapsedSecs }}
- - {{ getStartDate() | elapsedTime }} + {{ getElapsedSecs() }}
+ + {{ getElapsedSecs() | displayTime }}
@@ -27,7 +27,9 @@
Remaining:
- + + {{ getRemainingSecs() | displayTime }} +
diff --git a/projects/WTI-UI/src/app/modules/shared/components/app-header/app-header.component.ts b/projects/WTI-UI/src/app/modules/shared/components/app-header/app-header.component.ts index 4fcca263a..90373c247 100644 --- a/projects/WTI-UI/src/app/modules/shared/components/app-header/app-header.component.ts +++ b/projects/WTI-UI/src/app/modules/shared/components/app-header/app-header.component.ts @@ -1,6 +1,7 @@ import { Component } from '@angular/core'; import { AuthService } from 'src/app/modules/core/auth/auth.service'; -import { ElapsedTimePipe } from 'src/app/modules/core/services/elapsedTimePipe.service'; +import { IContestService } from 'src/app/modules/core/abstract-services/i-contest.service'; +import { DEBUG_MODE } from 'src/constants' @Component({ selector: 'app-header', @@ -26,26 +27,20 @@ export class AppHeaderComponent { return teamId; } - elapsedSecs = 0 ; - - getElapsedTimeAsDate(): Date { - let newDate = new Date(this.elapsedSecs * 1000); - console.log(newDate.toString()); - return newDate ; + constructor(private _authService: AuthService, private _contestService: IContestService) { + if (DEBUG_MODE ) { + console.log ("Executing AppHeaderComponent constructor") + } } - constructor(private _authService: AuthService) { - - setInterval( - //execute this function at the specified interval: - () => { - //add 1 second to the counter since 1000msec have passed - this.elapsedSecs += 1 ; - console.log(`Seconds: ${this.elapsedSecs}`); - }, - 1000 // 1000 milliseconds = 1 second interval - ); - + getElapsedSecs(): number { + let secs = this._contestService.getElapsedSecs() ; + return secs; + } + + getRemainingSecs(): number { + let secs = this._contestService.getRemainingSecs() ; + return secs; } } From 23da9ff82aed1ef3b17761c8fe879eb9b67fe013 Mon Sep 17 00:00:00 2001 From: John Clevenger Date: Thu, 26 Dec 2024 16:00:21 -0800 Subject: [PATCH 038/141] i1027: ContestTimerService: added conditional debugging output. --- .../core/services/contestTimer.service.ts | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/projects/WTI-UI/src/app/modules/core/services/contestTimer.service.ts b/projects/WTI-UI/src/app/modules/core/services/contestTimer.service.ts index 600828df7..09747bcab 100644 --- a/projects/WTI-UI/src/app/modules/core/services/contestTimer.service.ts +++ b/projects/WTI-UI/src/app/modules/core/services/contestTimer.service.ts @@ -14,10 +14,16 @@ export class ContestTimerService { } getElapsedSecs(): number { + if (DEBUG_MODE) { + console.log("ContestTimerService.getElapsedSecs(): returning ", this.elapsedSecs); + } return this.elapsedSecs; } getRemainingSecs(): number { + if (DEBUG_MODE) { + console.log("ContestTimerService.getRemainingSecs(): returning ", this.remainingSecs); + } return this.remainingSecs; } @@ -26,6 +32,9 @@ export class ContestTimerService { console.error ("ContestTimerService.setElapsedSecs(): cannot set elapsed seconds while Timer is running; stop the timer first.") return; } else { + if (DEBUG_MODE) { + console.log ("ContestTimerService.setElapsedSecs(): setting elapsed seconds to ", newElapsedSecs); + } this.elapsedSecs = newElapsedSecs; } } @@ -35,6 +44,9 @@ export class ContestTimerService { console.error ("ContestTimerService.setRemainingSecs(): cannot set remaining seconds while Timer is running; stop the timer first.") return; } else { + if (DEBUG_MODE) { + console.log ("ContestTimerService.setRemainingTime(): setting remaining seconds to ", newRemainingSecs); + } if (newRemainingSecs>=0) { this.remainingSecs = newRemainingSecs; } else { @@ -59,7 +71,9 @@ export class ContestTimerService { //bump the elapsed & remaining counters since 1000msec (one sec) has passed this.elapsedSecs += 1 ; this.remainingSecs -= 1 - console.log(`Elapsed secs: ${this.elapsedSecs}; remaining secs: ${this.remainingSecs}`); + if (DEBUG_MODE) { + console.log(`Elapsed secs: ${this.elapsedSecs}; remaining secs: ${this.remainingSecs}`); + } }, 1000 // 1000 milliseconds = 1 second interval ); From d067987a5fa92f57d52240efb691670f6c4fc9c8 Mon Sep 17 00:00:00 2001 From: John Clevenger Date: Thu, 26 Dec 2024 16:15:21 -0800 Subject: [PATCH 039/141] i1027: ContestService: added conditional debugging output. --- .../modules/core/services/contest.service.ts | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/projects/WTI-UI/src/app/modules/core/services/contest.service.ts b/projects/WTI-UI/src/app/modules/core/services/contest.service.ts index 20857642a..f9c82c836 100644 --- a/projects/WTI-UI/src/app/modules/core/services/contest.service.ts +++ b/projects/WTI-UI/src/app/modules/core/services/contest.service.ts @@ -88,6 +88,10 @@ export class ContestService extends IContestService { updateContestClock (newContestClock: ContestClock) { //save the new clock + if (DEBUG_MODE) { + console.log("ContestService (id ", this.uniqueId, ").updateContestClock(): replacing Contest Clock with:"); + console.log(newContestClock); + } this.contestClock = newContestClock; //pull the relevant values out of the new clock @@ -98,6 +102,9 @@ export class ContestService extends IContestService { //shut off timer if it is running (otherwise we can't update the elapsed/remaining time values) if (this.contestTimer.isTimerRunning) { + if (DEBUG_MODE) { + console.log("ContestService (id ", this.uniqueId, ").updateContestClock(): stopping timer"); + } this.contestTimer.stopTimer(); } @@ -107,24 +114,39 @@ export class ContestService extends IContestService { //restart the timer if the new contest clock values indicate it should be running if (timerShouldBeStarted) { + if (DEBUG_MODE) { + console.log("ContestService (id ", this.uniqueId, ").updateContestClock(): starting timer"); + } this.contestTimer.startTimer(); } } enableContestTimerUpdates() { + if (DEBUG_MODE) { + console.log("ContestService (id ", this.uniqueId, ").enableContestTimerUpdates(): starting timer"); + } this.contestTimer.startTimer(); } disableContestTimerUpdates() { + if (DEBUG_MODE) { + console.log("ContestService (id ", this.uniqueId, ").disableContestTimerUpdates(): stopping timer"); + } this.contestTimer.stopTimer(); } getElapsedSecs(): number { + if (DEBUG_MODE) { + console.log("ContestService (id ", this.uniqueId, ").getElapsedSecs(): returning ", this.contestClock.elapsedSecs); + } return parseInt(this.contestClock.elapsedSecs) ; } getRemainingSecs(): number { let remainingSecs = parseInt(this.contestClock.contestLengthSecs) - parseInt(this.contestClock.elapsedSecs) ; + if (DEBUG_MODE) { + console.log("ContestService (id ", this.uniqueId, ").getRemainingSecs(): returning ", remainingSecs); + } return remainingSecs ; } From c6fb9b11275572a656d6504d822ca804137d6582 Mon Sep 17 00:00:00 2001 From: John Clevenger Date: Thu, 26 Dec 2024 16:27:15 -0800 Subject: [PATCH 040/141] i1027: ContestService: return elapsed/remainging from Timer. --- .../src/app/modules/core/services/contest.service.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/projects/WTI-UI/src/app/modules/core/services/contest.service.ts b/projects/WTI-UI/src/app/modules/core/services/contest.service.ts index f9c82c836..18a43c0f5 100644 --- a/projects/WTI-UI/src/app/modules/core/services/contest.service.ts +++ b/projects/WTI-UI/src/app/modules/core/services/contest.service.ts @@ -136,16 +136,17 @@ export class ContestService extends IContestService { } getElapsedSecs(): number { + let elapsedSecs = this.contestTimer.getElapsedSecs(); if (DEBUG_MODE) { - console.log("ContestService (id ", this.uniqueId, ").getElapsedSecs(): returning ", this.contestClock.elapsedSecs); + console.log("ContestService (id ", this.uniqueId, ").getElapsedSecs(): returning elapsed secs from ContestTimer: ", elapsedSecs); } - return parseInt(this.contestClock.elapsedSecs) ; + return elapsedSecs ; } getRemainingSecs(): number { - let remainingSecs = parseInt(this.contestClock.contestLengthSecs) - parseInt(this.contestClock.elapsedSecs) ; + let remainingSecs = this.contestTimer.getRemainingSecs(); if (DEBUG_MODE) { - console.log("ContestService (id ", this.uniqueId, ").getRemainingSecs(): returning ", remainingSecs); + console.log("ContestService (id ", this.uniqueId, ").getRemainingSecs(): returning remaining secs from ContestTimer: ", remainingSecs); } return remainingSecs ; } From 95ef304f1eba75947ea86cd98365663c52af94de Mon Sep 17 00:00:00 2001 From: John Clevenger Date: Thu, 26 Dec 2024 16:42:03 -0800 Subject: [PATCH 041/141] i1027: LoginPageComponent: added conditional debugging output. --- .../components/login-page/login-page.component.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/projects/WTI-UI/src/app/modules/login/components/login-page/login-page.component.ts b/projects/WTI-UI/src/app/modules/login/components/login-page/login-page.component.ts index db0ab8fdd..d9583754d 100644 --- a/projects/WTI-UI/src/app/modules/login/components/login-page/login-page.component.ts +++ b/projects/WTI-UI/src/app/modules/login/components/login-page/login-page.component.ts @@ -134,18 +134,28 @@ export class LoginPageComponent implements OnInit, OnDestroy { }); //get the actual contest clock info from the PC2 server via the Contest Service (which gets it via the WTI Server and its PC2 API) + if (DEBUG_MODE) { + console.log(" Invoking ContestService.getContestClock(), subscribing for callback result") + } this._contestService.getContestClock() .subscribe( (data: any) => { if (!data) { - console.error ("Unable to get ContestClock from PC2 API via ContestService!"); + console.error ("LoginPageComponent.onSubmit() ContestClock subscription callback: unable to get ContestClock from PC2 API via ContestService!"); } else { //copy the data fields received from the PC2 Server (via the WTI-API) into the ContestService's ContestClock object + if (DEBUG_MODE) { + console.log("LoginPageComponent.onSubmit(): got callback from ContestService.getContestClock() subscription; callback data ="); + console.log(data); + } let newContestClock = new ContestClock(); newContestClock.isRunning = data.running ; newContestClock.contestLengthSecs = data.contestLengthInSecs ; newContestClock.elapsedSecs = data.elapsedSecs ; newContestClock.wallClockStartTime = data.wallClockStartTime ; + if (DEBUG_MODE) { + console.log("LoginPageComponent.onSubmit() ContestService.getContestClock() subscription callback: calling ContestService.updateContestClock() with new clock data) "); + } this._contestService.updateContestClock(newContestClock); } }, From 8793db2252d42db57bc57108ee9d3ea726c2a960 Mon Sep 17 00:00:00 2001 From: John Clevenger Date: Thu, 26 Dec 2024 16:55:15 -0800 Subject: [PATCH 042/141] i1027: AppHeaderComponent: added conditional debugging output. --- .../shared/components/app-header/app-header.component.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/projects/WTI-UI/src/app/modules/shared/components/app-header/app-header.component.ts b/projects/WTI-UI/src/app/modules/shared/components/app-header/app-header.component.ts index 90373c247..b04fe1b38 100644 --- a/projects/WTI-UI/src/app/modules/shared/components/app-header/app-header.component.ts +++ b/projects/WTI-UI/src/app/modules/shared/components/app-header/app-header.component.ts @@ -36,11 +36,17 @@ export class AppHeaderComponent { getElapsedSecs(): number { let secs = this._contestService.getElapsedSecs() ; + if (DEBUG_MODE) { + console.log("AppHeaderComponent.getElapsedSecs(): returning ", secs); + } return secs; } getRemainingSecs(): number { let secs = this._contestService.getRemainingSecs() ; + if (DEBUG_MODE) { + console.log("AppHeaderComponent.getRemainingSecs(): returning ", secs); + } return secs; } } From 0c133933e684d9a9d4e009cc25052c345b725dbc Mon Sep 17 00:00:00 2001 From: John Clevenger Date: Fri, 27 Dec 2024 12:34:08 -0800 Subject: [PATCH 043/141] i1027: ContestClock: added additional documentation. --- projects/WTI-UI/src/app/modules/core/models/contest-clock.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/projects/WTI-UI/src/app/modules/core/models/contest-clock.ts b/projects/WTI-UI/src/app/modules/core/models/contest-clock.ts index 474f7c012..dd31d6ccc 100644 --- a/projects/WTI-UI/src/app/modules/core/models/contest-clock.ts +++ b/projects/WTI-UI/src/app/modules/core/models/contest-clock.ts @@ -3,6 +3,9 @@ * whether the contest is running or paused right now, and how much time has elapsed on the contest clock since the * contest started (note that this latter value does NOT include any real time which passed while the contest was "paused"). * Essentially, this model class corresponds to the PC2 server class "ContestTime". + * Note however that because the (Java-based) PC2 class "ContestTime" is returned from the WTI Server as a JSON string, + * all of the values in the WTI-UI ContestClock model are type 'string' and must be converted to the appropriate type when + * they are actually used. */ export class ContestClock { isRunning: string = ''; //string 'true' or 'false' From 865767b92035f87bbc1c53bf583381daffd96d52 Mon Sep 17 00:00:00 2001 From: John Clevenger Date: Fri, 27 Dec 2024 12:50:33 -0800 Subject: [PATCH 044/141] i1027: commented-out no-longer-needed debug output. --- .../modules/core/services/contest.service.ts | 18 ++++++++++++++---- .../core/services/contestTimer.service.ts | 9 ++++++--- .../app-header/app-header.component.ts | 8 +++++--- 3 files changed, 25 insertions(+), 10 deletions(-) diff --git a/projects/WTI-UI/src/app/modules/core/services/contest.service.ts b/projects/WTI-UI/src/app/modules/core/services/contest.service.ts index 18a43c0f5..d5b5f7820 100644 --- a/projects/WTI-UI/src/app/modules/core/services/contest.service.ts +++ b/projects/WTI-UI/src/app/modules/core/services/contest.service.ts @@ -94,12 +94,20 @@ export class ContestService extends IContestService { } this.contestClock = newContestClock; - //pull the relevant values out of the new clock - let timerShouldBeStarted = this.contestClock.isRunning === 'true'; + //pull the values out of the updated clock + let timerShouldBeStarted = this.contestClock.isRunning ; let elapsedSecs = parseInt(this.contestClock.elapsedSecs); let contestLengthSecs = parseInt(this.contestClock.contestLengthSecs); let remainingSecs = contestLengthSecs - elapsedSecs; +/* if (DEBUG_MODE) { + console.log ("ContestService.updateContestClock(): values pulled from newContestClock:"); + console.log (" timerShouldBeStarted = ", timerShouldBeStarted); + console.log (" elapsedSecs = ", elapsedSecs); + console.log (" contestLengthSecs = ", contestLengthSecs); + console.log (" remainingSecs = ", remainingSecs); + } +*/ //shut off timer if it is running (otherwise we can't update the elapsed/remaining time values) if (this.contestTimer.isTimerRunning) { if (DEBUG_MODE) { @@ -137,17 +145,19 @@ export class ContestService extends IContestService { getElapsedSecs(): number { let elapsedSecs = this.contestTimer.getElapsedSecs(); - if (DEBUG_MODE) { +/* if (DEBUG_MODE) { console.log("ContestService (id ", this.uniqueId, ").getElapsedSecs(): returning elapsed secs from ContestTimer: ", elapsedSecs); } +*/ return elapsedSecs ; } getRemainingSecs(): number { let remainingSecs = this.contestTimer.getRemainingSecs(); - if (DEBUG_MODE) { +/* if (DEBUG_MODE) { console.log("ContestService (id ", this.uniqueId, ").getRemainingSecs(): returning remaining secs from ContestTimer: ", remainingSecs); } +*/ return remainingSecs ; } diff --git a/projects/WTI-UI/src/app/modules/core/services/contestTimer.service.ts b/projects/WTI-UI/src/app/modules/core/services/contestTimer.service.ts index 09747bcab..2ae8db0aa 100644 --- a/projects/WTI-UI/src/app/modules/core/services/contestTimer.service.ts +++ b/projects/WTI-UI/src/app/modules/core/services/contestTimer.service.ts @@ -14,16 +14,18 @@ export class ContestTimerService { } getElapsedSecs(): number { - if (DEBUG_MODE) { +/* if (DEBUG_MODE) { console.log("ContestTimerService.getElapsedSecs(): returning ", this.elapsedSecs); } +*/ return this.elapsedSecs; } getRemainingSecs(): number { - if (DEBUG_MODE) { +/* if (DEBUG_MODE) { console.log("ContestTimerService.getRemainingSecs(): returning ", this.remainingSecs); } +*/ return this.remainingSecs; } @@ -71,9 +73,10 @@ export class ContestTimerService { //bump the elapsed & remaining counters since 1000msec (one sec) has passed this.elapsedSecs += 1 ; this.remainingSecs -= 1 - if (DEBUG_MODE) { +/* if (DEBUG_MODE) { console.log(`Elapsed secs: ${this.elapsedSecs}; remaining secs: ${this.remainingSecs}`); } +*/ }, 1000 // 1000 milliseconds = 1 second interval ); diff --git a/projects/WTI-UI/src/app/modules/shared/components/app-header/app-header.component.ts b/projects/WTI-UI/src/app/modules/shared/components/app-header/app-header.component.ts index b04fe1b38..89cb6b543 100644 --- a/projects/WTI-UI/src/app/modules/shared/components/app-header/app-header.component.ts +++ b/projects/WTI-UI/src/app/modules/shared/components/app-header/app-header.component.ts @@ -36,17 +36,19 @@ export class AppHeaderComponent { getElapsedSecs(): number { let secs = this._contestService.getElapsedSecs() ; - if (DEBUG_MODE) { +/* if (DEBUG_MODE) { console.log("AppHeaderComponent.getElapsedSecs(): returning ", secs); } - return secs; +*/ + return secs; } getRemainingSecs(): number { let secs = this._contestService.getRemainingSecs() ; - if (DEBUG_MODE) { +/* if (DEBUG_MODE) { console.log("AppHeaderComponent.getRemainingSecs(): returning ", secs); } +*/ return secs; } } From 0a316c88ff750597d2a1e426a500f0495eb22f6a Mon Sep 17 00:00:00 2001 From: John Clevenger Date: Fri, 27 Dec 2024 13:26:51 -0800 Subject: [PATCH 045/141] i1027: updated debug comments; added "todo". --- .../modules/core/abstract-services/i-websocket.service.ts | 6 +++++- .../WTI-UI/src/app/modules/core/services/contest.service.ts | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/projects/WTI-UI/src/app/modules/core/abstract-services/i-websocket.service.ts b/projects/WTI-UI/src/app/modules/core/abstract-services/i-websocket.service.ts index b0d66ea6c..4ef42bd7b 100644 --- a/projects/WTI-UI/src/app/modules/core/abstract-services/i-websocket.service.ts +++ b/projects/WTI-UI/src/app/modules/core/abstract-services/i-websocket.service.ts @@ -44,7 +44,9 @@ export abstract class IWebsocketService { } case 'contest_clock': { if (DEBUG_MODE) { - console.log ("Got '", message.type, "' websocket message in IWebsocketService.incomingMessage()"); + console.log ("IWebsocketService.incomingMessage(): got websocket message '", message.type); + console.log ("IWebsocketService.incomingMessage(): invoking ContestService.getIsContestRunning() and"); + console.log (" subscribing to callback from HTTP GET"); } this._contestService.getIsContestRunning() .subscribe((val: any) => { @@ -55,6 +57,8 @@ export abstract class IWebsocketService { } this._contestService.isContestRunning = val; + //TODO: need to force the ContestService to update the contest clock and set the ContestTimer as appropriate here. + // Possibly: subscribe somewhere to the ContestClockEvent Subject and implement a subscription callback that does this updating? this._contestService.contestClockEvent.next(); }); break; diff --git a/projects/WTI-UI/src/app/modules/core/services/contest.service.ts b/projects/WTI-UI/src/app/modules/core/services/contest.service.ts index d5b5f7820..6fa9d1c4c 100644 --- a/projects/WTI-UI/src/app/modules/core/services/contest.service.ts +++ b/projects/WTI-UI/src/app/modules/core/services/contest.service.ts @@ -47,7 +47,7 @@ export class ContestService extends IContestService { getIsContestRunning(): Observable { if (DEBUG_MODE) { - console.log ("Executing ContestService.getIsContestRunning(): calling HTTP client get(.../contest.isRunning)") ; + console.log ("ContestService.getIsContestRunning(): calling HTTP client get(.../contest.isRunning)") ; } return this._httpClient.get(`${environment.baseUrl}/contest/isRunning`); } From bc5018bc447a7b83c6381b59004962281de9381f Mon Sep 17 00:00:00 2001 From: John Clevenger Date: Fri, 27 Dec 2024 22:38:09 -0800 Subject: [PATCH 046/141] i1027: refactor updateContestClock() to be a ContestService method. --- .../abstract-services/i-contest.service.ts | 2 +- .../core/services/contest.mock.service.ts | 2 +- .../modules/core/services/contest.service.ts | 93 ++++++++++++------- .../login-page/login-page.component.ts | 40 ++------ 4 files changed, 70 insertions(+), 67 deletions(-) diff --git a/projects/WTI-UI/src/app/modules/core/abstract-services/i-contest.service.ts b/projects/WTI-UI/src/app/modules/core/abstract-services/i-contest.service.ts index e832b9571..cabb3e975 100644 --- a/projects/WTI-UI/src/app/modules/core/abstract-services/i-contest.service.ts +++ b/projects/WTI-UI/src/app/modules/core/abstract-services/i-contest.service.ts @@ -58,7 +58,7 @@ export abstract class IContestService { abstract getContestClock(): Observable; //note this refers to a "ContestClock" model object, not the "contestClockEvent" Subject above - abstract updateContestClock (newClock: ContestClock): void; //update the WTI-UI representation of the PC2 contest clock + abstract updateContestClock (): void; //update the WTI-UI representation of the PC2 contest clock abstract getElapsedSecs(): number; diff --git a/projects/WTI-UI/src/app/modules/core/services/contest.mock.service.ts b/projects/WTI-UI/src/app/modules/core/services/contest.mock.service.ts index c9d387e28..b1e1d83fa 100644 --- a/projects/WTI-UI/src/app/modules/core/services/contest.mock.service.ts +++ b/projects/WTI-UI/src/app/modules/core/services/contest.mock.service.ts @@ -93,7 +93,7 @@ export class ContestMockService extends IContestService { {isRunning:'true', contestLengthSecs:'18000', elapsedSecs:'3600', wallClockStartTime:'1734593847'}); } - updateContestClock (newContestClock: ContestClock) { + updateContestClock () { //nothing to do -- this is a mock service } diff --git a/projects/WTI-UI/src/app/modules/core/services/contest.service.ts b/projects/WTI-UI/src/app/modules/core/services/contest.service.ts index 6fa9d1c4c..32c3b81ae 100644 --- a/projects/WTI-UI/src/app/modules/core/services/contest.service.ts +++ b/projects/WTI-UI/src/app/modules/core/services/contest.service.ts @@ -47,7 +47,7 @@ export class ContestService extends IContestService { getIsContestRunning(): Observable { if (DEBUG_MODE) { - console.log ("ContestService.getIsContestRunning(): calling HTTP client get(.../contest.isRunning)") ; + console.log ("ContestService.getIsContestRunning(): invoking HTTP get(.../contest.isRunning) WTI-API endpoint") ; } return this._httpClient.get(`${environment.baseUrl}/contest/isRunning`); } @@ -86,10 +86,56 @@ export class ContestService extends IContestService { return this.standingsAreCurrent ; } - updateContestClock (newContestClock: ContestClock) { + /** This method invokes the local getContestClock() method, which makes an HTTP call to the WTI-API to get the current + * PC2 Server clock (aka "ContestTime"). It subscribes to the Observable returned by the HTTP call, + * and when the subscription callback occurs it uses the received ContestClock data (an instance of + * WTI=UI models/ContestClock) to update the WTI-UI contest clock (including the onscreen displays). + */ + updateContestClock () { + + //get the actual contest clock info from the PC2 server via the Contest Service (which gets it via the WTI Server and its PC2 API) + if (DEBUG_MODE) { + console.log(" Invoking ContestService.getContestClock(), subscribing for HTTP callback result") + } + this.getContestClock() + .subscribe( + (data: any) => { + if (!data) { + console.error ("ContestService.updateContestClock() getContestClock() subscription callback: unable to get ContestClock from PC2 API via ContestService!"); + } else { + if (DEBUG_MODE) { + console.log("ContestService.updateContestClock(): got callback from getContestClock() subscription; callback data ="); + console.log(data); + } + //install the received contest clock data into the ContestService's ContestClock + this.installNewContestClock(data); + } + }, + (error: any) => { + console.error("ContestService.updateContestClock(): getContestClock() subscription callback error: "); + console.error (error); + } + ); + } + + /** This method receives a WTI-UI ContestClock model containing new values which should be used to update the WTI-UI contest clock, + * including the onscreen displays. It constructs a new ContestClock object containing the received data and installs that + * object as the current WTI-UI clock. It then updates the separate "ContestTimer" object with the specified values, and + * if the received data indicates the clock should be running it starts the ContestTimer (which then genrates a "clock tick" + * once per second to update the clock displays). + */ + installNewContestClock(data: any) { + + //copy the data fields (received from the PC2 Server via the WTI-API) into a new ContestService ContestClock object + let newContestClock = new ContestClock(); + newContestClock.isRunning = data.running ; + newContestClock.contestLengthSecs = data.contestLengthInSecs ; + newContestClock.elapsedSecs = data.elapsedSecs ; + newContestClock.wallClockStartTime = data.wallClockStartTime ; + //save the new clock if (DEBUG_MODE) { - console.log("ContestService (id ", this.uniqueId, ").updateContestClock(): replacing Contest Clock with:"); + console.log("ContestService (id", this.uniqueId, ").installNewContestClock(): replacing Contest Clock with:"); console.log(newContestClock); } this.contestClock = newContestClock; @@ -100,18 +146,18 @@ export class ContestService extends IContestService { let contestLengthSecs = parseInt(this.contestClock.contestLengthSecs); let remainingSecs = contestLengthSecs - elapsedSecs; -/* if (DEBUG_MODE) { - console.log ("ContestService.updateContestClock(): values pulled from newContestClock:"); + if (DEBUG_MODE) { + console.log ("ContestService.installNewContestClock(): values pulled from newContestClock:"); console.log (" timerShouldBeStarted = ", timerShouldBeStarted); console.log (" elapsedSecs = ", elapsedSecs); console.log (" contestLengthSecs = ", contestLengthSecs); console.log (" remainingSecs = ", remainingSecs); } -*/ + //shut off timer if it is running (otherwise we can't update the elapsed/remaining time values) if (this.contestTimer.isTimerRunning) { if (DEBUG_MODE) { - console.log("ContestService (id ", this.uniqueId, ").updateContestClock(): stopping timer"); + console.log("ContestService (id", this.uniqueId, ").installNewContestClock(): stopping timer"); } this.contestTimer.stopTimer(); } @@ -123,42 +169,25 @@ export class ContestService extends IContestService { //restart the timer if the new contest clock values indicate it should be running if (timerShouldBeStarted) { if (DEBUG_MODE) { - console.log("ContestService (id ", this.uniqueId, ").updateContestClock(): starting timer"); + console.log("ContestService (id", this.uniqueId, ").installNewContestClock(): starting timer"); } this.contestTimer.startTimer(); } } - enableContestTimerUpdates() { - if (DEBUG_MODE) { - console.log("ContestService (id ", this.uniqueId, ").enableContestTimerUpdates(): starting timer"); - } - this.contestTimer.startTimer(); - } - - disableContestTimerUpdates() { - if (DEBUG_MODE) { - console.log("ContestService (id ", this.uniqueId, ").disableContestTimerUpdates(): stopping timer"); - } - this.contestTimer.stopTimer(); - } - + /** Returns the number of seconds which have elapsed so far in the contest, which it obtains from the separate + * ContestTimer object. + */ getElapsedSecs(): number { - let elapsedSecs = this.contestTimer.getElapsedSecs(); -/* if (DEBUG_MODE) { - console.log("ContestService (id ", this.uniqueId, ").getElapsedSecs(): returning elapsed secs from ContestTimer: ", elapsedSecs); - } -*/ + let elapsedSecs = this.contestTimer.getElapsedSecs(); return elapsedSecs ; } + /** Returns the number of seconds remaining in the contest, which it obtains from the separate + * ContestTimer object. + */ getRemainingSecs(): number { let remainingSecs = this.contestTimer.getRemainingSecs(); -/* if (DEBUG_MODE) { - console.log("ContestService (id ", this.uniqueId, ").getRemainingSecs(): returning remaining secs from ContestTimer: ", remainingSecs); - } -*/ return remainingSecs ; } - } diff --git a/projects/WTI-UI/src/app/modules/login/components/login-page/login-page.component.ts b/projects/WTI-UI/src/app/modules/login/components/login-page/login-page.component.ts index d9583754d..43cc08f41 100644 --- a/projects/WTI-UI/src/app/modules/login/components/login-page/login-page.component.ts +++ b/projects/WTI-UI/src/app/modules/login/components/login-page/login-page.component.ts @@ -111,7 +111,7 @@ export class LoginPageComponent implements OnInit, OnDestroy { this._websocketService.startWebsocket(); if (DEBUG_MODE) { - console.log (" invoking ContestService.getisContestRunning() and subscribing to the result") ; + console.log (" Invoking ContestService.getisContestRunning() and subscribing to the result") ; } this._contestService.getIsContestRunning() .subscribe((val: boolean) => { @@ -133,38 +133,12 @@ export class LoginPageComponent implements OnInit, OnDestroy { this._contestService.contestClockEvent.next(); }); - //get the actual contest clock info from the PC2 server via the Contest Service (which gets it via the WTI Server and its PC2 API) - if (DEBUG_MODE) { - console.log(" Invoking ContestService.getContestClock(), subscribing for callback result") - } - this._contestService.getContestClock() - .subscribe( - (data: any) => { - if (!data) { - console.error ("LoginPageComponent.onSubmit() ContestClock subscription callback: unable to get ContestClock from PC2 API via ContestService!"); - } else { - //copy the data fields received from the PC2 Server (via the WTI-API) into the ContestService's ContestClock object - if (DEBUG_MODE) { - console.log("LoginPageComponent.onSubmit(): got callback from ContestService.getContestClock() subscription; callback data ="); - console.log(data); - } - let newContestClock = new ContestClock(); - newContestClock.isRunning = data.running ; - newContestClock.contestLengthSecs = data.contestLengthInSecs ; - newContestClock.elapsedSecs = data.elapsedSecs ; - newContestClock.wallClockStartTime = data.wallClockStartTime ; - if (DEBUG_MODE) { - console.log("LoginPageComponent.onSubmit() ContestService.getContestClock() subscription callback: calling ContestService.updateContestClock() with new clock data) "); - } - this._contestService.updateContestClock(newContestClock); - } - }, - (error: any) => { - console.error("LoginPageComponent.onSubmit(): getContestClock() subscription callback error: "); - console.error (error); - } - ); - + //update the WTI-UI representations of the PC2 Contest Clock + if (DEBUG_MODE) { + console.log (" Invoking ContestService.updateContestClock()") ; + } + this._contestService.updateContestClock(); + }, (error: any) => { console.error ("LoginPageComponent.onSubmit(): AuthService.login() subscription callback error: "); console.error(error); From d43bfce9f1250ec2851c274772d8fb02b336ab92 Mon Sep 17 00:00:00 2001 From: John Clevenger Date: Fri, 27 Dec 2024 23:20:27 -0800 Subject: [PATCH 047/141] i1027: IWebsocketService: invoke updateContestClock() on 'clock' msg. --- .../modules/core/abstract-services/i-websocket.service.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/projects/WTI-UI/src/app/modules/core/abstract-services/i-websocket.service.ts b/projects/WTI-UI/src/app/modules/core/abstract-services/i-websocket.service.ts index 4ef42bd7b..de01c24f0 100644 --- a/projects/WTI-UI/src/app/modules/core/abstract-services/i-websocket.service.ts +++ b/projects/WTI-UI/src/app/modules/core/abstract-services/i-websocket.service.ts @@ -52,14 +52,14 @@ export abstract class IWebsocketService { .subscribe((val: any) => { if (DEBUG_MODE) { console.log ("IWebsocketService.incomingMessage(): callback from ContestService.getIsContestRunning() returned '", val, "'"); - console.log ("Setting ContestService.isContestRunning to '", val, "'") ; - console.log (" and invoking ContestService.contestClockEvent.next()") ; + console.log ("Setting ContestService.isContestRunning to '", val, "',") ; + console.log (" invoking ContestService.contestClockEvent.next()") ; + console.log (" and invoking ContestService.updateContestClock()"); } this._contestService.isContestRunning = val; - //TODO: need to force the ContestService to update the contest clock and set the ContestTimer as appropriate here. - // Possibly: subscribe somewhere to the ContestClockEvent Subject and implement a subscription callback that does this updating? this._contestService.contestClockEvent.next(); + this._contestService.updateContestClock(); }); break; } From f17264424c6a924033c46ef4ff2e11397eb636fa Mon Sep 17 00:00:00 2001 From: John Clevenger Date: Fri, 27 Dec 2024 23:21:04 -0800 Subject: [PATCH 048/141] i1027: LoginPageComponent: add comments (no code changes). --- .../modules/login/components/login-page/login-page.component.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/projects/WTI-UI/src/app/modules/login/components/login-page/login-page.component.ts b/projects/WTI-UI/src/app/modules/login/components/login-page/login-page.component.ts index 43cc08f41..717531ae9 100644 --- a/projects/WTI-UI/src/app/modules/login/components/login-page/login-page.component.ts +++ b/projects/WTI-UI/src/app/modules/login/components/login-page/login-page.component.ts @@ -130,6 +130,7 @@ export class LoginPageComponent implements OnInit, OnDestroy { if (DEBUG_MODE) { console.log ("Invoking ContestService.contestClockEvent.next()") ; } + //trigger a contestClockEvent so the SelectProblems dropdown can decide whether to display the problems or not this._contestService.contestClockEvent.next(); }); From 85529fc848509e09469df997ea3d108937266916 Mon Sep 17 00:00:00 2001 From: John Clevenger Date: Sat, 28 Dec 2024 13:09:32 -0800 Subject: [PATCH 049/141] i127: AppHeaderComponent: remove debugging display of raw seconds. --- .../shared/components/app-header/app-header.component.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/projects/WTI-UI/src/app/modules/shared/components/app-header/app-header.component.html b/projects/WTI-UI/src/app/modules/shared/components/app-header/app-header.component.html index 4f3354b3a..d6e482470 100644 --- a/projects/WTI-UI/src/app/modules/shared/components/app-header/app-header.component.html +++ b/projects/WTI-UI/src/app/modules/shared/components/app-header/app-header.component.html @@ -7,8 +7,8 @@
Elapsed:
- {{ getElapsedSecs() }}
- + {{ getElapsedSecs() | displayTime }}
From 4cee5bd677970199f17defc48906eb61bd7ed262 Mon Sep 17 00:00:00 2001 From: John Clevenger Date: Sat, 28 Dec 2024 13:33:12 -0800 Subject: [PATCH 050/141] i1027: WTI-API.ContestController: fix copy/paste typo in response. --- projects/WTI-API/src/main/controllers/ContestController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/projects/WTI-API/src/main/controllers/ContestController.java b/projects/WTI-API/src/main/controllers/ContestController.java index a5de70b5d..bd788b249 100644 --- a/projects/WTI-API/src/main/controllers/ContestController.java +++ b/projects/WTI-API/src/main/controllers/ContestController.java @@ -479,7 +479,7 @@ public Response contestClock(@ApiParam(value="token used by logged in users to a catch(NullPointerException e) { logger.severe(e.getMessage()); return Response.status(Response.Status.INTERNAL_SERVER_ERROR) - .entity(new ServerErrorResponseModel(Response.Status.INTERNAL_SERVER_ERROR, "NullPointerException in ContestController.clarifications()")) + .entity(new ServerErrorResponseModel(Response.Status.INTERNAL_SERVER_ERROR, "NullPointerException in ContestController.contestClock()")) .type(MediaType.APPLICATION_JSON).build(); } From 01f2fa3f49294440c3cd6f38267ac667b5c833d7 Mon Sep 17 00:00:00 2001 From: John Clevenger Date: Sat, 28 Dec 2024 13:37:51 -0800 Subject: [PATCH 051/141] i1027: ContestTimer: set default remaining time to zero. Note that... ...all logic paths SHOULD override this by getting the ACTUAL remaining time from the PC2 server and updating the timer accordingly. --- .../src/app/modules/core/services/contestTimer.service.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/projects/WTI-UI/src/app/modules/core/services/contestTimer.service.ts b/projects/WTI-UI/src/app/modules/core/services/contestTimer.service.ts index 2ae8db0aa..39a0b053b 100644 --- a/projects/WTI-UI/src/app/modules/core/services/contestTimer.service.ts +++ b/projects/WTI-UI/src/app/modules/core/services/contestTimer.service.ts @@ -2,8 +2,8 @@ import { DEBUG_MODE } from 'src/constants' export class ContestTimerService { - elapsedSecs = 0 ; //how many seconds the contest has been running (doesn't include any 'paused' time) - remainingSecs = 18000 ; //how many seconds remain in the contest (default initial value: 5 hours = 18,000 secs) + elapsedSecs = 0 ; //how many seconds the contest has been running (doesn't include any 'paused' time) + remainingSecs = 0 ; //how many seconds remain in the contest (default default=0 until specified otherwise) intervalId: ReturnType ; //the id of the JavaScript "interval" used to generate timer ticks isTimerRunning: boolean = false; From 8913a0529eb805106a4d75f37df9a117189ee018 Mon Sep 17 00:00:00 2001 From: John Clevenger Date: Wed, 1 Jan 2025 14:08:16 -0800 Subject: [PATCH 052/141] i1027: added comments (no code changes). --- projects/WTI-API/src/main/models/ContestClockModel.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/projects/WTI-API/src/main/models/ContestClockModel.java b/projects/WTI-API/src/main/models/ContestClockModel.java index 260c1c9f5..41211e35e 100644 --- a/projects/WTI-API/src/main/models/ContestClockModel.java +++ b/projects/WTI-API/src/main/models/ContestClockModel.java @@ -14,7 +14,8 @@ public class ContestClockModel { private boolean isRunning; private long contestLengthInSecs; private long elapsedSecs; //total time in seconds that the contest has been running -- does NOT include time during any "pauses" - private long wallClockStartTime; //unix timestamp when the contest actually started -- msec since the Epoch. Does not change due to "pauses" + private long wallClockStartTime; //unix timestamp when the contest actually started (msec since the Epoch), + // or zero if contest has not ever been started. Does not change due to "pauses". public ContestClockModel(boolean isRunning, long contestLengthInSecs, long elapsedSecs, long wallClockStartTime) { this.isRunning = isRunning; From 90716840c3d5f4658b07ed0f0585d0820dd7b5ad Mon Sep 17 00:00:00 2001 From: John Clevenger Date: Wed, 1 Jan 2025 14:12:53 -0800 Subject: [PATCH 053/141] i1027: added toString() method. This is important because... ...the class already defines a hashCode() method, and that hashCode() method references the local variable "elementId" -- which is NULL! The result is that anyone who called toString() (expecting to be able to use the toString() inherited from Object) was getting a NullPointerException (because Object's toString() invokes the hashCode() method, which attempts to dereference elementId). A separate question is "why does ContestTimeImplementation have a null elementId?" I'll file a separate issue for that.. --- .../implementation/ContestTimeImplementation.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/edu/csus/ecs/pc2/api/implementation/ContestTimeImplementation.java b/src/edu/csus/ecs/pc2/api/implementation/ContestTimeImplementation.java index 46d3c09de..79ca58038 100644 --- a/src/edu/csus/ecs/pc2/api/implementation/ContestTimeImplementation.java +++ b/src/edu/csus/ecs/pc2/api/implementation/ContestTimeImplementation.java @@ -70,4 +70,16 @@ public int hashCode() { return elementId.toString().hashCode(); } + @Override + public String toString() { + String retStr = "ContestTimeImplementation (aka 'ContestClockImplementation'): ["; + retStr += "elementId:" + this.elementId + ";"; + retStr += "contestLengthSecs:" + contestTime.getContestLengthSecs() + ";"; + retStr += "elapsedSecs:" + contestTime.getElapsedSecs() + ";"; + retStr += "remainingSecs:" + contestTime.getRemainingSecs(); + retStr += "]"; + return retStr ; + + } + } From 6cc9d40eaa2c797a439de9921facdff5c36eb2ec Mon Sep 17 00:00:00 2001 From: John Clevenger Date: Wed, 1 Jan 2025 20:08:51 -0800 Subject: [PATCH 054/141] i1027: handle wallClockStartTime when contest hasn't been started; also, added error logging to support debugging. --- .../main/controllers/ContestController.java | 31 +++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/projects/WTI-API/src/main/controllers/ContestController.java b/projects/WTI-API/src/main/controllers/ContestController.java index bd788b249..25e968f85 100644 --- a/projects/WTI-API/src/main/controllers/ContestController.java +++ b/projects/WTI-API/src/main/controllers/ContestController.java @@ -5,6 +5,7 @@ import java.net.URISyntaxException; import java.util.ArrayList; import java.util.Arrays; +import java.util.Calendar; import java.util.HashMap; import java.util.List; import java.util.Properties; @@ -25,6 +26,7 @@ import config.ServerInit; import edu.csus.ecs.pc2.api.IClarification; import edu.csus.ecs.pc2.api.IClient; +import edu.csus.ecs.pc2.api.IContest; import edu.csus.ecs.pc2.api.IContestClock; import edu.csus.ecs.pc2.api.IJudgement; import edu.csus.ecs.pc2.api.ILanguage; @@ -440,6 +442,11 @@ public Response isRunning( public Response contestClock(@ApiParam(value="token used by logged in users to access teams information", required = true) @HeaderParam("team_id")String key) { + if (connections == null) { + System.err.println ("SEVERE: ContestController.contestClock(): team connections map in MainController is null!"); + logger.severe("ContestController.contestClock(): team connections map in MainController is null!"); + throw new NullPointerException("Team connections map in MainController is null in ContestController.contestClock()"); + } ServerConnection userInformation = connections.get(key); //verify the user is logged in @@ -459,13 +466,33 @@ public Response contestClock(@ApiParam(value="token used by logged in users to a try { //get the contest clock from the PC2 Server via the PC2 API ServerConnection - IContestClock contestClock = userInformation.getContest().getContestClock(); +// IContestClock contestClock = userInformation.getContest().getContestClock(); + IContest contest = userInformation.getContest(); + if (contest == null) { + System.err.println ("SEVERE: ContestController.contestClock(): ServerConnection.getContest() returned null!"); + logger.severe("ContestController.contestClock(): ServerConnection.getContest() returned null!"); + throw new NullPointerException("ServerConnection.getContest() returned null in ContestController.contestClock()"); + } + IContestClock contestClock = contest.getContestClock(); + if (contestClock == null) { + System.err.println ("SEVERE: ContestController.contestClock(): ServerConnection.getContest().getContestClock() returned null!"); + logger.severe("ContestController.contestClock(): ServerConnection.getContest().getContestClock() returned null!"); + throw new NullPointerException("ServerConnection.getContest().getContestClock() returned null in ContestController.contestClock()"); + } + //retrieve the relevant fields from the PC2 contest clock boolean isRunning = contestClock.isContestClockRunning(); long contestLengthInSecs = contestClock.getContestLengthSecs(); long elapsedSecs = contestClock.getElapsedSecs(); - long wallClockStartTime = contestClock.getContestStartTime().getTimeInMillis(); + long wallClockStartTime ; + //"contest start time" is a Calendar object and might be null if the contest has never been started + Calendar startTimeCalendar = contestClock.getContestStartTime(); + if (startTimeCalendar == null) { + wallClockStartTime = 0; + } else { + wallClockStartTime = startTimeCalendar.getTimeInMillis(); + } //construct a ContestClock containing the PC2 Server clock values returnableContestClock = new ContestClockModel(isRunning,contestLengthInSecs, elapsedSecs, wallClockStartTime); From 458548ca07795d29a74ba7987827cfd74b7c5e0e Mon Sep 17 00:00:00 2001 From: John Clevenger Date: Thu, 2 Jan 2025 16:39:00 -0800 Subject: [PATCH 055/141] i1027: update contest clocks on F5 refresh/reload. --- projects/WTI-UI/src/app/app.component.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/projects/WTI-UI/src/app/app.component.ts b/projects/WTI-UI/src/app/app.component.ts index 774cc5dcd..63d51d137 100644 --- a/projects/WTI-UI/src/app/app.component.ts +++ b/projects/WTI-UI/src/app/app.component.ts @@ -180,6 +180,9 @@ export class AppComponent implements OnInit { } } + //restore the contest clock on-screen display values (elapsed and remaining) + this._contestService.updateContestClock(); + // transfer to the (former) "current page". let page = getCurrentPage(); if (DEBUG_MODE) { From 983ff8d26838cde5c7e50e976e3779063fba3c7f Mon Sep 17 00:00:00 2001 From: johnc Date: Sat, 4 Jan 2025 16:58:30 -0800 Subject: [PATCH 056/141] i1027, i1029: update "contest is running" req for viewing clars & probs: This commit removes the requirement that the contest be running in order for the WTI-API /clarifications endpoint to return clars; now the only requirement for returning clars is that the client "is-logged-in". The commit also changes the requirement in the /problems endpoint: previously the contest had to be RUNNING in order to see problems; now, it just has to have been STARTED (i.e., there must have been greater than zero "elapsed time". This allows teams to see problems during a pause. Note that this commit is part of a PR for https://github.com/pc2ccs/pc2v9/issues/1027, but it solves https://github.com/pc2ccs/pc2v9/issues/1029 as well. --- .../src/main/controllers/ContestController.java | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/projects/WTI-API/src/main/controllers/ContestController.java b/projects/WTI-API/src/main/controllers/ContestController.java index 25e968f85..9d685c969 100644 --- a/projects/WTI-API/src/main/controllers/ContestController.java +++ b/projects/WTI-API/src/main/controllers/ContestController.java @@ -329,8 +329,9 @@ public Response problems( if (userInformation == null) { throw new NotLoggedInException(); } else { - // make sure that the contest is running (problems are not allowed to be seen when the contest is not running) - if (!userInformation.getContest().isContestClockRunning()) { + // make sure that the contest has been started (problems are not allowed to be seen before the contest starts -- + // i.e., before the contest clock has started accumulating elapsed time) + if (!(userInformation.getContest().getContestClock().getElapsedSecs()>0)) { return Response.status(Response.Status.UNAUTHORIZED).entity( new ServerErrorResponseModel(Response.Status.UNAUTHORIZED, "Unauthorized user request")) .type(MediaType.APPLICATION_JSON).build(); @@ -549,13 +550,6 @@ public Response clarifications(@ApiParam(value="token used by logged in users to // make sure we have connection information for this user (i.e. that the user is logged in) if (userInformation == null) { throw new NotLoggedInException(); - } else { - // make sure that the contest is running (problems are not allowed to be seen when the contest is not running) - if (!userInformation.getContest().isContestClockRunning()) { - return Response.status(Response.Status.UNAUTHORIZED).entity( - new ServerErrorResponseModel(Response.Status.UNAUTHORIZED, "Unauthorized user request")) - .type(MediaType.APPLICATION_JSON).build(); - } } } catch (NotLoggedInException e1) { return Response.status(Response.Status.UNAUTHORIZED) From b069333efd1ab3fed731ce81f57093e9c763c711 Mon Sep 17 00:00:00 2001 From: johnc Date: Sat, 4 Jan 2025 17:43:06 -0800 Subject: [PATCH 057/141] i1027: add code to resync UI clocks with PC2 periodically. --- .../app/modules/core/services/contest.service.ts | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/projects/WTI-UI/src/app/modules/core/services/contest.service.ts b/projects/WTI-UI/src/app/modules/core/services/contest.service.ts index 32c3b81ae..75e0e7ef3 100644 --- a/projects/WTI-UI/src/app/modules/core/services/contest.service.ts +++ b/projects/WTI-UI/src/app/modules/core/services/contest.service.ts @@ -9,6 +9,7 @@ import { ContestClock } from '../models/contest-clock'; import { ContestTimerService } from './contestTimer.service' ; import { Clarification } from '../models/clarification'; import { DEBUG_MODE } from 'src/constants'; +import { RESYNC_INTERVAL_IN_MINUTES } from 'src/constants'; @Injectable({ providedIn: 'root' //forces the service to be a singleton across all app components ('root' == "root injector") @@ -18,7 +19,7 @@ export class ContestService extends IContestService { standingsAreCurrent: boolean ; cachedStandings: Observable ; - //the WTI-UI timer service which updates elapsed and remaining time when started + //the WTI-UI timer service which updates on-screen elapsed and remaining time when started (enabled) contestTimer: ContestTimerService = new ContestTimerService() ; constructor(private _httpClient: HttpClient) { @@ -27,6 +28,18 @@ export class ContestService extends IContestService { console.log ("Executing ContestService constructor; instance ID = ", this.uniqueId) ; } this.standingsAreCurrent = false; + + //set a timer to auto-refresh the contest clock displays, at a rate defined in src/constants + setInterval( + //execute this function at the following-specified interval: + () => { + if (DEBUG_MODE) { + console.log("ContestService: resyncing clocks with PC2 Server"); + } + this.updateContestClock(); + }, + RESYNC_INTERVAL_IN_MINUTES * 60 * 1000 // timer interval in msec: minutes * (secs-per-min) * (msec-per-sec) + ); } getLanguages(): Observable { From d8d0c7c61eb85461fa03990014592482cf3c88fb Mon Sep 17 00:00:00 2001 From: johnc Date: Sat, 4 Jan 2025 17:45:19 -0800 Subject: [PATCH 058/141] i1027: constants.ts: set UI clock resync interval to every 15 mins. --- projects/WTI-UI/src/constants.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/projects/WTI-UI/src/constants.ts b/projects/WTI-UI/src/constants.ts index 2e055ede0..86f936706 100644 --- a/projects/WTI-UI/src/constants.ts +++ b/projects/WTI-UI/src/constants.ts @@ -7,6 +7,11 @@ */ export let DEBUG_MODE = true ; +/** + * Interval in minutes at which the WTI-UI will resync its clock displays with PC2 + */ + export const RESYNC_INTERVAL_IN_MINUTES = 15 ; + /** * The key under which the currently-active WTI page is stored in sessionStorage. */ From db34af4d78982263346b28485cc8b058bb83130a Mon Sep 17 00:00:00 2001 From: johnc Date: Sat, 4 Jan 2025 18:10:38 -0800 Subject: [PATCH 059/141] i1027: source code cleanup (remove outdated comments, etc.) --- projects/WTI-UI/src/app/app.component.ts | 1 - .../app/modules/core/abstract-services/i-contest.service.ts | 1 - projects/WTI-UI/src/app/modules/core/core.module.ts | 3 --- .../WTI-UI/src/app/modules/core/services/contest.service.ts | 1 - 4 files changed, 6 deletions(-) diff --git a/projects/WTI-UI/src/app/app.component.ts b/projects/WTI-UI/src/app/app.component.ts index 63d51d137..f4f8ac45b 100644 --- a/projects/WTI-UI/src/app/app.component.ts +++ b/projects/WTI-UI/src/app/app.component.ts @@ -190,7 +190,6 @@ export class AppComponent implements OnInit { } //navigate to the most recently saved page - // TODO: consider whether using history.pushState()/popState() is a better solution for this... this.router.navigate([page]) .then(nav => { if (DEBUG_MODE) { diff --git a/projects/WTI-UI/src/app/modules/core/abstract-services/i-contest.service.ts b/projects/WTI-UI/src/app/modules/core/abstract-services/i-contest.service.ts index cabb3e975..a33557b61 100644 --- a/projects/WTI-UI/src/app/modules/core/abstract-services/i-contest.service.ts +++ b/projects/WTI-UI/src/app/modules/core/abstract-services/i-contest.service.ts @@ -31,7 +31,6 @@ export abstract class IContestService { isContestRunning = false; //the WTI-UI representation of the PC2 Server's ContestClock (PC2 class ContestTime) - //todo: where does this get updated when the PC2 clock changes? ; contestClock: ContestClock = new ContestClock(); //give each instance of IContestService a unique ID for debugging purposes diff --git a/projects/WTI-UI/src/app/modules/core/core.module.ts b/projects/WTI-UI/src/app/modules/core/core.module.ts index 731dde7be..ef67073b5 100644 --- a/projects/WTI-UI/src/app/modules/core/core.module.ts +++ b/projects/WTI-UI/src/app/modules/core/core.module.ts @@ -93,9 +93,6 @@ export function WebsocketServiceFactory(injector: Injector, DisplayTimePipe, //TODO: should the following two still be declared here since they are now listed in the above "deps" list? UiHelperService, - ContestService, - //TODO: should the following two still be declared here since they are now listed in the above "deps" list? - UiHelperService, ContestService ], imports: [ diff --git a/projects/WTI-UI/src/app/modules/core/services/contest.service.ts b/projects/WTI-UI/src/app/modules/core/services/contest.service.ts index 75e0e7ef3..51f4611b7 100644 --- a/projects/WTI-UI/src/app/modules/core/services/contest.service.ts +++ b/projects/WTI-UI/src/app/modules/core/services/contest.service.ts @@ -67,7 +67,6 @@ export class ContestService extends IContestService { /** This method returns an Observable "ContestClock" object -- a WTI-UI model corresponding to the PC2 "ContestTime" class, * which itself encapsulates the "contest clock" on the PC2 server. - * TODO: should this method return the local copy of the ContestClock? */ getContestClock(): Observable { return this._httpClient.get(`${environment.baseUrl}/contest/contestclock`); From 50aceab9df87df56e0096742e41e3ae643becb7f Mon Sep 17 00:00:00 2001 From: johnc Date: Sat, 4 Jan 2025 23:12:56 -0800 Subject: [PATCH 060/141] i1027: added documentation. No code changes. --- projects/WTI-UI/src/app/app-routing.module.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/projects/WTI-UI/src/app/app-routing.module.ts b/projects/WTI-UI/src/app/app-routing.module.ts index 0c781a8f8..87992d380 100644 --- a/projects/WTI-UI/src/app/app-routing.module.ts +++ b/projects/WTI-UI/src/app/app-routing.module.ts @@ -8,6 +8,17 @@ import { LogoutComponent } from './modules/login/components/logout/logout.compon import { ClarificationsPageComponent } from './modules/clarifications/components/clarifications-page/clarifications-page.component'; import { ScoreboardPageComponent } from './modules/scoreboard/components/scoreboard-page/scoreboard-page.component'; +/** + * This class defines the "routes" which the Angular application (specifically, the Angular Router) knows how to follow. + * Note that ORDER MATTERS; the Router searches the 'Routes' array from the beginning to find a route which matches the + * path it has been told to attempt to follow. For this reason, the "login" page will always be the default route. + * Note that remaining routes are protected by a "canActivate: [AuthGuard]" clause; the effect of this is that if the Router + * decides to attempt to follow that route, it first invokes the "AuthGuard" class, which determines whether following that + * route is 'allowed'. (The criterion defined in "AuthGuard" is that the user is logged in.) + * If the route specified by the user does not match any other route defined here, the Router defaults to the LAST route, + * the one with a "path" of "**". The effect of this is that any attempt by the user to enter any undefined route in the + * browser will cause a transfer to the "/runs" page. + */ const routes: Routes = [ { path: 'login', From 5702c96be0326df8f1d94d7cd15436bba61aa14c Mon Sep 17 00:00:00 2001 From: johnc Date: Sun, 5 Jan 2025 18:38:56 -0800 Subject: [PATCH 061/141] i1027: use "start time" not elapsed secs to determine "started"; also: added additional exception handling and updated HTTP error response messages. --- .../main/controllers/ContestController.java | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/projects/WTI-API/src/main/controllers/ContestController.java b/projects/WTI-API/src/main/controllers/ContestController.java index 9d685c969..77580bccf 100644 --- a/projects/WTI-API/src/main/controllers/ContestController.java +++ b/projects/WTI-API/src/main/controllers/ContestController.java @@ -329,17 +329,23 @@ public Response problems( if (userInformation == null) { throw new NotLoggedInException(); } else { - // make sure that the contest has been started (problems are not allowed to be seen before the contest starts -- - // i.e., before the contest clock has started accumulating elapsed time) - if (!(userInformation.getContest().getContestClock().getElapsedSecs()>0)) { + // make sure that the contest has been started (problems are not allowed to be seen before the contest starts) + // Note that starting the contest results in setting the "contest start time" in the ContestClock object to + // the Unix Epoch time at which the contest was started; prior to starting the contest that value will be null. + if (userInformation.getContest().getContestClock().getContestStartTime()==null) { return Response.status(Response.Status.UNAUTHORIZED).entity( - new ServerErrorResponseModel(Response.Status.UNAUTHORIZED, "Unauthorized user request")) + new ServerErrorResponseModel(Response.Status.UNAUTHORIZED, "Unauthorized user request - contest has not started.")) .type(MediaType.APPLICATION_JSON).build(); } } } catch (NotLoggedInException e1) { return Response.status(Response.Status.UNAUTHORIZED) - .entity(new ServerErrorResponseModel(Response.Status.UNAUTHORIZED, "Unauthorized user request")) + .entity(new ServerErrorResponseModel(Response.Status.UNAUTHORIZED, "Unauthorized user request -- not logged in.")) + .type(MediaType.APPLICATION_JSON).build(); + } catch (Exception e2) { + logger.severe("Exception in ContestController /problems endppoint: " + e2.getMessage()); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity(new ServerErrorResponseModel(Response.Status.INTERNAL_SERVER_ERROR, "Exception in ContestController /problems endppoint: " + e2.getMessage())) .type(MediaType.APPLICATION_JSON).build(); } @@ -553,7 +559,7 @@ public Response clarifications(@ApiParam(value="token used by logged in users to } } catch (NotLoggedInException e1) { return Response.status(Response.Status.UNAUTHORIZED) - .entity(new ServerErrorResponseModel(Response.Status.UNAUTHORIZED, "Unauthorized user request")) + .entity(new ServerErrorResponseModel(Response.Status.UNAUTHORIZED, "Unauthorized user request - not logged in.")) .type(MediaType.APPLICATION_JSON).build(); } From b92c51cf12a69ef2437199588ea8b639d1856742 Mon Sep 17 00:00:00 2001 From: johnc Date: Tue, 7 Jan 2025 12:17:54 -0800 Subject: [PATCH 062/141] i1027: allow problem display if contest started (not just if running). --- .../problem-selector.component.ts | 207 +++++++++++------- 1 file changed, 131 insertions(+), 76 deletions(-) diff --git a/projects/WTI-UI/src/app/modules/shared/components/problem-selector/problem-selector.component.ts b/projects/WTI-UI/src/app/modules/shared/components/problem-selector/problem-selector.component.ts index 910b85650..fefda144d 100644 --- a/projects/WTI-UI/src/app/modules/shared/components/problem-selector/problem-selector.component.ts +++ b/projects/WTI-UI/src/app/modules/shared/components/problem-selector/problem-selector.component.ts @@ -2,43 +2,54 @@ import { Component, forwardRef, OnInit, OnDestroy, Input } from '@angular/core'; import { NG_VALUE_ACCESSOR, ControlValueAccessor } from '@angular/forms'; import { ContestProblem } from 'src/app/modules/core/models/contest-problem'; import { IContestService } from 'src/app/modules/core/abstract-services/i-contest.service'; +import { ContestClock } from 'src/app/modules/core/models/contest-clock'; import { Subject } from 'rxjs'; import { takeUntil } from 'rxjs/operators'; import { DEBUG_MODE } from 'src/constants'; +/** + * This class defines a component which displays a drop-down list containing the current contest problems. + * Each time the component is initialized (that is, on each rendering during which its "ngOnInit(" method is invoked) + * it invokes its "loadProblems()" method, which examines the current contest clock to determine + * if the contest has been started. If so, it fetches the current problem list from the server + * and displays it in the dropdown list; if the contest has not been started it sets the dropdown list to be empty. + * Method loadProblems() is also invoked any time there is a change in the contest clock state (such as stopping + * or restarting the contest clock), and the same process happens (the list is repopulated if the contest has been + * started, otherwise it is set to empty). + */ @Component({ - selector: 'app-problem-selector', - templateUrl: './problem-selector.component.html', - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => ProblemSelectorComponent), - multi: true - } - ] +selector: 'app-problem-selector', +templateUrl: './problem-selector.component.html', +providers: [ +{ +provide: NG_VALUE_ACCESSOR, +useExisting: forwardRef(() => ProblemSelectorComponent), +multi: true +} +] }) export class ProblemSelectorComponent implements OnInit, OnDestroy, ControlValueAccessor { - private _unsubscribe = new Subject(); - @Input() allowGeneral = false; - problems: ContestProblem[] = []; - value: string; - onChange = (event: any) => { }; - onTouched = (event: any) => { }; + private _unsubscribe = new Subject(); + @Input() allowGeneral = false; + problems: ContestProblem[] = []; + value: string; + onChange = (event: any) => { }; + onTouched = (event: any) => { }; - constructor(private _contestService: IContestService) { - if (DEBUG_MODE) { - console.log ("Executing ProblemSelectorComponent constructor; IContestService id = ", this._contestService.uniqueId) ; + constructor(private _contestService: IContestService) { + if (DEBUG_MODE) { + console.log ("Executing ProblemSelectorComponent constructor; IContestService id = ", this._contestService.uniqueId) ; + } } - } - ngOnInit(): void { - if (DEBUG_MODE) { - console.log ("Executing ProblemSelectorComponent.ngOnInit()") ; - } - this.loadProblems(); - if (this.allowGeneral) { - this.writeValue('general'); - } + ngOnInit(): void { + if (DEBUG_MODE) { + console.log ("Executing ProblemSelectorComponent.ngOnInit()") ; + } + this.loadProblems(); + if (this.allowGeneral) { + this.writeValue('general'); + } //Listen for (subscribe to) contest start/stop events to show/hide contest problems. //The contestClockEvent "Subject" (defined in IContestService) is used as a toggle to indicate when @@ -51,58 +62,102 @@ export class ProblemSelectorComponent implements OnInit, OnDestroy, ControlValue //PC2 contest clock. This causes the Component to execute its "loadProblems()" method; that method in turn checks //whether the contest is currently RUNNING; if so, it loads the problem names; if not, it blanks out problem names. - this._contestService.contestClockEvent - .pipe(takeUntil(this._unsubscribe)) - .subscribe(_ => this.loadProblems()); - } - - ngOnDestroy(): void { - this._unsubscribe.next(); - this._unsubscribe.complete(); - } + this._contestService.contestClockEvent + .pipe(takeUntil(this._unsubscribe)) + .subscribe(_ => this.loadProblems()); + } - registerOnChange(fn: (event: any) => void) { - this.onChange = fn; - } + ngOnDestroy(): void { + this._unsubscribe.next(); + this._unsubscribe.complete(); + } - registerOnTouched(fn: (event: any) => void) { - this.onTouched = fn; - } + registerOnChange(fn: (event: any) => void) { + this.onChange = fn; + } - writeValue(value: string) { - this.value = (value === null) ? undefined : value; - } + registerOnTouched(fn: (event: any) => void) { + this.onTouched = fn; + } - private loadProblems(): void { - if (DEBUG_MODE) { - console.log ("Executing ProblemSelectorComponent.loadProblems()") ; - } - if (this._contestService.isContestRunning) { - if (DEBUG_MODE) { - console.log ("ProblemSelectorComponent.loadProblems(): ContestService.isContestRunning() returned positive Truthy value") ; - } - this._contestService.getProblems() - .pipe(takeUntil(this._unsubscribe)) - .subscribe((data: ContestProblem[]) => { - if (DEBUG_MODE) { - console.log ("ProblemSelectorComponent.loadProblems(): subscription callback from ContestService.getProblems() returned data:"); - console.log (data) ; - } - this.problems = data; - }, (error: any) => { - if (DEBUG_MODE) { - console.log ("ProblemSelectorComponent.loadProblems(): subscription callback from ContestService.getProblems() returned error"); - console.log (" Setting contest problem list to empty array." ) ; - } - this.problems = []; - }); - } else { - if (DEBUG_MODE) { - console.log ("ProblemSelectorComponent.loadProblems(): ContestService.isContestRunning() returned negative Truthy value") ; - console.log (" Setting contest problem list to empty array." ) ; + writeValue(value: string) { + this.value = (value === null) ? undefined : value; + } - } - this.problems = []; - } - } + /** + * This method determines whether the contest problems should be displayed in a "ProblemSelectorComponent". + * Problems are only allowed to be displayed when the contest has been started, which is determined by looking + * at the "wallClockStartTime" value in the current ContestClock. The method fetches the current clock from the WTI-API server, + * and if wallClockStartTime is greater than zero it fetches the current problem list from the server and displays it in the + * component; otherwise it sets the list of displayed problems to an empty list (array). + */ + private loadProblems(): void { + if (DEBUG_MODE) { + console.log ("Executing ProblemSelectorComponent.loadProblems()") ; + } + //get the contest clock to see whether problems should be loaded/displayed + this._contestService.getContestClock() + .subscribe( + (contestClock: ContestClock) => { + if (!contestClock) { + console.error ("ProblemSelectorComponent.loadProblems(): ContestService.getContestClock() subscription callback: unable to get ContestClock from PC2 API via ContestService!"); + } else { + if (DEBUG_MODE) { + console.log("ProblemSelectorComponent.loadProblems(): got callback from ContestService.getContestClock() subscription; callback data ="); + console.log(contestClock); + } + //we got back a contest clock; check if the contest has been started + if (this.contestHasBeenStarted(contestClock)) { + //yes, contest has been started; get the problems and display them + this._contestService.getProblems() + .subscribe( + (data: ContestProblem[]) => { + if (DEBUG_MODE) { + console.log ("ProblemSelectorComponent.loadProblems(): subscription callback from ContestService.getProblems() returned data:"); + console.log (data) ; + } + this.problems = data; + }, + (error: any) => { + if (DEBUG_MODE) { + console.log ("ProblemSelectorComponent.loadProblems(): subscription callback from ContestService.getProblems() returned error"); + console.log (" Setting contest problem list to empty array." ) ; + } + console.warn("ProblemSelectorComponent.loadProblems(): error getting contest problems:"); + console.warn(error); + this.problems = []; + } + ); + } else { + if (DEBUG_MODE) { + console.log ("ProblemSelectorComponent.loadProblems(): contest has not been started; setting contest problem list to empty array.") ; + console.log (" Setting contest problem list to empty array." ) ; + } + this.problems = []; + } + + } + }, + (error: any) => { + console.error("ProblemSelectorComponent.loadProblems(): ContestService.getContestClock() subscription callback error: "); + console.error (error); + } + ); + } //end loadProblems() + + /** + * Returns true if the wallClockStartTime in the specified ContestClock is greater than zero (in other words, + * if the contest has been started); false otherwise. + * (Note that wallClockStartTime is the Unix Epoch time at which the contest was first started (if it has been started); + * otherwise (i.e. if the contest has never been started), wallClockStartTime is null in PC2 ContestTime objects + * and such a null value is converted to zero by the WTI-API "/contestclock" endpoint.) + * + */ + private contestHasBeenStarted (contestClock: ContestClock) : boolean { + if (parseInt(contestClock.wallClockStartTime) > 0) { + return true; + } else { + return false; + } + } } From f7ca705aec42ac3772c955b667abf660c7969d16 Mon Sep 17 00:00:00 2001 From: johnc Date: Tue, 7 Jan 2025 12:19:32 -0800 Subject: [PATCH 063/141] i1027: pass ContestService to ContestTimer constructor; add "TODO" note. --- .../WTI-UI/src/app/modules/core/services/contest.service.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/projects/WTI-UI/src/app/modules/core/services/contest.service.ts b/projects/WTI-UI/src/app/modules/core/services/contest.service.ts index 51f4611b7..2bdee3fa6 100644 --- a/projects/WTI-UI/src/app/modules/core/services/contest.service.ts +++ b/projects/WTI-UI/src/app/modules/core/services/contest.service.ts @@ -20,7 +20,7 @@ export class ContestService extends IContestService { cachedStandings: Observable ; //the WTI-UI timer service which updates on-screen elapsed and remaining time when started (enabled) - contestTimer: ContestTimerService = new ContestTimerService() ; + contestTimer: ContestTimerService = new ContestTimerService(this) ; constructor(private _httpClient: HttpClient) { super(); @@ -30,6 +30,8 @@ export class ContestService extends IContestService { this.standingsAreCurrent = false; //set a timer to auto-refresh the contest clock displays, at a rate defined in src/constants + //TODO: what happens to the timing of this update when the browser is minimized (because setInterval() runs slower when + // minimized)? Need to track "most recent update"?? setInterval( //execute this function at the following-specified interval: () => { From a9273b0d13f571037c6ce5292842bbeb895fe5cd Mon Sep 17 00:00:00 2001 From: johnc Date: Tue, 7 Jan 2025 12:24:12 -0800 Subject: [PATCH 064/141] i1027: Add code to handle time elapsed while browser is minimized; also: added documentation --- .../core/services/contestTimer.service.ts | 69 ++++++++++++++++--- 1 file changed, 60 insertions(+), 9 deletions(-) diff --git a/projects/WTI-UI/src/app/modules/core/services/contestTimer.service.ts b/projects/WTI-UI/src/app/modules/core/services/contestTimer.service.ts index 39a0b053b..7dce5860a 100644 --- a/projects/WTI-UI/src/app/modules/core/services/contestTimer.service.ts +++ b/projects/WTI-UI/src/app/modules/core/services/contestTimer.service.ts @@ -1,5 +1,21 @@ +import { IContestService } from 'src/app/modules/core/abstract-services/i-contest.service'; import { DEBUG_MODE } from 'src/constants' +/** + * This class defines a Timer which updates at a (nominal) rate of once per second. The Timer maintains + * two values: elapsed time and remaining time; these values default to initial values of zero but their + * values are intended to be initialized via external calls to methods setElapsedSec() and setRemainingSecs(). + * Each time the Timer "ticks" these values are updated (incremented, for elapsed time, and decremented, + * for remaining time). + * The Timer uses a JavaScript "setInterval()" to generate update events at the nominal once-per-second + * rate. Since setInterval() is documented to not be guaranteed to operate at its specified rate when + * the browser has lost focus or is minimized, each update event checks to see if there has been a recent + * update; if not, it resets the clocks by invoking the ContestService updateContestClock() method, which + * resynchronizes the Timer's elapsed and remaining time from the server. + * + * The Timer is started by calling method startTimer(); it can be stopped by calling method stopTimer(). + * When stopped and the restarted the Timer picks up counting where it left off. + */ export class ContestTimerService { elapsedSecs = 0 ; //how many seconds the contest has been running (doesn't include any 'paused' time) @@ -7,9 +23,11 @@ export class ContestTimerService { intervalId: ReturnType ; //the id of the JavaScript "interval" used to generate timer ticks isTimerRunning: boolean = false; - constructor() { + mostRecentTimerUpdate: Date = null; + + constructor(private _contestService: IContestService) { if (DEBUG_MODE) { - console.log ("Executing ContestTimerService constructor"); + console.log ("Executing ContestTimerService constructor using IContestService with id ", _contestService.uniqueId); } } @@ -58,6 +76,14 @@ export class ContestTimerService { } } + /** + * This method starts a Timer which fires once per second, updating the "elapsedSecs" and "remainingSecs" variables each second. + * The method uses the JavaScript "setInterval()" method to detect passage of each second (1000 msec). + * Note that setInterval() is not guaranteed to continue operating at the correct rate if the browser is minimized; + * the method compensates for this by saving the current time at each update and then checking to be sure the + * current update is happening less than two seconds since the last update; if not, it invokes the + * ContestService updateContestClock() method to resync the local contest clock with the server. + */ startTimer() { if (this.intervalId) { console.error("ContestTimerService.startTimer(): call to startTimer() when timer is already running"); @@ -70,13 +96,38 @@ export class ContestTimerService { this.intervalId = setInterval( //execute this function at the specified interval: () => { - //bump the elapsed & remaining counters since 1000msec (one sec) has passed - this.elapsedSecs += 1 ; - this.remainingSecs -= 1 -/* if (DEBUG_MODE) { - console.log(`Elapsed secs: ${this.elapsedSecs}; remaining secs: ${this.remainingSecs}`); - } -*/ + + //update the tracking of when the last timer update happened + let now: Date = new Date(); + if (DEBUG_MODE) { + console.log("ContestTimer.startTimer().setInterval() callback: now = ", now.getTime()); + } + if (this.mostRecentTimerUpdate == null) { + //we've never updated the timer; save current update time + if (DEBUG_MODE) { + console.log ("Contest.startTimer().setInterval() callback: mostRecentTimerUpate is null, setting to 'now'"); + } + this.mostRecentTimerUpdate = now ; + } + + //check how long it's been since a timer update has happened (it could be much longer than the 1-second implied by the setInterval() + // rate because that rate can be significantly slowed if the browser has been minimized) + let timeSpan = now.getTime() - this.mostRecentTimerUpdate.getTime(); //epoch times, in milliseconds + if (timeSpan > 1999) { + + //last update was more than 2 seconds ago; update the contest clock (which also updates the timer) + if (DEBUG_MODE) { + console.log("ContestTimer.startTimer().setInterval() callback: timeSpan since last update is ", timeSpan, "; clock is off by more than 2 seconds; calling ContestService.updateContestClock() to update clock from server"); + } + this._contestService.updateContestClock(); + } else { + //we've seen an update within the last two seconds; just update by one second + this.elapsedSecs += 1 ; + this.remainingSecs -= 1 + if (DEBUG_MODE) { + console.log(`Elapsed: ${this.elapsedSecs}; Remaining: ${this.remainingSecs}; intervalId: ${this.intervalId}`); + } + } }, 1000 // 1000 milliseconds = 1 second interval ); From 72745519b62bbed7529b3961ade8bbd2b703b7eb Mon Sep 17 00:00:00 2001 From: John Clevenger Date: Tue, 7 Jan 2025 17:08:29 -0800 Subject: [PATCH 065/141] i1027: rename method to updateLocalContestClockFromServer() --- projects/WTI-UI/src/app/app.component.ts | 2 +- .../core/abstract-services/i-contest.service.ts | 2 +- .../core/abstract-services/i-websocket.service.ts | 4 ++-- .../app/modules/core/services/contest.mock.service.ts | 2 +- .../src/app/modules/core/services/contest.service.ts | 10 +++++----- .../components/login-page/login-page.component.ts | 4 ++-- 6 files changed, 12 insertions(+), 12 deletions(-) diff --git a/projects/WTI-UI/src/app/app.component.ts b/projects/WTI-UI/src/app/app.component.ts index f4f8ac45b..bc94e010b 100644 --- a/projects/WTI-UI/src/app/app.component.ts +++ b/projects/WTI-UI/src/app/app.component.ts @@ -181,7 +181,7 @@ export class AppComponent implements OnInit { } //restore the contest clock on-screen display values (elapsed and remaining) - this._contestService.updateContestClock(); + this._contestService.updateLocalContestClockFromServer(); // transfer to the (former) "current page". let page = getCurrentPage(); diff --git a/projects/WTI-UI/src/app/modules/core/abstract-services/i-contest.service.ts b/projects/WTI-UI/src/app/modules/core/abstract-services/i-contest.service.ts index a33557b61..6c4d89c94 100644 --- a/projects/WTI-UI/src/app/modules/core/abstract-services/i-contest.service.ts +++ b/projects/WTI-UI/src/app/modules/core/abstract-services/i-contest.service.ts @@ -57,7 +57,7 @@ export abstract class IContestService { abstract getContestClock(): Observable; //note this refers to a "ContestClock" model object, not the "contestClockEvent" Subject above - abstract updateContestClock (): void; //update the WTI-UI representation of the PC2 contest clock + abstract updateLocalContestClockFromServer (): void; //update the WTI-UI representation of the PC2 contest clock abstract getElapsedSecs(): number; diff --git a/projects/WTI-UI/src/app/modules/core/abstract-services/i-websocket.service.ts b/projects/WTI-UI/src/app/modules/core/abstract-services/i-websocket.service.ts index de01c24f0..5db8f3483 100644 --- a/projects/WTI-UI/src/app/modules/core/abstract-services/i-websocket.service.ts +++ b/projects/WTI-UI/src/app/modules/core/abstract-services/i-websocket.service.ts @@ -54,12 +54,12 @@ export abstract class IWebsocketService { console.log ("IWebsocketService.incomingMessage(): callback from ContestService.getIsContestRunning() returned '", val, "'"); console.log ("Setting ContestService.isContestRunning to '", val, "',") ; console.log (" invoking ContestService.contestClockEvent.next()") ; - console.log (" and invoking ContestService.updateContestClock()"); + console.log (" and invoking ContestService.updateLocalContestClockFromServer()"); } this._contestService.isContestRunning = val; this._contestService.contestClockEvent.next(); - this._contestService.updateContestClock(); + this._contestService.updateLocalContestClockFromServer(); }); break; } diff --git a/projects/WTI-UI/src/app/modules/core/services/contest.mock.service.ts b/projects/WTI-UI/src/app/modules/core/services/contest.mock.service.ts index b1e1d83fa..b23399d32 100644 --- a/projects/WTI-UI/src/app/modules/core/services/contest.mock.service.ts +++ b/projects/WTI-UI/src/app/modules/core/services/contest.mock.service.ts @@ -93,7 +93,7 @@ export class ContestMockService extends IContestService { {isRunning:'true', contestLengthSecs:'18000', elapsedSecs:'3600', wallClockStartTime:'1734593847'}); } - updateContestClock () { + updateLocalContestClockFromServer () { //nothing to do -- this is a mock service } diff --git a/projects/WTI-UI/src/app/modules/core/services/contest.service.ts b/projects/WTI-UI/src/app/modules/core/services/contest.service.ts index 2bdee3fa6..1a344e959 100644 --- a/projects/WTI-UI/src/app/modules/core/services/contest.service.ts +++ b/projects/WTI-UI/src/app/modules/core/services/contest.service.ts @@ -38,7 +38,7 @@ export class ContestService extends IContestService { if (DEBUG_MODE) { console.log("ContestService: resyncing clocks with PC2 Server"); } - this.updateContestClock(); + this.updateLocalContestClockFromServer(); }, RESYNC_INTERVAL_IN_MINUTES * 60 * 1000 // timer interval in msec: minutes * (secs-per-min) * (msec-per-sec) ); @@ -105,7 +105,7 @@ export class ContestService extends IContestService { * and when the subscription callback occurs it uses the received ContestClock data (an instance of * WTI=UI models/ContestClock) to update the WTI-UI contest clock (including the onscreen displays). */ - updateContestClock () { + updateLocalContestClockFromServer () { //get the actual contest clock info from the PC2 server via the Contest Service (which gets it via the WTI Server and its PC2 API) if (DEBUG_MODE) { @@ -115,10 +115,10 @@ export class ContestService extends IContestService { .subscribe( (data: any) => { if (!data) { - console.error ("ContestService.updateContestClock() getContestClock() subscription callback: unable to get ContestClock from PC2 API via ContestService!"); + console.error ("ContestService.updateLocalContestClockFromServer() getContestClock() subscription callback: unable to get ContestClock from PC2 API via ContestService!"); } else { if (DEBUG_MODE) { - console.log("ContestService.updateContestClock(): got callback from getContestClock() subscription; callback data ="); + console.log("ContestService.updateLocalContestClockFromServer(): got callback from getContestClock() subscription; callback data ="); console.log(data); } //install the received contest clock data into the ContestService's ContestClock @@ -126,7 +126,7 @@ export class ContestService extends IContestService { } }, (error: any) => { - console.error("ContestService.updateContestClock(): getContestClock() subscription callback error: "); + console.error("ContestService.updateLocalContestClockFromServer(): getContestClock() subscription callback error: "); console.error (error); } ); diff --git a/projects/WTI-UI/src/app/modules/login/components/login-page/login-page.component.ts b/projects/WTI-UI/src/app/modules/login/components/login-page/login-page.component.ts index 717531ae9..62eb75e83 100644 --- a/projects/WTI-UI/src/app/modules/login/components/login-page/login-page.component.ts +++ b/projects/WTI-UI/src/app/modules/login/components/login-page/login-page.component.ts @@ -136,9 +136,9 @@ export class LoginPageComponent implements OnInit, OnDestroy { //update the WTI-UI representations of the PC2 Contest Clock if (DEBUG_MODE) { - console.log (" Invoking ContestService.updateContestClock()") ; + console.log (" Invoking ContestService.updateLocalContestClockFromServer()") ; } - this._contestService.updateContestClock(); + this._contestService.updateLocalContestClockFromServer(); }, (error: any) => { console.error ("LoginPageComponent.onSubmit(): AuthService.login() subscription callback error: "); From dec524c9652af08d8570e761f1e3619d2d1c8279 Mon Sep 17 00:00:00 2001 From: John Clevenger Date: Tue, 7 Jan 2025 17:13:46 -0800 Subject: [PATCH 066/141] i1027: ContestTimer: always update "mostRecentTimerUpdate"; also: apply renaming of updateContestClock() to updateLocalContestClockFromServer(). --- .../modules/core/services/contestTimer.service.ts | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/projects/WTI-UI/src/app/modules/core/services/contestTimer.service.ts b/projects/WTI-UI/src/app/modules/core/services/contestTimer.service.ts index 7dce5860a..db9f90297 100644 --- a/projects/WTI-UI/src/app/modules/core/services/contestTimer.service.ts +++ b/projects/WTI-UI/src/app/modules/core/services/contestTimer.service.ts @@ -10,7 +10,7 @@ import { DEBUG_MODE } from 'src/constants' * The Timer uses a JavaScript "setInterval()" to generate update events at the nominal once-per-second * rate. Since setInterval() is documented to not be guaranteed to operate at its specified rate when * the browser has lost focus or is minimized, each update event checks to see if there has been a recent - * update; if not, it resets the clocks by invoking the ContestService updateContestClock() method, which + * update; if not, it resets the clocks by invoking the ContestService updateLocalContestClockFromServer() method, which * resynchronizes the Timer's elapsed and remaining time from the server. * * The Timer is started by calling method startTimer(); it can be stopped by calling method stopTimer(). @@ -82,7 +82,7 @@ export class ContestTimerService { * Note that setInterval() is not guaranteed to continue operating at the correct rate if the browser is minimized; * the method compensates for this by saving the current time at each update and then checking to be sure the * current update is happening less than two seconds since the last update; if not, it invokes the - * ContestService updateContestClock() method to resync the local contest clock with the server. + * ContestService updateLocalContestClockFromServer() method to resync the local contest clock with the server. */ startTimer() { if (this.intervalId) { @@ -117,9 +117,10 @@ export class ContestTimerService { //last update was more than 2 seconds ago; update the contest clock (which also updates the timer) if (DEBUG_MODE) { - console.log("ContestTimer.startTimer().setInterval() callback: timeSpan since last update is ", timeSpan, "; clock is off by more than 2 seconds; calling ContestService.updateContestClock() to update clock from server"); + console.log("ContestTimer.startTimer().setInterval() callback: timeSpan since last update is ", timeSpan ); + console.log ("clock is off by more than 2 seconds; calling ContestService.updateLocalContestClockFromServer() to update clock"); } - this._contestService.updateContestClock(); + this._contestService.updateLocalContestClockFromServer(); } else { //we've seen an update within the last two seconds; just update by one second this.elapsedSecs += 1 ; @@ -128,6 +129,10 @@ export class ContestTimerService { console.log(`Elapsed: ${this.elapsedSecs}; Remaining: ${this.remainingSecs}; intervalId: ${this.intervalId}`); } } + + //record that we've now updated the contest clock display + this.mostRecentTimerUpdate = now ; + }, 1000 // 1000 milliseconds = 1 second interval ); From 046b4211ccdc6c20ebedb23fb62db4c739e3a20f Mon Sep 17 00:00:00 2001 From: johnc Date: Fri, 10 Jan 2025 19:09:54 -0800 Subject: [PATCH 067/141] i1027: removed multiple no-longer-needed debugging outputs. --- .../modules/core/services/contest.service.ts | 11 +------- .../core/services/contestTimer.service.ts | 28 ++----------------- 2 files changed, 4 insertions(+), 35 deletions(-) diff --git a/projects/WTI-UI/src/app/modules/core/services/contest.service.ts b/projects/WTI-UI/src/app/modules/core/services/contest.service.ts index 1a344e959..458d8e6ec 100644 --- a/projects/WTI-UI/src/app/modules/core/services/contest.service.ts +++ b/projects/WTI-UI/src/app/modules/core/services/contest.service.ts @@ -118,8 +118,7 @@ export class ContestService extends IContestService { console.error ("ContestService.updateLocalContestClockFromServer() getContestClock() subscription callback: unable to get ContestClock from PC2 API via ContestService!"); } else { if (DEBUG_MODE) { - console.log("ContestService.updateLocalContestClockFromServer(): got callback from getContestClock() subscription; callback data ="); - console.log(data); + console.log("ContestService.updateLocalContestClockFromServer(): got callback from getContestClock() subscription."); } //install the received contest clock data into the ContestService's ContestClock this.installNewContestClock(data); @@ -159,14 +158,6 @@ export class ContestService extends IContestService { let elapsedSecs = parseInt(this.contestClock.elapsedSecs); let contestLengthSecs = parseInt(this.contestClock.contestLengthSecs); let remainingSecs = contestLengthSecs - elapsedSecs; - - if (DEBUG_MODE) { - console.log ("ContestService.installNewContestClock(): values pulled from newContestClock:"); - console.log (" timerShouldBeStarted = ", timerShouldBeStarted); - console.log (" elapsedSecs = ", elapsedSecs); - console.log (" contestLengthSecs = ", contestLengthSecs); - console.log (" remainingSecs = ", remainingSecs); - } //shut off timer if it is running (otherwise we can't update the elapsed/remaining time values) if (this.contestTimer.isTimerRunning) { diff --git a/projects/WTI-UI/src/app/modules/core/services/contestTimer.service.ts b/projects/WTI-UI/src/app/modules/core/services/contestTimer.service.ts index db9f90297..2a8175059 100644 --- a/projects/WTI-UI/src/app/modules/core/services/contestTimer.service.ts +++ b/projects/WTI-UI/src/app/modules/core/services/contestTimer.service.ts @@ -32,18 +32,10 @@ export class ContestTimerService { } getElapsedSecs(): number { -/* if (DEBUG_MODE) { - console.log("ContestTimerService.getElapsedSecs(): returning ", this.elapsedSecs); - } -*/ return this.elapsedSecs; } getRemainingSecs(): number { -/* if (DEBUG_MODE) { - console.log("ContestTimerService.getRemainingSecs(): returning ", this.remainingSecs); - } -*/ return this.remainingSecs; } @@ -99,35 +91,21 @@ export class ContestTimerService { //update the tracking of when the last timer update happened let now: Date = new Date(); - if (DEBUG_MODE) { - console.log("ContestTimer.startTimer().setInterval() callback: now = ", now.getTime()); - } if (this.mostRecentTimerUpdate == null) { //we've never updated the timer; save current update time - if (DEBUG_MODE) { - console.log ("Contest.startTimer().setInterval() callback: mostRecentTimerUpate is null, setting to 'now'"); - } this.mostRecentTimerUpdate = now ; } //check how long it's been since a timer update has happened (it could be much longer than the 1-second implied by the setInterval() // rate because that rate can be significantly slowed if the browser has been minimized) let timeSpan = now.getTime() - this.mostRecentTimerUpdate.getTime(); //epoch times, in milliseconds - if (timeSpan > 1999) { - - //last update was more than 2 seconds ago; update the contest clock (which also updates the timer) - if (DEBUG_MODE) { - console.log("ContestTimer.startTimer().setInterval() callback: timeSpan since last update is ", timeSpan ); - console.log ("clock is off by more than 2 seconds; calling ContestService.updateLocalContestClockFromServer() to update clock"); - } + if (timeSpan > 4999) { + //last update was more than 5 seconds ago; update the contest clock from the server (which also updates the timer) this._contestService.updateLocalContestClockFromServer(); } else { - //we've seen an update within the last two seconds; just update by one second + //we've seen an update within the last five seconds; just update by one second this.elapsedSecs += 1 ; this.remainingSecs -= 1 - if (DEBUG_MODE) { - console.log(`Elapsed: ${this.elapsedSecs}; Remaining: ${this.remainingSecs}; intervalId: ${this.intervalId}`); - } } //record that we've now updated the contest clock display From fcd4a060650bdd4009228b3542b29bc980a42996 Mon Sep 17 00:00:00 2001 From: johnc Date: Fri, 10 Jan 2025 19:10:21 -0800 Subject: [PATCH 068/141] i1027: added debug output. --- projects/WTI-UI/src/app/app.component.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/projects/WTI-UI/src/app/app.component.ts b/projects/WTI-UI/src/app/app.component.ts index bc94e010b..c64f3ce91 100644 --- a/projects/WTI-UI/src/app/app.component.ts +++ b/projects/WTI-UI/src/app/app.component.ts @@ -181,6 +181,9 @@ export class AppComponent implements OnInit { } //restore the contest clock on-screen display values (elapsed and remaining) + if (DEBUG_MODE) { + console.log ('Updating local contest clock values from server'); + } this._contestService.updateLocalContestClockFromServer(); // transfer to the (former) "current page". From 3465ccc9538b1390d4eb92d79eafdac3ba3cea75 Mon Sep 17 00:00:00 2001 From: johnc Date: Fri, 10 Jan 2025 20:37:02 -0800 Subject: [PATCH 069/141] i1027: added documentation (no code changes) --- .../abstract-services/i-websocket.service.ts | 5 +++++ .../modules/core/services/contest.service.ts | 16 ++++++++++++++++ .../core/services/contestTimer.service.ts | 7 +++++-- .../core/services/displayTimePipe.service.ts | 13 +++++++------ .../app-header/app-header.component.ts | 17 +++++++++-------- 5 files changed, 42 insertions(+), 16 deletions(-) diff --git a/projects/WTI-UI/src/app/modules/core/abstract-services/i-websocket.service.ts b/projects/WTI-UI/src/app/modules/core/abstract-services/i-websocket.service.ts index 5db8f3483..ee61ea0ac 100644 --- a/projects/WTI-UI/src/app/modules/core/abstract-services/i-websocket.service.ts +++ b/projects/WTI-UI/src/app/modules/core/abstract-services/i-websocket.service.ts @@ -43,6 +43,11 @@ export abstract class IWebsocketService { break; } case 'contest_clock': { + /* This case is invoked when the WTI Server gets a "Contest_Clock" message from the PC2 Server + * (via the PC2 API "ConfigurationItemUpdated()" listener in the WTI class "ConfigurationService"). + * Such a message from the PC2 Server causes the WTI Server to send a "contest_clock" message through + * the websocket to this client. + */ if (DEBUG_MODE) { console.log ("IWebsocketService.incomingMessage(): got websocket message '", message.type); console.log ("IWebsocketService.incomingMessage(): invoking ContestService.getIsContestRunning() and"); diff --git a/projects/WTI-UI/src/app/modules/core/services/contest.service.ts b/projects/WTI-UI/src/app/modules/core/services/contest.service.ts index 458d8e6ec..affdc8598 100644 --- a/projects/WTI-UI/src/app/modules/core/services/contest.service.ts +++ b/projects/WTI-UI/src/app/modules/core/services/contest.service.ts @@ -11,6 +11,22 @@ import { Clarification } from '../models/clarification'; import { DEBUG_MODE } from 'src/constants'; import { RESYNC_INTERVAL_IN_MINUTES } from 'src/constants'; +/** + * This class provides a variety of "contest-related" services for clients. + * It provides methods for initiating HTTP calls to the WTI Server to obtain contest + * information (such as languages, problems, clarifications, standings, and the current + * contest clock value); each of these services returns an "Observable" to which the invoking + * client can "subscribe" to receive a callback when the actual data is available from the HTTP call. + * + * The class also provides localized services such as: + * + * - keeping track of whether the current WTI-UI understanding of the contest standings (scoreboard) + * are current (it maintains a cache of standings and keeps track of events which could cause the cache to be out-of-date); + * - a set of methods for providing clients with the current contest elapsed and remaining times, together with methods + * to force the local copy of the contest clock to be updated from the server and methods to load a new value into the + * local contest clock; + * - a local timer which forces the local (on-screen) contest clock to be resynced with the server periodically. + */ @Injectable({ providedIn: 'root' //forces the service to be a singleton across all app components ('root' == "root injector") }) diff --git a/projects/WTI-UI/src/app/modules/core/services/contestTimer.service.ts b/projects/WTI-UI/src/app/modules/core/services/contestTimer.service.ts index 2a8175059..e90df4354 100644 --- a/projects/WTI-UI/src/app/modules/core/services/contestTimer.service.ts +++ b/projects/WTI-UI/src/app/modules/core/services/contestTimer.service.ts @@ -13,8 +13,11 @@ import { DEBUG_MODE } from 'src/constants' * update; if not, it resets the clocks by invoking the ContestService updateLocalContestClockFromServer() method, which * resynchronizes the Timer's elapsed and remaining time from the server. * - * The Timer is started by calling method startTimer(); it can be stopped by calling method stopTimer(). - * When stopped and the restarted the Timer picks up counting where it left off. + * The Timer is started by calling method startTimer(); it can be stopped by calling method stopTimer(); + * these methods are intended to be invoked by external code which detects when the PC2 Admin starts/stops + * the PC2 contest clock. + * + * When stopped and then restarted the Timer picks up counting where it left off. */ export class ContestTimerService { diff --git a/projects/WTI-UI/src/app/modules/core/services/displayTimePipe.service.ts b/projects/WTI-UI/src/app/modules/core/services/displayTimePipe.service.ts index 8307df7ad..eef6b9d32 100644 --- a/projects/WTI-UI/src/app/modules/core/services/displayTimePipe.service.ts +++ b/projects/WTI-UI/src/app/modules/core/services/displayTimePipe.service.ts @@ -1,12 +1,13 @@ import { Pipe, PipeTransform } from '@angular/core'; import { DEBUG_MODE } from 'src/constants' -//This class defines a "pipe" which accepts a time, represented as a number of seconds, and returns -// a formatted string representing the number of days/hours/minutes/seconds represented by the input number of seconds. -//"Days" are only included in the output if they are greater than zero. -//The "pipe" is suitable for use, for example, in an Angular HTML template wanting to display an elapsed number of seconds -// in terms of hours/mins/secs (see app-header.component.html). - +/** This class defines a "pipe" which accepts a time, represented as a number of seconds, and returns + * a formatted string representing the number of days/hours/minutes/seconds represented by the input number of seconds. + * "Days" are only included in the output if they are greater than zero. + * The "pipe" is suitable for use, for example, in an Angular HTML template wanting to display an elapsed number of seconds + * in terms of hours/mins/secs (see app-header.component.html). + */ + @Pipe({ name: 'displayTime', standalone: true diff --git a/projects/WTI-UI/src/app/modules/shared/components/app-header/app-header.component.ts b/projects/WTI-UI/src/app/modules/shared/components/app-header/app-header.component.ts index 89cb6b543..2023a9fca 100644 --- a/projects/WTI-UI/src/app/modules/shared/components/app-header/app-header.component.ts +++ b/projects/WTI-UI/src/app/modules/shared/components/app-header/app-header.component.ts @@ -3,6 +3,15 @@ import { AuthService } from 'src/app/modules/core/auth/auth.service'; import { IContestService } from 'src/app/modules/core/abstract-services/i-contest.service'; import { DEBUG_MODE } from 'src/constants' +/** + * This class defines a component which acts as the "header" for all WTI-UI pages. Together with the + * corresponding app-header.component.html file, the component displays header images (such as the PC2 Balloon Logo). + * If there is currently a team logged in, then the header component also shows + * links to the various WTI-UI pages (such as Runs, Clarifications, Scoreboard, etc.), the currently logged-in team's ID, + * and clocks showing the current contest elapsed time and remaining time (whose values it gets from the + * injected ContestService). + * + */ @Component({ selector: 'app-header', templateUrl: './app-header.component.html', @@ -36,19 +45,11 @@ export class AppHeaderComponent { getElapsedSecs(): number { let secs = this._contestService.getElapsedSecs() ; -/* if (DEBUG_MODE) { - console.log("AppHeaderComponent.getElapsedSecs(): returning ", secs); - } -*/ return secs; } getRemainingSecs(): number { let secs = this._contestService.getRemainingSecs() ; -/* if (DEBUG_MODE) { - console.log("AppHeaderComponent.getRemainingSecs(): returning ", secs); - } -*/ return secs; } } From 3af4594f9416cb2e3d1f3a4d853d9997f696096e Mon Sep 17 00:00:00 2001 From: johnc Date: Sat, 14 Dec 2024 16:52:48 -0800 Subject: [PATCH 070/141] i1027: added contest timers to app-header files in "shared" module. --- .../components/app-header/app-header.component.html | 13 +++++++++++++ .../components/app-header/app-header.component.scss | 10 ++++++++++ .../components/app-header/app-header.component.ts | 3 +++ .../WTI-UI/src/app/modules/shared/shared.module.ts | 3 +++ 4 files changed, 29 insertions(+) diff --git a/projects/WTI-UI/src/app/modules/shared/components/app-header/app-header.component.html b/projects/WTI-UI/src/app/modules/shared/components/app-header/app-header.component.html index 470818851..d35e4c331 100644 --- a/projects/WTI-UI/src/app/modules/shared/components/app-header/app-header.component.html +++ b/projects/WTI-UI/src/app/modules/shared/components/app-header/app-header.component.html @@ -4,9 +4,17 @@ VerticalBar ICPC Programming Contest Logo
+ +
+ Elapsed: + {{time.days}}d {{time.hours}}h {{time.minutes}}m {{time.seconds}}s + +
+

TEAM {{teamId}}

+ + +
+ Remaining: +
+ diff --git a/projects/WTI-UI/src/app/modules/shared/components/app-header/app-header.component.scss b/projects/WTI-UI/src/app/modules/shared/components/app-header/app-header.component.scss index 5f354d056..163b99a76 100644 --- a/projects/WTI-UI/src/app/modules/shared/components/app-header/app-header.component.scss +++ b/projects/WTI-UI/src/app/modules/shared/components/app-header/app-header.component.scss @@ -14,6 +14,16 @@ header { .teamid-container { color: white; font-size: 1.5rem; + padding-left: 8px; + padding-right: 8px; + } + + /* Set the container holding the contest clock value(s) to medium, white font */ + .clock-container { + color: white; + font-size: 1.0rem; + padding-left: 8px; + padding-right: 8px; } nav a { diff --git a/projects/WTI-UI/src/app/modules/shared/components/app-header/app-header.component.ts b/projects/WTI-UI/src/app/modules/shared/components/app-header/app-header.component.ts index 912435306..dae851204 100644 --- a/projects/WTI-UI/src/app/modules/shared/components/app-header/app-header.component.ts +++ b/projects/WTI-UI/src/app/modules/shared/components/app-header/app-header.component.ts @@ -14,6 +14,9 @@ export class AppHeaderComponent { //Return a boolean indicating whether or not to show a teamId in the header get showTeamId(): boolean { return this._authService.isLoggedIn; } + //Return a boolean indicating whether or not to show the contest clock(s) in the header + get showClocks(): boolean { return this._authService.isLoggedIn; } + /* Return a string containing the "team id" -- that is, the PC2 team account number with the leading "team" removed */ get teamId(): string { diff --git a/projects/WTI-UI/src/app/modules/shared/shared.module.ts b/projects/WTI-UI/src/app/modules/shared/shared.module.ts index 434aae915..c4d736029 100644 --- a/projects/WTI-UI/src/app/modules/shared/shared.module.ts +++ b/projects/WTI-UI/src/app/modules/shared/shared.module.ts @@ -13,6 +13,7 @@ import {MatInputModule} from '@angular/material/input'; import {MatSelectModule} from '@angular/material/select'; import {MatSnackBarModule} from '@angular/material/snack-bar'; import { AboutWtiComponent } from './components/about-wti/about-wti.component'; +import { CountdownModule } from 'ngx-countdown'; @NgModule({ declarations: [ @@ -32,6 +33,7 @@ import { AboutWtiComponent } from './components/about-wti/about-wti.component'; MatInputModule, MatSelectModule, MatSnackBarModule, + CountdownModule ], exports: [ AppHeaderComponent, @@ -44,6 +46,7 @@ import { AboutWtiComponent } from './components/about-wti/about-wti.component'; MatInputModule, MatSelectModule, MatSnackBarModule, + CountdownModule ] }) export class SharedModule { } From 83d33ab602b32d8128cfb98069d107d2383a3e79 Mon Sep 17 00:00:00 2001 From: johnc Date: Sat, 14 Dec 2024 16:53:39 -0800 Subject: [PATCH 071/141] i1027: added ngx-countdown@v16.0 to package.json --- projects/WTI-UI/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/projects/WTI-UI/package.json b/projects/WTI-UI/package.json index 86a8a1dd7..d85d926f5 100644 --- a/projects/WTI-UI/package.json +++ b/projects/WTI-UI/package.json @@ -22,6 +22,7 @@ "@angular/platform-browser-dynamic": "^16.2.11", "@angular/router": "^16.2.11", "core-js": "^3.33.1", + "ngx-countdown": "16.0", "rxjs": "^6.5.3 || ^7.4.0", "tslib": "^1.9.0", "zone.js": "^0.13.X" From 9130038e430ffb9ba1203fd1a160f9f6a985f52e Mon Sep 17 00:00:00 2001 From: johnc Date: Tue, 17 Dec 2024 14:56:48 -0800 Subject: [PATCH 072/141] i1027: added various attempts to get HTML properly displaying clocks. --- .../app-header/app-header.component.html | 28 +++++++++++++++++-- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/projects/WTI-UI/src/app/modules/shared/components/app-header/app-header.component.html b/projects/WTI-UI/src/app/modules/shared/components/app-header/app-header.component.html index d35e4c331..ddb2acfa7 100644 --- a/projects/WTI-UI/src/app/modules/shared/components/app-header/app-header.component.html +++ b/projects/WTI-UI/src/app/modules/shared/components/app-header/app-header.component.html @@ -6,9 +6,30 @@
- Elapsed: + Elapsed:
+ +
{{ elapsedSecs }}
+ + +
{{ getElapsedTimeAsDate() | elapsedTime }}
+ + + + + + + +
@@ -24,7 +45,8 @@
- Remaining: + Remaining:
+
From 8ef6949971eb7a1f752c597ccdd32f9ccabcdd4f Mon Sep 17 00:00:00 2001 From: johnc Date: Tue, 17 Dec 2024 14:58:17 -0800 Subject: [PATCH 073/141] i1027: added setInterval object to gen timer ticks; added date func. --- .../app-header/app-header.component.ts | 24 ++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/projects/WTI-UI/src/app/modules/shared/components/app-header/app-header.component.ts b/projects/WTI-UI/src/app/modules/shared/components/app-header/app-header.component.ts index dae851204..4fcca263a 100644 --- a/projects/WTI-UI/src/app/modules/shared/components/app-header/app-header.component.ts +++ b/projects/WTI-UI/src/app/modules/shared/components/app-header/app-header.component.ts @@ -1,5 +1,6 @@ import { Component } from '@angular/core'; import { AuthService } from 'src/app/modules/core/auth/auth.service'; +import { ElapsedTimePipe } from 'src/app/modules/core/services/elapsedTimePipe.service'; @Component({ selector: 'app-header', @@ -24,6 +25,27 @@ export class AppHeaderComponent { let teamId = acctId.substr(4); return teamId; } + + elapsedSecs = 0 ; + + getElapsedTimeAsDate(): Date { + let newDate = new Date(this.elapsedSecs * 1000); + console.log(newDate.toString()); + return newDate ; + } - constructor(private _authService: AuthService) { } + + constructor(private _authService: AuthService) { + + setInterval( + //execute this function at the specified interval: + () => { + //add 1 second to the counter since 1000msec have passed + this.elapsedSecs += 1 ; + console.log(`Seconds: ${this.elapsedSecs}`); + }, + 1000 // 1000 milliseconds = 1 second interval + ); + + } } From 84246f0a4d2390156741ec015d87d4ea59a4e2f4 Mon Sep 17 00:00:00 2001 From: johnc Date: Tue, 17 Dec 2024 14:59:40 -0800 Subject: [PATCH 074/141] i1027: added attempts to access new ElapsedTimePipe from core module. --- projects/WTI-UI/src/app/modules/core/core.module.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/projects/WTI-UI/src/app/modules/core/core.module.ts b/projects/WTI-UI/src/app/modules/core/core.module.ts index d155d6844..6f353f04d 100644 --- a/projects/WTI-UI/src/app/modules/core/core.module.ts +++ b/projects/WTI-UI/src/app/modules/core/core.module.ts @@ -16,6 +16,7 @@ import { IWebsocketService } from './abstract-services/i-websocket.service'; import { UiHelperService } from './services/ui-helper.service'; import { SharedModule } from '../shared/shared.module'; import { DEBUG_MODE } from 'src/constants'; +import { CommonModule } from '@angular/common'; export function TeamsServiceFactory(http: HttpClient) { if (DEBUG_MODE) { @@ -93,8 +94,8 @@ export function WebsocketServiceFactory(injector: Injector, ], imports: [ HttpClientModule, - SharedModule + SharedModule, + CommonModule ], - exports: [], }) export class CoreModule { } From a21c90be3614c5184d971ce718fd500a3d7f5d7c Mon Sep 17 00:00:00 2001 From: johnc Date: Tue, 17 Dec 2024 15:01:27 -0800 Subject: [PATCH 075/141] i1027: added attempts to access ElapsedTimePipe from shared module. --- .../WTI-UI/src/app/modules/shared/shared.module.ts | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/projects/WTI-UI/src/app/modules/shared/shared.module.ts b/projects/WTI-UI/src/app/modules/shared/shared.module.ts index c4d736029..3e5f52b71 100644 --- a/projects/WTI-UI/src/app/modules/shared/shared.module.ts +++ b/projects/WTI-UI/src/app/modules/shared/shared.module.ts @@ -8,12 +8,14 @@ import { AppFooterComponent } from './components/app-footer/app-footer.component import { RouterModule } from '@angular/router'; import { JudgementSelectorComponent } from './components/judgement-selector/judgement-selector.component'; import { MatDialogModule } from '@angular/material/dialog'; -import {MatFormFieldModule} from '@angular/material/form-field'; -import {MatInputModule} from '@angular/material/input'; -import {MatSelectModule} from '@angular/material/select'; -import {MatSnackBarModule} from '@angular/material/snack-bar'; +import { MatFormFieldModule } from '@angular/material/form-field'; +import { MatInputModule } from '@angular/material/input'; +import { MatSelectModule } from '@angular/material/select'; +import { MatSnackBarModule } from '@angular/material/snack-bar'; import { AboutWtiComponent } from './components/about-wti/about-wti.component'; import { CountdownModule } from 'ngx-countdown'; +import { BrowserModule } from '@angular/platform-browser'; +import { ElapsedTimePipe } from 'src/app/modules/core/services/elapsedTimePipe.service'; @NgModule({ declarations: [ @@ -33,7 +35,9 @@ import { CountdownModule } from 'ngx-countdown'; MatInputModule, MatSelectModule, MatSnackBarModule, - CountdownModule + CountdownModule, + BrowserModule, + ElapsedTimePipe ], exports: [ AppHeaderComponent, From 75604ce33cf3405c75587718f12976980f57b196 Mon Sep 17 00:00:00 2001 From: johnc Date: Tue, 17 Dec 2024 15:02:37 -0800 Subject: [PATCH 076/141] i1027: added new ElapsedTimePipe for formatting dates into dd:hh:mm:ss. --- .../core/services/elapsedTimePipe.service.ts | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 projects/WTI-UI/src/app/modules/core/services/elapsedTimePipe.service.ts diff --git a/projects/WTI-UI/src/app/modules/core/services/elapsedTimePipe.service.ts b/projects/WTI-UI/src/app/modules/core/services/elapsedTimePipe.service.ts new file mode 100644 index 000000000..ddaee036b --- /dev/null +++ b/projects/WTI-UI/src/app/modules/core/services/elapsedTimePipe.service.ts @@ -0,0 +1,28 @@ +import { Pipe, PipeTransform } from '@angular/core'; + +//coded generated by Google AI +//Explanation: +//The pipe takes a Date or a timestamp as input. +//It calculates the difference in milliseconds between the given date and the current time. +//Then, it extracts days, hours, minutes, and seconds from the difference. +//Finally, it formats the output string. + +@Pipe({ + name: 'elapsedTime' +}) +export class ElapsedTimePipe implements PipeTransform { + + transform(value: Date | number): string { + const now = new Date().getTime(); + const then = new Date(value).getTime(); + + const diff = now - then; + + const days = Math.floor(diff / (1000 * 60 * 60 * 24)); + const hours = Math.floor((diff % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)); + const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60)); + const seconds = Math.floor((diff % (1000 * 60)) / 1000); + + return `${days}d ${hours}h ${minutes}m ${seconds}s`; + } +} \ No newline at end of file From 31e5afccbf10db692f193f1b1bb42eb5abf7f805 Mon Sep 17 00:00:00 2001 From: John Clevenger Date: Wed, 18 Dec 2024 14:26:06 -0800 Subject: [PATCH 077/141] i1027: added method getContestClock() to ContestServices (to both... the abstract and concrete versions) --- .../modules/core/abstract-services/i-contest.service.ts | 6 +++++- .../src/app/modules/core/services/contest.service.ts | 8 ++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/projects/WTI-UI/src/app/modules/core/abstract-services/i-contest.service.ts b/projects/WTI-UI/src/app/modules/core/abstract-services/i-contest.service.ts index 0f2463880..c905f074e 100644 --- a/projects/WTI-UI/src/app/modules/core/abstract-services/i-contest.service.ts +++ b/projects/WTI-UI/src/app/modules/core/abstract-services/i-contest.service.ts @@ -2,6 +2,7 @@ import { Injectable } from '@angular/core'; import { Observable, Subject } from 'rxjs'; import { ContestLanguage } from '../models/contest-language'; import { ContestProblem } from '../models/contest-problem'; +import { ContestClock } from '../models/contest-clock'; import { Clarification } from '../models/clarification'; import { DEBUG_MODE } from 'src/constants'; @@ -10,7 +11,8 @@ import { DEBUG_MODE } from 'src/constants'; }) export abstract class IContestService { clarificationsUpdated = new Subject(); - contestClock = new Subject(); + contestClock = new Subject(); //note that this is a WTI-UI-specific object used in conjunction with Websocket "clock" messages; + //it is NOT a "ContestClock" object in the sense of classes defined in core/models. standingsUpdated = new Subject(); isContestRunning = false; @@ -35,6 +37,8 @@ export abstract class IContestService { abstract getClarifications(): Observable; abstract getIsContestRunning(): Observable; + + abstract getContestClock(): Observable; //note that this refers to a "ContestClock" model object, not the "contestClock" subject above abstract getStandings(): Observable; diff --git a/projects/WTI-UI/src/app/modules/core/services/contest.service.ts b/projects/WTI-UI/src/app/modules/core/services/contest.service.ts index 5f299e0ac..a2ddb8d68 100644 --- a/projects/WTI-UI/src/app/modules/core/services/contest.service.ts +++ b/projects/WTI-UI/src/app/modules/core/services/contest.service.ts @@ -5,6 +5,7 @@ import { ContestLanguage } from '../models/contest-language'; import { HttpClient } from '@angular/common/http'; import { environment } from 'src/environments/environment'; import { ContestProblem } from '../models/contest-problem'; +import { ContestClock } from '../models/contest-clock'; import { Clarification } from '../models/clarification'; import { DEBUG_MODE } from 'src/constants'; @@ -49,6 +50,13 @@ export class ContestService extends IContestService { return this._httpClient.get(`${environment.baseUrl}/contest/isRunning`); } + /** This method returns an Observable "ContestClock" object -- a WTI-UI model corresponding to the PC2 "ContestTime" class, + * which itself encapsulates the "contest clock" on the PC2 server. + */ + getContestClock(): Observable { + return this._httpClient.get(`${environment.baseUrl}/contest/contestclock`); + } + getStandings(): Observable { if (DEBUG_MODE) { console.log("ContestService.getStandings():") From 4e22cce663bb9541e1e41e104790c5d727036206 Mon Sep 17 00:00:00 2001 From: John Clevenger Date: Wed, 18 Dec 2024 14:30:07 -0800 Subject: [PATCH 078/141] i1027: added ContestClockModel to hold the WTI-API rep of ContestTime. --- .../src/main/models/ContestClockModel.java | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 projects/WTI-API/src/main/models/ContestClockModel.java diff --git a/projects/WTI-API/src/main/models/ContestClockModel.java b/projects/WTI-API/src/main/models/ContestClockModel.java new file mode 100644 index 000000000..260c1c9f5 --- /dev/null +++ b/projects/WTI-API/src/main/models/ContestClockModel.java @@ -0,0 +1,43 @@ +package models; + +import edu.csus.ecs.pc2.core.model.ContestTime; + +/** + * This class encapsulates the features of the PC2 "contest clock" (class {@link ContestTime}) which need to be able to be passed + * to the WTI-UI. + * + * @author JohnC + * + */ +public class ContestClockModel { + + private boolean isRunning; + private long contestLengthInSecs; + private long elapsedSecs; //total time in seconds that the contest has been running -- does NOT include time during any "pauses" + private long wallClockStartTime; //unix timestamp when the contest actually started -- msec since the Epoch. Does not change due to "pauses" + + public ContestClockModel(boolean isRunning, long contestLengthInSecs, long elapsedSecs, long wallClockStartTime) { + this.isRunning = isRunning; + this.contestLengthInSecs = contestLengthInSecs; + this.elapsedSecs = elapsedSecs; + this.wallClockStartTime = wallClockStartTime; + } + + public ContestClockModel() { } + + public boolean isRunning() { + return isRunning; + } + + public long getContestLengthInSecs() { + return contestLengthInSecs; + } + + public long getElapsedSecs() { + return elapsedSecs; + } + + public long getWallClockStartTime() { + return wallClockStartTime; + } +} From 6679a44c3963c20703023faeefbb58338a759998 Mon Sep 17 00:00:00 2001 From: John Clevenger Date: Wed, 18 Dec 2024 15:19:28 -0800 Subject: [PATCH 079/141] i1027: added support for getting contest start time via the PC2 API. --- src/edu/csus/ecs/pc2/api/IContestClock.java | 18 +++++++++++++++++- .../ContestTimeImplementation.java | 11 +++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/src/edu/csus/ecs/pc2/api/IContestClock.java b/src/edu/csus/ecs/pc2/api/IContestClock.java index f7e3239b8..89ec83d5f 100644 --- a/src/edu/csus/ecs/pc2/api/IContestClock.java +++ b/src/edu/csus/ecs/pc2/api/IContestClock.java @@ -1,10 +1,13 @@ // Copyright (C) 1989-2019 PC2 Development Team: John Clevenger, Douglas Lane, Samir Ashoo, and Troy Boudreau. package edu.csus.ecs.pc2.api; +import java.util.Calendar; + /** * This interface describes the PC2 API view of Contest Time information. * It provides methods for accessing various time-related aspects of the contest, including - * how much time has elapsed, how much time remains, and how long the contest is scheduled to last. + * how much time has elapsed, how much time remains, how long the contest is scheduled to last, + * and (once the contest has been started) the date/time at which it was started. *

* Note that under the current implementation, an {@link IContestClock} object is static once it * is obtained; to get a current copy of the contest time information a new {@link IContestClock} object @@ -62,6 +65,19 @@ public interface IContestClock { * @return true if the contest clock is currently running; false otherwise. */ boolean isContestClockRunning (); + + /** + * Returns a {@link Calendar} containing the date/time that the contest actually started, + * or null if the contest has not ever been started. + * Note that the returned value is independent of (has nothing to do with) the "Scheduled Start Time"; + * the latter is only relevant before the contest actually starts. + * Note also that once the contest is started, the value returned by this method never changes; + * specifically, it is not affected by any subsequent "pause" (a.k.a. "stop contest") operations, + * nor by any "start contest" operations following a pause; the returned value always indicates + * when the contest was FIRST started. + * @return a {@link Calendar} object indicating the date/time that the contest actually started. + */ + public Calendar getContestStartTime(); /** * Check whether this ContestClock object is the same as some other ContestClock. diff --git a/src/edu/csus/ecs/pc2/api/implementation/ContestTimeImplementation.java b/src/edu/csus/ecs/pc2/api/implementation/ContestTimeImplementation.java index f50a105bb..46d3c09de 100644 --- a/src/edu/csus/ecs/pc2/api/implementation/ContestTimeImplementation.java +++ b/src/edu/csus/ecs/pc2/api/implementation/ContestTimeImplementation.java @@ -1,6 +1,8 @@ // Copyright (C) 1989-2019 PC2 Development Team: John Clevenger, Douglas Lane, Samir Ashoo, and Troy Boudreau. package edu.csus.ecs.pc2.api.implementation; +import java.util.Calendar; + import edu.csus.ecs.pc2.api.IContestClock; import edu.csus.ecs.pc2.core.model.ContestTime; import edu.csus.ecs.pc2.core.model.ElementId; @@ -36,6 +38,15 @@ public long getElapsedSecs() { return contestTime.getElapsedSecs(); } + /** + * Returns the time at which the contest was first started, or null if the contest has never + * been started. Note that the returned time is never affected by any "pause" operations + * which occur after the contest has been first started. + */ + public Calendar getContestStartTime() { + return contestTime.getContestStartTime(); + } + public boolean isContestClockRunning() { return contestTime.isContestRunning(); } From 1b901ce7a8d093b6bdd9a36fb9e6e9b52ab45fe5 Mon Sep 17 00:00:00 2001 From: John Clevenger Date: Wed, 18 Dec 2024 15:38:29 -0800 Subject: [PATCH 080/141] i1027: add support for URL /contestclock to WTI-API server. --- .../main/controllers/ContestController.java | 76 +++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/projects/WTI-API/src/main/controllers/ContestController.java b/projects/WTI-API/src/main/controllers/ContestController.java index 59942107f..d8a747a28 100644 --- a/projects/WTI-API/src/main/controllers/ContestController.java +++ b/projects/WTI-API/src/main/controllers/ContestController.java @@ -25,6 +25,7 @@ import config.ServerInit; import edu.csus.ecs.pc2.api.IClarification; import edu.csus.ecs.pc2.api.IClient; +import edu.csus.ecs.pc2.api.IContestClock; import edu.csus.ecs.pc2.api.IJudgement; import edu.csus.ecs.pc2.api.ILanguage; import edu.csus.ecs.pc2.api.IProblem; @@ -48,6 +49,7 @@ import io.swagger.annotations.ApiResponses; import io.swagger.annotations.Authorization; import models.ClarificationModel; +import models.ContestClockModel; import models.LanguageModel; import models.ProblemModel; import models.ServerErrorResponseModel; @@ -411,6 +413,80 @@ public Response isRunning( .type(MediaType.APPLICATION_JSON).build(); } + + /*** + * This method returns a "ContestClock" object encapsulating the current state of the PC2 contest clock + * as maintained by the PC2 class "ContestTime". + * It first checks to see that the user (team) specified by the received "key" is currently logged in. If so, the method + * returns the current contest time information, obtained from the PC2 {@link ServerConnection} for the team. + * + * @param key a String containing a key which uniquely identifies the team making the request. + * The value of "key" is obtained from the HTTP header parameter "team_id". + * + * @return Response of: + * 401 (unauthorized) if team's credentials are incorrect (i.e. the team is not logged in or is not allowed to make such a request); + * 500 (INTERNAL_SERVER_ERROR) if an error occurs in fetching the requested data from the PC2 server; + * otherwise 200 (OK) and a JSON string containing a {@link ContestClock} is returned. + */ + @Path("/contestclock") + @GET + @ApiOperation(value = "contestclock", + notes = "Gets the PC2 contest clock (time info).") + @ApiResponses({ + @ApiResponse(code = 200, message = "Returns a contest clock object.", response = ContestClockModel.class), + @ApiResponse(code = 401, message = "Returned if invalid credentials are supplied", response = ServerErrorResponseModel.class) + }) + + public Response contestClock(@ApiParam(value="token used by logged in users to access teams information", + required = true) @HeaderParam("team_id")String key) { + + ServerConnection userInformation = connections.get(key); + + //verify the user is logged in + try { + // make sure we have connection information for this user (i.e. that the user is logged in) + if (userInformation == null) { + throw new NotLoggedInException(); + } + } catch (NotLoggedInException e1) { + return Response.status(Response.Status.UNAUTHORIZED) + .entity(new ServerErrorResponseModel(Response.Status.UNAUTHORIZED, "Unauthorized user request")) + .type(MediaType.APPLICATION_JSON).build(); + } + + //the ContestClock object to be returned in the response to the HTTP request + ContestClockModel returnableContestClock ; + + try { + //get the contest clock from the PC2 Server via the PC2 API ServerConnection + IContestClock contestClock = userInformation.getContest().getContestClock(); + + //retrieve the relevant fields from the PC2 contest clock + boolean isRunning = contestClock.isContestClockRunning(); + long contestLengthInSecs = contestClock.getContestLengthSecs(); + long elapsedSecs = contestClock.getElapsedSecs(); + long wallClockStartTime = contestClock.getContestStartTime().getTimeInMillis(); + + returnableContestClock = new ContestClockModel(isRunning,contestLengthInSecs, elapsedSecs, wallClockStartTime); + + } + catch(NotLoggedInException e) { + return Response.status(Response.Status.UNAUTHORIZED) + .entity(new ServerErrorResponseModel(Response.Status.UNAUTHORIZED, "Unauthorized user request")) + .type(MediaType.APPLICATION_JSON).build(); + } + catch(NullPointerException e) { + logger.severe(e.getMessage()); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity(new ServerErrorResponseModel(Response.Status.INTERNAL_SERVER_ERROR, "NullPointerException in ContestController.clarifications()")) + .type(MediaType.APPLICATION_JSON).build(); + } + + return Response.ok() + .entity(returnableContestClock) + .type(MediaType.APPLICATION_JSON).build(); + } + /*** * This method returns a list of all the clarifications in the PC^2 contest submitted by the specified team. * It first checks to see that the user (team) specified by the received "key" is currently logged in and that the From 6e88d1e2f0f325cf5a3f3998bbc83d4dfd0d4abe Mon Sep 17 00:00:00 2001 From: John Clevenger Date: Wed, 18 Dec 2024 15:42:03 -0800 Subject: [PATCH 081/141] i1027: fix minor typo in javadoc comment. --- src/edu/csus/ecs/pc2/api/IContest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/edu/csus/ecs/pc2/api/IContest.java b/src/edu/csus/ecs/pc2/api/IContest.java index 7748ef0db..c920cd73a 100644 --- a/src/edu/csus/ecs/pc2/api/IContest.java +++ b/src/edu/csus/ecs/pc2/api/IContest.java @@ -299,7 +299,7 @@ public interface IContest { /** * Get an {@link IContestClock} object containing contest time-related information. * The {@link IContestClock} object can be queried for values such as the amount of - * time elasped so far in the contest, the amount of time remaining in the contest, and + * time elapsed so far in the contest, the amount of time remaining in the contest, and * whether the contest clock is currently "paused" or not. *

* Note that the {@link IContestClock} object returned by the current implementation of From 400ae36ee1111d553f3ec0bb87a5d47ad45cb0e4 Mon Sep 17 00:00:00 2001 From: John Clevenger Date: Wed, 18 Dec 2024 15:51:06 -0800 Subject: [PATCH 082/141] i1027: add elapsedTimePipe to core/services -- an Angular "Pipe" that... can be used to transform Date objects into strings displaying day/hours/minutes/seconds. --- .../src/app/modules/core/core.module.ts | 3 +- .../core/services/elapsedTimePipe.service.ts | 35 ++++++++++++++----- 2 files changed, 27 insertions(+), 11 deletions(-) diff --git a/projects/WTI-UI/src/app/modules/core/core.module.ts b/projects/WTI-UI/src/app/modules/core/core.module.ts index 6f353f04d..80cd5abe7 100644 --- a/projects/WTI-UI/src/app/modules/core/core.module.ts +++ b/projects/WTI-UI/src/app/modules/core/core.module.ts @@ -94,8 +94,7 @@ export function WebsocketServiceFactory(injector: Injector, ], imports: [ HttpClientModule, - SharedModule, - CommonModule + SharedModule ], }) export class CoreModule { } diff --git a/projects/WTI-UI/src/app/modules/core/services/elapsedTimePipe.service.ts b/projects/WTI-UI/src/app/modules/core/services/elapsedTimePipe.service.ts index ddaee036b..ad3dc7287 100644 --- a/projects/WTI-UI/src/app/modules/core/services/elapsedTimePipe.service.ts +++ b/projects/WTI-UI/src/app/modules/core/services/elapsedTimePipe.service.ts @@ -1,20 +1,27 @@ import { Pipe, PipeTransform } from '@angular/core'; -//coded generated by Google AI -//Explanation: -//The pipe takes a Date or a timestamp as input. +//This class defines a "pipe" which accepts a "Date" object and returns +// a formatted string representing the number of days/hours/minutes/seconds that have +// elapsed between the specified Date and "now". +//The "pipe" is suitable for use, for example, in an Angular HTML template (see app-header.component.html). +//It is based on code generated by Google AI. + //It calculates the difference in milliseconds between the given date and the current time. -//Then, it extracts days, hours, minutes, and seconds from the difference. -//Finally, it formats the output string. +//Then it extracts days, hours, minutes, and seconds from the difference. +//Finally, it formats the output string. "Days" are only included in the output if they are greater than zero. @Pipe({ - name: 'elapsedTime' + name: 'elapsedTime', + standalone: true }) export class ElapsedTimePipe implements PipeTransform { - transform(value: Date | number): string { + /** + Returns a string representing the elapsed time between the input Date and "now". + */ + transform(startDate: Date): string { const now = new Date().getTime(); - const then = new Date(value).getTime(); + const then = startDate.getTime(); const diff = now - then; @@ -23,6 +30,16 @@ export class ElapsedTimePipe implements PipeTransform { const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60)); const seconds = Math.floor((diff % (1000 * 60)) / 1000); - return `${days}d ${hours}h ${minutes}m ${seconds}s`; + const formattedHours = hours.toString().padStart(2, '0'); + const formattedMinutes = minutes.toString().padStart(2, '0'); + const formattedSeconds = seconds.toString().padStart(2, '0'); + + + if (days > 0) { + return `${days}d ${formattedHours}:${formattedMinutes}:${formattedSeconds}`; + } else { + return `${formattedHours}:${formattedMinutes}:${formattedSeconds}`; + } + } } \ No newline at end of file From a38ade14080cc978b2d50d4d0ee818003b139924 Mon Sep 17 00:00:00 2001 From: John Clevenger Date: Wed, 18 Dec 2024 15:56:45 -0800 Subject: [PATCH 083/141] i1027: clean up app-header HTML to display elapsed time in two ways: - a count of the number of elapsed seconds (planned for removal), and - a string showing days plus hh:mm:ss (the real plan). --- .../app-header/app-header.component.html | 27 +++---------------- 1 file changed, 4 insertions(+), 23 deletions(-) diff --git a/projects/WTI-UI/src/app/modules/shared/components/app-header/app-header.component.html b/projects/WTI-UI/src/app/modules/shared/components/app-header/app-header.component.html index ddb2acfa7..132082b19 100644 --- a/projects/WTI-UI/src/app/modules/shared/components/app-header/app-header.component.html +++ b/projects/WTI-UI/src/app/modules/shared/components/app-header/app-header.component.html @@ -7,29 +7,10 @@

Elapsed:
- -
{{ elapsedSecs }}
- - -
{{ getElapsedTimeAsDate() | elapsedTime }}
- - - - - - - - + {{ elapsedSecs }}
+ + {{ getStartDate() | elapsedTime }}
From 7f6b45e577de0d720f2e9afde6d99b4e0ca98f4b Mon Sep 17 00:00:00 2001 From: John Clevenger Date: Fri, 20 Dec 2024 18:16:28 -0800 Subject: [PATCH 084/141] i1027: minor doc (comments) added. --- projects/WTI-API/src/main/controllers/ContestController.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/projects/WTI-API/src/main/controllers/ContestController.java b/projects/WTI-API/src/main/controllers/ContestController.java index d8a747a28..53aea1dbd 100644 --- a/projects/WTI-API/src/main/controllers/ContestController.java +++ b/projects/WTI-API/src/main/controllers/ContestController.java @@ -467,8 +467,9 @@ public Response contestClock(@ApiParam(value="token used by logged in users to a long elapsedSecs = contestClock.getElapsedSecs(); long wallClockStartTime = contestClock.getContestStartTime().getTimeInMillis(); + //construct a ContestClock containing the PC2 Server clock values returnableContestClock = new ContestClockModel(isRunning,contestLengthInSecs, elapsedSecs, wallClockStartTime); - + } catch(NotLoggedInException e) { return Response.status(Response.Status.UNAUTHORIZED) @@ -482,6 +483,7 @@ public Response contestClock(@ApiParam(value="token used by logged in users to a .type(MediaType.APPLICATION_JSON).build(); } + //return the PC2 ContestClock, mapped into JSON return Response.ok() .entity(returnableContestClock) .type(MediaType.APPLICATION_JSON).build(); From b1ef34675bda37d06d30228fd946b7cb830e00d6 Mon Sep 17 00:00:00 2001 From: John Clevenger Date: Fri, 20 Dec 2024 18:18:14 -0800 Subject: [PATCH 085/141] i1027: added newly-required getContestClock() method to mock service. --- .../src/app/modules/core/services/contest.mock.service.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/projects/WTI-UI/src/app/modules/core/services/contest.mock.service.ts b/projects/WTI-UI/src/app/modules/core/services/contest.mock.service.ts index a973cd620..b069dbee6 100644 --- a/projects/WTI-UI/src/app/modules/core/services/contest.mock.service.ts +++ b/projects/WTI-UI/src/app/modules/core/services/contest.mock.service.ts @@ -5,6 +5,7 @@ import { ContestLanguage } from '../models/contest-language'; import { ContestProblem } from '../models/contest-problem'; import { Clarification } from '../models/clarification'; import { DEBUG_MODE } from 'src/constants'; +import { ContestClock } from '../models/contest-clock'; @Injectable() export class ContestMockService extends IContestService { @@ -86,6 +87,12 @@ export class ContestMockService extends IContestService { return of(true); } + getContestClock(): Observable { + return of( + //return a contest that is running, last 5 hours, has been running one hour, and started on December 18, 2024 + {isRunning:'true', contestLengthSecs:'18000', elapsedSecs:'3600', wallClockStartTime:'1734593847'}); + } + getStandings(): Observable { //TODO: this method needs to return a legitimate (mock) team standing array! From ce3f22ea23e625027eb96ffe1f4d128852744508 Mon Sep 17 00:00:00 2001 From: John Clevenger Date: Fri, 20 Dec 2024 18:23:54 -0800 Subject: [PATCH 086/141] i1027: added fetch of PC2 contest clock upon successful login. --- .../login-page/login-page.component.ts | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/projects/WTI-UI/src/app/modules/login/components/login-page/login-page.component.ts b/projects/WTI-UI/src/app/modules/login/components/login-page/login-page.component.ts index ea6b5ca0c..e2ace87ad 100644 --- a/projects/WTI-UI/src/app/modules/login/components/login-page/login-page.component.ts +++ b/projects/WTI-UI/src/app/modules/login/components/login-page/login-page.component.ts @@ -10,6 +10,7 @@ import { Router } from '@angular/router'; import { IContestService } from 'src/app/modules/core/abstract-services/i-contest.service'; import { AppTitleService } from 'src/app/modules/core/services/app-title.service'; import { DEBUG_MODE } from 'src/constants'; +import { ContestClock } from 'src/app/modules/core/models/contest-clock'; /* LoginPageComponent is the initial page displayed by the WTI-UI Single-Page-Application (SPA). @@ -40,6 +41,7 @@ export class LoginPageComponent implements OnInit, OnDestroy { formGroup: FormGroup; invalidCreds = false; loginStarted = false; + contestClock: ContestClock = new ContestClock(); constructor(private _formBuilder: FormBuilder, private _authService: AuthService, @@ -135,6 +137,53 @@ export class LoginPageComponent implements OnInit, OnDestroy { this.invalidCreds = true; this.loginStarted = false; }); + + //get the actual contest clock info from the PC2 server via the Contest Service (which gets it via the WTI Server and its PC2 API) + if (DEBUG_MODE) { + console.log ("LoginPageComponent.onSubmit(): invoking ContestService.getContestClock() and subscribing to wait for response.") + } + this._contestService.getContestClock() + .subscribe( + (data: any) => { + if (DEBUG_MODE) { + console.log ("LoginPageComponent.onSubmit(): got subscription callback from getContestClock()"); + } + if (!data) { + console.error ("Unable to get ContestClock from PC2 API via ContestService!"); + } else { + if (DEBUG_MODE) { + console.log ("LoginPageComponent.onSubmit(): data object returned from getContestClock():"); + console.log (data); + } + + //copy the data fields received from the PC2 Server (via the WTI-API) into the local ContestClock object + this.contestClock.isRunning = data.running ; + this.contestClock.contestLengthSecs = data.contestLengthInSecs ; + this.contestClock.elapsedSecs = data.elapsedSecs ; + this.contestClock.wallClockStartTime = data.wallClockStartTime ; + + if (DEBUG_MODE) { + console.log (" Contest Clock values:"); + console.log (" isRunning = ", this.contestClock.isRunning); + console.log (" contestLengthSecs = ", this.contestClock.contestLengthSecs); + console.log (" elapsedSecs = ", this.contestClock.elapsedSecs); + console.log (" wallClockStartTime = ", this.contestClock.wallClockStartTime); + } + } + }, + (error: any) => { + console.error("LoginPageComponent.onSubmit(): getContestClock() subscription callback error:"); + console.error (error); + } + ); + + }, (error: any) => { + if (DEBUG_MODE) { + console.error ("AuthService.login() subscription returned error:", error) ; + } + this.invalidCreds = true; + this.loginStarted = false; + }); } private buildForm(): void { From 66b40aebe520a8f6d4602b73c948bc07ea5eb6e6 Mon Sep 17 00:00:00 2001 From: John Clevenger Date: Sat, 21 Dec 2024 15:13:42 -0800 Subject: [PATCH 087/141] i1027: removed no-longer-needed debugging; reformat code for readability --- .../login-page/login-page.component.ts | 42 +++++++++---------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/projects/WTI-UI/src/app/modules/login/components/login-page/login-page.component.ts b/projects/WTI-UI/src/app/modules/login/components/login-page/login-page.component.ts index e2ace87ad..8d257fae4 100644 --- a/projects/WTI-UI/src/app/modules/login/components/login-page/login-page.component.ts +++ b/projects/WTI-UI/src/app/modules/login/components/login-page/login-page.component.ts @@ -99,18 +99,30 @@ export class LoginPageComponent implements OnInit, OnDestroy { this._authService.login(loginCreds) .pipe(takeUntil(this._unsubscribe)) .subscribe((result: TeamsLoginResponse) => { +<<<<<<< Upstream, based on c4b09354e015df5ecedbdcd004dea9810eecc455 if (DEBUG_MODE) { console.log ("Received callback from subscribing to AuthService.login();" ) ; console.log (" Invoking AuthService.completeLogin()") ; } +======= + +>>>>>>> d55fa7f i1027: removed no-longer-needed debugging; reformat code for readability this._authService.completeLogin(result.teamId, result.teamName); +<<<<<<< Upstream, based on c4b09354e015df5ecedbdcd004dea9810eecc455 if (DEBUG_MODE) { console.log (" Invoking WebsocketService.startWebsocket()") ; } +======= + +>>>>>>> d55fa7f i1027: removed no-longer-needed debugging; reformat code for readability this._websocketService.startWebsocket(); +<<<<<<< Upstream, based on c4b09354e015df5ecedbdcd004dea9810eecc455 if (DEBUG_MODE) { console.log (" invoking ContestService.getisContestRunning() and subscribing to the result") ; } +======= + +>>>>>>> d55fa7f i1027: removed no-longer-needed debugging; reformat code for readability this._contestService.getIsContestRunning() .subscribe((val: boolean) => { if (DEBUG_MODE) { @@ -139,50 +151,38 @@ export class LoginPageComponent implements OnInit, OnDestroy { }); //get the actual contest clock info from the PC2 server via the Contest Service (which gets it via the WTI Server and its PC2 API) - if (DEBUG_MODE) { - console.log ("LoginPageComponent.onSubmit(): invoking ContestService.getContestClock() and subscribing to wait for response.") - } this._contestService.getContestClock() .subscribe( (data: any) => { - if (DEBUG_MODE) { - console.log ("LoginPageComponent.onSubmit(): got subscription callback from getContestClock()"); - } if (!data) { console.error ("Unable to get ContestClock from PC2 API via ContestService!"); - } else { - if (DEBUG_MODE) { - console.log ("LoginPageComponent.onSubmit(): data object returned from getContestClock():"); - console.log (data); - } - + } else { //copy the data fields received from the PC2 Server (via the WTI-API) into the local ContestClock object this.contestClock.isRunning = data.running ; this.contestClock.contestLengthSecs = data.contestLengthInSecs ; this.contestClock.elapsedSecs = data.elapsedSecs ; this.contestClock.wallClockStartTime = data.wallClockStartTime ; - - if (DEBUG_MODE) { - console.log (" Contest Clock values:"); - console.log (" isRunning = ", this.contestClock.isRunning); - console.log (" contestLengthSecs = ", this.contestClock.contestLengthSecs); - console.log (" elapsedSecs = ", this.contestClock.elapsedSecs); - console.log (" wallClockStartTime = ", this.contestClock.wallClockStartTime); - } } }, (error: any) => { - console.error("LoginPageComponent.onSubmit(): getContestClock() subscription callback error:"); + console.error("LoginPageComponent.onSubmit(): getContestClock() subscription callback error: "); console.error (error); } ); }, (error: any) => { +<<<<<<< Upstream, based on c4b09354e015df5ecedbdcd004dea9810eecc455 if (DEBUG_MODE) { console.error ("AuthService.login() subscription returned error:", error) ; } this.invalidCreds = true; this.loginStarted = false; +======= + console.error ("LoginPageComponent.onSubmit(): AuthService.login() subscription callback error: "); + console.error(error); + this.invalidCreds = true; + this.loginStarted = false; +>>>>>>> d55fa7f i1027: removed no-longer-needed debugging; reformat code for readability }); } From 5098a2d24df73ee8ad3875ce2b904e814f7fd584 Mon Sep 17 00:00:00 2001 From: John Clevenger Date: Sat, 21 Dec 2024 16:17:21 -0800 Subject: [PATCH 088/141] i1027: rename time display pipe, rework to accept numSeconds --- .../core/services/displayTimePipe.service.ts | 42 +++++++++++++++++ .../core/services/elapsedTimePipe.service.ts | 45 ------------------- 2 files changed, 42 insertions(+), 45 deletions(-) create mode 100644 projects/WTI-UI/src/app/modules/core/services/displayTimePipe.service.ts delete mode 100644 projects/WTI-UI/src/app/modules/core/services/elapsedTimePipe.service.ts diff --git a/projects/WTI-UI/src/app/modules/core/services/displayTimePipe.service.ts b/projects/WTI-UI/src/app/modules/core/services/displayTimePipe.service.ts new file mode 100644 index 000000000..5082f3ac9 --- /dev/null +++ b/projects/WTI-UI/src/app/modules/core/services/displayTimePipe.service.ts @@ -0,0 +1,42 @@ +import { Pipe, PipeTransform } from '@angular/core'; +import { DEBUG_MODE } from 'src/constants' + +//This class defines a "pipe" which accepts a time, represented as a number of seconds, and returns +// a formatted string representing the number of days/hours/minutes/seconds represented by the input number of seconds. +//"Days" are only included in the output if they are greater than zero. +//The "pipe" is suitable for use, for example, in an Angular HTML template wanting to display an elapsed number of seconds +// in terms of hours/mins/secs (see app-header.component.html). + +@Pipe({ + name: 'displayTime', + standalone: true +}) +export class DisplayTimePipe implements PipeTransform { + + /** + Returns a formatted string representing the specified elapsed time (in seconds). + */ + transform(numSecs: number): string { + + const days = Math.floor(numSecs / (60 * 60 * 24)); + const hours = Math.floor((numSecs % (60 * 60 * 24)) / (60 * 60)); + const minutes = Math.floor((numSecs % (60 * 60)) / (60)); + const seconds = Math.floor(numSecs % 60); + + if (DEBUG_MODE) { + console.log("DisplayTimePipe.transform(): computed ", numSecs, " as ", days, "d ", hours, "h ", minutes, "m ", seconds, "s"); + } + + const formattedHours = hours.toString().padStart(2, '0'); + const formattedMinutes = minutes.toString().padStart(2, '0'); + const formattedSeconds = seconds.toString().padStart(2, '0'); + + + if (days > 0) { + return `${days}d ${formattedHours}:${formattedMinutes}:${formattedSeconds}`; + } else { + return `${formattedHours}:${formattedMinutes}:${formattedSeconds}`; + } + + } +} \ No newline at end of file diff --git a/projects/WTI-UI/src/app/modules/core/services/elapsedTimePipe.service.ts b/projects/WTI-UI/src/app/modules/core/services/elapsedTimePipe.service.ts deleted file mode 100644 index ad3dc7287..000000000 --- a/projects/WTI-UI/src/app/modules/core/services/elapsedTimePipe.service.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { Pipe, PipeTransform } from '@angular/core'; - -//This class defines a "pipe" which accepts a "Date" object and returns -// a formatted string representing the number of days/hours/minutes/seconds that have -// elapsed between the specified Date and "now". -//The "pipe" is suitable for use, for example, in an Angular HTML template (see app-header.component.html). -//It is based on code generated by Google AI. - -//It calculates the difference in milliseconds between the given date and the current time. -//Then it extracts days, hours, minutes, and seconds from the difference. -//Finally, it formats the output string. "Days" are only included in the output if they are greater than zero. - -@Pipe({ - name: 'elapsedTime', - standalone: true -}) -export class ElapsedTimePipe implements PipeTransform { - - /** - Returns a string representing the elapsed time between the input Date and "now". - */ - transform(startDate: Date): string { - const now = new Date().getTime(); - const then = startDate.getTime(); - - const diff = now - then; - - const days = Math.floor(diff / (1000 * 60 * 60 * 24)); - const hours = Math.floor((diff % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)); - const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60)); - const seconds = Math.floor((diff % (1000 * 60)) / 1000); - - const formattedHours = hours.toString().padStart(2, '0'); - const formattedMinutes = minutes.toString().padStart(2, '0'); - const formattedSeconds = seconds.toString().padStart(2, '0'); - - - if (days > 0) { - return `${days}d ${formattedHours}:${formattedMinutes}:${formattedSeconds}`; - } else { - return `${formattedHours}:${formattedMinutes}:${formattedSeconds}`; - } - - } -} \ No newline at end of file From f446c225c0cdc7dc5e7ee8d426e6dc214ca36f0e Mon Sep 17 00:00:00 2001 From: John Clevenger Date: Mon, 23 Dec 2024 17:34:32 -0800 Subject: [PATCH 089/141] i1027: add WTI-UI model for contest clock. --- .../src/app/modules/core/models/contest-clock.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 projects/WTI-UI/src/app/modules/core/models/contest-clock.ts diff --git a/projects/WTI-UI/src/app/modules/core/models/contest-clock.ts b/projects/WTI-UI/src/app/modules/core/models/contest-clock.ts new file mode 100644 index 000000000..474f7c012 --- /dev/null +++ b/projects/WTI-UI/src/app/modules/core/models/contest-clock.ts @@ -0,0 +1,12 @@ +/** This class encapsulates the PC2 notion of a "Contest Clock" -- a collection of items which together provide + * information on the state of state of time in the contest -- what time the contest started, how long the contest runs, + * whether the contest is running or paused right now, and how much time has elapsed on the contest clock since the + * contest started (note that this latter value does NOT include any real time which passed while the contest was "paused"). + * Essentially, this model class corresponds to the PC2 server class "ContestTime". + */ +export class ContestClock { + isRunning: string = ''; //string 'true' or 'false' + contestLengthSecs: string = ''; //string representing an integer number of seconds + elapsedSecs: string = ''; //string representing total time in seconds that the contest has been running -- does NOT include time during any "pauses" + wallClockStartTime: string = ''; //string representing unix timestamp when the contest actually started -- msec since the Epoch. Does not change due to "pauses" +} \ No newline at end of file From 3ff1b6ef02fe4b23064971e44332d3a56c6299ca Mon Sep 17 00:00:00 2001 From: John Clevenger Date: Mon, 23 Dec 2024 17:35:58 -0800 Subject: [PATCH 090/141] i1027: added Timer class to enable/disable incr of display times. --- .../core/services/contestTimer.service.ts | 80 +++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 projects/WTI-UI/src/app/modules/core/services/contestTimer.service.ts diff --git a/projects/WTI-UI/src/app/modules/core/services/contestTimer.service.ts b/projects/WTI-UI/src/app/modules/core/services/contestTimer.service.ts new file mode 100644 index 000000000..368978acf --- /dev/null +++ b/projects/WTI-UI/src/app/modules/core/services/contestTimer.service.ts @@ -0,0 +1,80 @@ +import { DEBUG_MODE } from 'src/constants' + +export class ContestTimerService { + + elapsedSecs = 0 ; //how many seconds the contest has been running (doesn't include any 'paused' time) + remainingSecs = 18000 ; //how many seconds remain in the contest (default initial value: 5 hours = 18,000 secs) + intervalId: ReturnType ; //the id of the JavaScript "interval" used to generate timer ticks + isTimerRunning: boolean = false; + + constructor() { + if (DEBUG_MODE) { + console.log ("Executing ContestTimerService constructor"); + } + } + + getElapsedSecs(): number { + return this.elapsedSecs; + } + + getRemainingSecs(): number { + return this.remainingSecs; + } + + setElapsedSecs(newElapsedSecs: number) { + if (this.intervalId) { + console.error ("ContestTimerService.setElapsedSecs(): cannot set elapsed seconds while Timer is running; stop the timer first.") + return; + } else { + this.elapsedSecs = newElapsedSecs; + } + } + + setRemainingSecs(newRemainingSecs: number) { + if (this.intervalId) { + console.error ("ContestTimerService.setRemainingSecs(): cannot set remaining seconds while Timer is running; stop the timer first.") + return; + } else { + this.remainingSecs = newRemainingSecs>=0 ? newRemainingSecs : 0; + } + } + + startTimer() { + if (this.intervalId) { + console.error("ContestTimerService.startTimer(): call to startTimer() when timer is already running"); + return; + } else { + if (DEBUG_MODE) { + console.log ("ContestTimerService.startTimer(): starting 1-second interval timer"); + } + //Start a 1-second interval timer going, save the interval timer Id + this.intervalId = setInterval( + //execute this function at the specified interval: + () => { + //bump the elapsed & remaining counters since 1000msec (one sec) has passed + this.elapsedSecs += 1 ; + this.remainingSecs -= 1 + console.log(`Elapsed secs: ${this.elapsedSecs}; remaining secs: ${this.remainingSecs}`); + }, + 1000 // 1000 milliseconds = 1 second interval + ); + this.isTimerRunning = true; + } + } + + stopTimer() { + if (!this.intervalId) { + console.error("ContestTimerService.stopTimer(): call to stopTimer() when timer is not running"); + return; + } else { + if (DEBUG_MODE) { + console.log ("ContestTimerService.stopTimer(): stopping timer"); + } + //dispose of the existing interval timer (thus stopping it) + clearInterval(this.intervalId); + this.intervalId = undefined; + this.isTimerRunning = false; + } + } + +} From 40d6d4f552ed4a88f9cc46994020b6254ceaac8e Mon Sep 17 00:00:00 2001 From: John Clevenger Date: Wed, 25 Dec 2024 14:45:30 -0800 Subject: [PATCH 091/141] i1027: remove no-longer-used ngx-countdown. --- projects/WTI-UI/package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/projects/WTI-UI/package.json b/projects/WTI-UI/package.json index d85d926f5..86a8a1dd7 100644 --- a/projects/WTI-UI/package.json +++ b/projects/WTI-UI/package.json @@ -22,7 +22,6 @@ "@angular/platform-browser-dynamic": "^16.2.11", "@angular/router": "^16.2.11", "core-js": "^3.33.1", - "ngx-countdown": "16.0", "rxjs": "^6.5.3 || ^7.4.0", "tslib": "^1.9.0", "zone.js": "^0.13.X" From 2b844080c05ca1897af76c065c5d362f5f33371b Mon Sep 17 00:00:00 2001 From: John Clevenger Date: Wed, 25 Dec 2024 16:00:34 -0800 Subject: [PATCH 092/141] i1027: update for compatibility with PR for i871. --- .../src/app/modules/core/core.module.ts | 23 +++++++++++++++++++ .../core/services/contest.mock.service.ts | 1 + 2 files changed, 24 insertions(+) diff --git a/projects/WTI-UI/src/app/modules/core/core.module.ts b/projects/WTI-UI/src/app/modules/core/core.module.ts index 80cd5abe7..4832194e0 100644 --- a/projects/WTI-UI/src/app/modules/core/core.module.ts +++ b/projects/WTI-UI/src/app/modules/core/core.module.ts @@ -16,7 +16,11 @@ import { IWebsocketService } from './abstract-services/i-websocket.service'; import { UiHelperService } from './services/ui-helper.service'; import { SharedModule } from '../shared/shared.module'; import { DEBUG_MODE } from 'src/constants'; +<<<<<<< Upstream, based on c4b09354e015df5ecedbdcd004dea9810eecc455 import { CommonModule } from '@angular/common'; +======= +import { DisplayTimePipe } from './services/displayTimePipe.service'; +>>>>>>> 7a71e77 i1027: update for compatibility with PR for i871. export function TeamsServiceFactory(http: HttpClient) { if (DEBUG_MODE) { @@ -84,17 +88,36 @@ export function WebsocketServiceFactory(injector: Injector, providers: [ { provide: ITeamsService, useFactory: TeamsServiceFactory, deps: [HttpClient] }, { provide: IContestService, useFactory: ContestServiceFactory, deps: [HttpClient] }, + { provide: ContestService, useFactory: ContestServiceFactory, deps: [HttpClient] }, { provide: AuthService, useClass: AuthService }, { provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true }, { provide: IWebsocketService, useFactory: WebsocketServiceFactory, deps: [Injector, UiHelperService, IContestService, ITeamsService, AuthService] }, AuthGuard, //TODO: should the following two still be declared here since they are now listed in the above "deps" list? UiHelperService, +<<<<<<< Upstream, based on c4b09354e015df5ecedbdcd004dea9810eecc455 +======= + DisplayTimePipe, + //TODO: should the following two still be declared here since they are now listed in the above "deps" list? + UiHelperService, + ContestService, + //TODO: should the following two still be declared here since they are now listed in the above "deps" list? + UiHelperService, +>>>>>>> 7a71e77 i1027: update for compatibility with PR for i871. ContestService ], imports: [ HttpClientModule, +<<<<<<< Upstream, based on c4b09354e015df5ecedbdcd004dea9810eecc455 SharedModule ], +======= + SharedModule, + DisplayTimePipe + ], + exports: [ + DisplayTimePipe + ] +>>>>>>> 7a71e77 i1027: update for compatibility with PR for i871. }) export class CoreModule { } diff --git a/projects/WTI-UI/src/app/modules/core/services/contest.mock.service.ts b/projects/WTI-UI/src/app/modules/core/services/contest.mock.service.ts index b069dbee6..ddd73ad30 100644 --- a/projects/WTI-UI/src/app/modules/core/services/contest.mock.service.ts +++ b/projects/WTI-UI/src/app/modules/core/services/contest.mock.service.ts @@ -6,6 +6,7 @@ import { ContestProblem } from '../models/contest-problem'; import { Clarification } from '../models/clarification'; import { DEBUG_MODE } from 'src/constants'; import { ContestClock } from '../models/contest-clock'; +import { DEBUG_MODE } from 'src/constants'; @Injectable() export class ContestMockService extends IContestService { From c7da8c67fec987a6b0149c2b29736408ad5097f7 Mon Sep 17 00:00:00 2001 From: John Clevenger Date: Thu, 26 Dec 2024 12:06:43 -0800 Subject: [PATCH 093/141] i1027: SharedModule: remove ngx-countdown; add ContestServices. --- .../src/app/modules/shared/shared.module.ts | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/projects/WTI-UI/src/app/modules/shared/shared.module.ts b/projects/WTI-UI/src/app/modules/shared/shared.module.ts index 3e5f52b71..711126b75 100644 --- a/projects/WTI-UI/src/app/modules/shared/shared.module.ts +++ b/projects/WTI-UI/src/app/modules/shared/shared.module.ts @@ -13,11 +13,18 @@ import { MatInputModule } from '@angular/material/input'; import { MatSelectModule } from '@angular/material/select'; import { MatSnackBarModule } from '@angular/material/snack-bar'; import { AboutWtiComponent } from './components/about-wti/about-wti.component'; -import { CountdownModule } from 'ngx-countdown'; import { BrowserModule } from '@angular/platform-browser'; -import { ElapsedTimePipe } from 'src/app/modules/core/services/elapsedTimePipe.service'; +import { DisplayTimePipe } from 'src/app/modules/core/services/displayTimePipe.service'; +import { IContestService } from 'src/app/modules/core/abstract-services/i-contest.service'; +import { ContestService } from 'src/app/modules/core/services/contest.service'; +import { ContestServiceFactory } from '../core/core.module'; +import { HttpClient } from '@angular/common/http'; @NgModule({ + providers: [ + { provide: IContestService, useFactory: ContestServiceFactory, deps: [HttpClient] }, + { provide: ContestService, useFactory: ContestServiceFactory, deps: [HttpClient] }, + ], declarations: [ AppHeaderComponent, AppFooterComponent, @@ -35,9 +42,8 @@ import { ElapsedTimePipe } from 'src/app/modules/core/services/elapsedTimePipe.s MatInputModule, MatSelectModule, MatSnackBarModule, - CountdownModule, BrowserModule, - ElapsedTimePipe + DisplayTimePipe ], exports: [ AppHeaderComponent, @@ -49,8 +55,7 @@ import { ElapsedTimePipe } from 'src/app/modules/core/services/elapsedTimePipe.s MatFormFieldModule, MatInputModule, MatSelectModule, - MatSnackBarModule, - CountdownModule + MatSnackBarModule ] }) export class SharedModule { } From 7674bc81c80750732fa90eeea24b0dfb307d2cba Mon Sep 17 00:00:00 2001 From: John Clevenger Date: Thu, 26 Dec 2024 12:08:47 -0800 Subject: [PATCH 094/141] i1027: rename contestClock Subject to contestClockEvent; add debugging --- .../problem-selector/problem-selector.component.ts | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/projects/WTI-UI/src/app/modules/shared/components/problem-selector/problem-selector.component.ts b/projects/WTI-UI/src/app/modules/shared/components/problem-selector/problem-selector.component.ts index 4f2210244..910b85650 100644 --- a/projects/WTI-UI/src/app/modules/shared/components/problem-selector/problem-selector.component.ts +++ b/projects/WTI-UI/src/app/modules/shared/components/problem-selector/problem-selector.component.ts @@ -40,8 +40,18 @@ export class ProblemSelectorComponent implements OnInit, OnDestroy, ControlValue this.writeValue('general'); } - // listen for contest start/stop to show/hide contest problems - this._contestService.contestClock + //Listen for (subscribe to) contest start/stop events to show/hide contest problems. + //The contestClockEvent "Subject" (defined in IContestService) is used as a toggle to indicate when + //contest clock-related events have occured. Subscribing to it means that this component will get a + //callback whenever the contestClockEvent's "next()" method is invoked. + //Currently the contestClockEvent's "next()" method is invoked in exactly one + //place: IWebsocketService.incomingMessage(), when a message of type "contest_clock" is received by the WTI-UI from the WTI-API + //(which in turn happens when the WTI-API receives a "contest clock configuration update" notice via the PC2 API). + //The effect of all this is that this Component gets notified when "some change" has occurred in the state of the + //PC2 contest clock. This causes the Component to execute its "loadProblems()" method; that method in turn checks + //whether the contest is currently RUNNING; if so, it loads the problem names; if not, it blanks out problem names. + + this._contestService.contestClockEvent .pipe(takeUntil(this._unsubscribe)) .subscribe(_ => this.loadProblems()); } From a81166797ff79c9cebe887879024d371c142914d Mon Sep 17 00:00:00 2001 From: John Clevenger Date: Thu, 26 Dec 2024 12:16:17 -0800 Subject: [PATCH 095/141] i1027: LoginPage: move ContestClock to ContestService; add doc and debug --- .../login-page/login-page.component.ts | 21 +------------------ 1 file changed, 1 insertion(+), 20 deletions(-) diff --git a/projects/WTI-UI/src/app/modules/login/components/login-page/login-page.component.ts b/projects/WTI-UI/src/app/modules/login/components/login-page/login-page.component.ts index 8d257fae4..b33b10122 100644 --- a/projects/WTI-UI/src/app/modules/login/components/login-page/login-page.component.ts +++ b/projects/WTI-UI/src/app/modules/login/components/login-page/login-page.component.ts @@ -99,30 +99,18 @@ export class LoginPageComponent implements OnInit, OnDestroy { this._authService.login(loginCreds) .pipe(takeUntil(this._unsubscribe)) .subscribe((result: TeamsLoginResponse) => { -<<<<<<< Upstream, based on c4b09354e015df5ecedbdcd004dea9810eecc455 if (DEBUG_MODE) { console.log ("Received callback from subscribing to AuthService.login();" ) ; console.log (" Invoking AuthService.completeLogin()") ; } -======= - ->>>>>>> d55fa7f i1027: removed no-longer-needed debugging; reformat code for readability this._authService.completeLogin(result.teamId, result.teamName); -<<<<<<< Upstream, based on c4b09354e015df5ecedbdcd004dea9810eecc455 if (DEBUG_MODE) { console.log (" Invoking WebsocketService.startWebsocket()") ; } -======= - ->>>>>>> d55fa7f i1027: removed no-longer-needed debugging; reformat code for readability this._websocketService.startWebsocket(); -<<<<<<< Upstream, based on c4b09354e015df5ecedbdcd004dea9810eecc455 if (DEBUG_MODE) { console.log (" invoking ContestService.getisContestRunning() and subscribing to the result") ; } -======= - ->>>>>>> d55fa7f i1027: removed no-longer-needed debugging; reformat code for readability this._contestService.getIsContestRunning() .subscribe((val: boolean) => { if (DEBUG_MODE) { @@ -144,7 +132,7 @@ export class LoginPageComponent implements OnInit, OnDestroy { }); }, (error: any) => { if (DEBUG_MODE) { - console.log ("AuthService.login() subscription returned error.") ; + console.log ("AuthService.login() subscription returned error: ", error) ; } this.invalidCreds = true; this.loginStarted = false; @@ -171,18 +159,11 @@ export class LoginPageComponent implements OnInit, OnDestroy { ); }, (error: any) => { -<<<<<<< Upstream, based on c4b09354e015df5ecedbdcd004dea9810eecc455 if (DEBUG_MODE) { console.error ("AuthService.login() subscription returned error:", error) ; } this.invalidCreds = true; this.loginStarted = false; -======= - console.error ("LoginPageComponent.onSubmit(): AuthService.login() subscription callback error: "); - console.error(error); - this.invalidCreds = true; - this.loginStarted = false; ->>>>>>> d55fa7f i1027: removed no-longer-needed debugging; reformat code for readability }); } From 53797b56c6d19e7ba897819d94c5c5cffe07a613 Mon Sep 17 00:00:00 2001 From: John Clevenger Date: Thu, 26 Dec 2024 12:19:18 -0800 Subject: [PATCH 096/141] i1027: IWebSocketService: make root-injectable; add debug output. --- .../modules/core/abstract-services/i-websocket.service.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/projects/WTI-UI/src/app/modules/core/abstract-services/i-websocket.service.ts b/projects/WTI-UI/src/app/modules/core/abstract-services/i-websocket.service.ts index 81db1fb98..67b9d527a 100644 --- a/projects/WTI-UI/src/app/modules/core/abstract-services/i-websocket.service.ts +++ b/projects/WTI-UI/src/app/modules/core/abstract-services/i-websocket.service.ts @@ -51,11 +51,15 @@ export abstract class IWebsocketService { if (DEBUG_MODE) { console.log ("IWebsocketService.incomingMessage(): callback from ContestService.getIsContestRunning() returned '", val, "'"); console.log ("Setting ContestService.isContestRunning to '", val, "'") ; +<<<<<<< Upstream, based on c4b09354e015df5ecedbdcd004dea9810eecc455 console.log (" and invoking ContestService.contestClock.next()") ; +======= + console.log (" and invoking ContestService.contestClockEvent.next()") ; +>>>>>>> 5291696 i1027: IWebSocketService: make root-injectable; add debug output. } this._contestService.isContestRunning = val; - this._contestService.contestClock.next(); + this._contestService.contestClockEvent.next(); }); break; } From f3b8a84d74ebe58ff7d9acaa1bafad60070ef137 Mon Sep 17 00:00:00 2001 From: John Clevenger Date: Thu, 26 Dec 2024 12:23:40 -0800 Subject: [PATCH 097/141] i1027: IContestService: make root-injectable; also: - renamed contestClock to contestClockEvent; - added support for unique instant ids; - added abstract accessors for contest clock times; --- .../abstract-services/i-contest.service.ts | 42 +++++++++++++++++-- 1 file changed, 39 insertions(+), 3 deletions(-) diff --git a/projects/WTI-UI/src/app/modules/core/abstract-services/i-contest.service.ts b/projects/WTI-UI/src/app/modules/core/abstract-services/i-contest.service.ts index c905f074e..9d3d0171e 100644 --- a/projects/WTI-UI/src/app/modules/core/abstract-services/i-contest.service.ts +++ b/projects/WTI-UI/src/app/modules/core/abstract-services/i-contest.service.ts @@ -10,11 +10,41 @@ import { DEBUG_MODE } from 'src/constants'; providedIn: 'root' //forces the service to be a singleton across all app components ('root' == "root injector") }) export abstract class IContestService { + + //need to document how the "clarificationsUpdated" Subject works (see the description of contestClockEvent, below; + //this operates similarly although it is used in more places) clarificationsUpdated = new Subject(); - contestClock = new Subject(); //note that this is a WTI-UI-specific object used in conjunction with Websocket "clock" messages; - //it is NOT a "ContestClock" object in the sense of classes defined in core/models. + + //The contestClockEvent "Subject" is used as a toggle to indicate when contest clock-related events have occured. + //It is "subscribed to" by the "ProblemSelectorComponent.ngOnInit()" method (meaning, that component will get a callback + //whenever the contestClockEvent's "next()" method is invoked). The contestClockEvent's "next()" method is invoked in exactly one + //place: IWebsocketService.incomingMessage(), when a message of type "contest_clock" is received by the WTI-UI from the WTI-API + //(which in turn happens when the WTI-API receives a "contest clock configuration update" notice via the PC2 API). + //The effect of all this is that the ProblemSelectorComponent gets notified when "some change" has occurred in the state of the + //PC2 contest clock. This causes the ProblemSelectorComponent to execute its "loadProblems()" method; that method in turn checks + //whether the contest is currently RUNNING; if so, it loads the problem names; if not, it blanks out problem names. + //note that contestClockEvent is a WTI-UI-specific object used to coordinate with Websocket "clock" messages; + //it is NOT a "ContestClock" object in the sense of classes defined in core/models, nor an "Event" it PC2 terms. + contestClockEvent = new Subject(); + standingsUpdated = new Subject(); isContestRunning = false; + + //the WTI-UI representation of the PC2 Server's ContestClock (PC2 class ContestTime) + //todo: where does this get updated when the PC2 clock changes? ; + contestClock: ContestClock = new ContestClock(); + + //give each instance of IContestService a unique ID for debugging purposes + private static nextId: number = 1; + public uniqueId: number; + + + constructor () { + this.uniqueId = IContestService.nextId++; + if (DEBUG_MODE) { + console.log ("Executing IContestService constructor for unique instance ", this.uniqueId) ; + } + } //give each instance of IContestService a unique ID for debugging purposes private static nextId: number = 1; @@ -38,8 +68,14 @@ export abstract class IContestService { abstract getIsContestRunning(): Observable; - abstract getContestClock(): Observable; //note that this refers to a "ContestClock" model object, not the "contestClock" subject above + abstract getContestClock(): Observable; //note this refers to a "ContestClock" model object, not the "contestClockEvent" Subject above + abstract updateContestClock (newClock: ContestClock): void; //update the WTI-UI representation of the PC2 contest clock + + abstract getElapsedSecs(): number; + + abstract getRemainingSecs(): number; + abstract getStandings(): Observable; abstract markStandingsOutOfDate(): void; From 5060f19140a364266a2a30f4bc3e8437488048b3 Mon Sep 17 00:00:00 2001 From: John Clevenger Date: Thu, 26 Dec 2024 12:24:49 -0800 Subject: [PATCH 098/141] i1027: DisplayTimePipe: added debug output. --- .../src/app/modules/core/services/displayTimePipe.service.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/projects/WTI-UI/src/app/modules/core/services/displayTimePipe.service.ts b/projects/WTI-UI/src/app/modules/core/services/displayTimePipe.service.ts index 5082f3ac9..8307df7ad 100644 --- a/projects/WTI-UI/src/app/modules/core/services/displayTimePipe.service.ts +++ b/projects/WTI-UI/src/app/modules/core/services/displayTimePipe.service.ts @@ -23,10 +23,6 @@ export class DisplayTimePipe implements PipeTransform { const minutes = Math.floor((numSecs % (60 * 60)) / (60)); const seconds = Math.floor(numSecs % 60); - if (DEBUG_MODE) { - console.log("DisplayTimePipe.transform(): computed ", numSecs, " as ", days, "d ", hours, "h ", minutes, "m ", seconds, "s"); - } - const formattedHours = hours.toString().padStart(2, '0'); const formattedMinutes = minutes.toString().padStart(2, '0'); const formattedSeconds = seconds.toString().padStart(2, '0'); From b5dcf6a01c9cc11f2f48397888085bdd9de13481 Mon Sep 17 00:00:00 2001 From: John Clevenger Date: Thu, 26 Dec 2024 12:26:17 -0800 Subject: [PATCH 099/141] i1027: ContestTimerService: added error output when remainingTime<0 --- .../src/app/modules/core/services/contestTimer.service.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/projects/WTI-UI/src/app/modules/core/services/contestTimer.service.ts b/projects/WTI-UI/src/app/modules/core/services/contestTimer.service.ts index 368978acf..600828df7 100644 --- a/projects/WTI-UI/src/app/modules/core/services/contestTimer.service.ts +++ b/projects/WTI-UI/src/app/modules/core/services/contestTimer.service.ts @@ -35,7 +35,12 @@ export class ContestTimerService { console.error ("ContestTimerService.setRemainingSecs(): cannot set remaining seconds while Timer is running; stop the timer first.") return; } else { - this.remainingSecs = newRemainingSecs>=0 ? newRemainingSecs : 0; + if (newRemainingSecs>=0) { + this.remainingSecs = newRemainingSecs; + } else { + console.error("ContestTimerService.setRemainingSecs(): attempt to set Remaining Time to less than zero not allowed; setting to zero instead."); + this.remainingSecs = 0; + } } } From ebf4449fb375b87f299d21312e7a24d6e069782b Mon Sep 17 00:00:00 2001 From: John Clevenger Date: Thu, 26 Dec 2024 12:30:17 -0800 Subject: [PATCH 100/141] i1027: ContestService: make root-injectable; add ContestTimer; also: - add support for updating ContestClock - add debug output --- .../modules/core/services/contest.service.ts | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/projects/WTI-UI/src/app/modules/core/services/contest.service.ts b/projects/WTI-UI/src/app/modules/core/services/contest.service.ts index a2ddb8d68..b95789cab 100644 --- a/projects/WTI-UI/src/app/modules/core/services/contest.service.ts +++ b/projects/WTI-UI/src/app/modules/core/services/contest.service.ts @@ -6,6 +6,7 @@ import { HttpClient } from '@angular/common/http'; import { environment } from 'src/environments/environment'; import { ContestProblem } from '../models/contest-problem'; import { ContestClock } from '../models/contest-clock'; +import { ContestTimerService } from './contestTimer.service' ; import { Clarification } from '../models/clarification'; import { DEBUG_MODE } from 'src/constants'; @@ -17,7 +18,11 @@ export class ContestService extends IContestService { standingsAreCurrent: boolean ; cachedStandings: Observable ; + //the WTI-UI timer service which updates elapsed and remaining time when started + contestTimer: ContestTimerService = new ContestTimerService() ; + constructor(private _httpClient: HttpClient) { +<<<<<<< Upstream, based on c4b09354e015df5ecedbdcd004dea9810eecc455 super(); if (DEBUG_MODE) { @@ -25,6 +30,13 @@ export class ContestService extends IContestService { } this.standingsAreCurrent = false; +======= + super(); + if (DEBUG_MODE) { + console.log ("Executing ContestService constructor; instance ID = ", this.uniqueId) ; + } + this.standingsAreCurrent = false; +>>>>>>> ab9b09a i1027: ContestService: make root-injectable; add ContestTimer; also: } getLanguages(): Observable { @@ -52,6 +64,7 @@ export class ContestService extends IContestService { /** This method returns an Observable "ContestClock" object -- a WTI-UI model corresponding to the PC2 "ContestTime" class, * which itself encapsulates the "contest clock" on the PC2 server. + * TODO: should this method return the local copy of the ContestClock? */ getContestClock(): Observable { return this._httpClient.get(`${environment.baseUrl}/contest/contestclock`); @@ -82,5 +95,47 @@ export class ContestService extends IContestService { getStandingsAreCurrentFlag() : boolean { return this.standingsAreCurrent ; } + + updateContestClock (newContestClock: ContestClock) { + //save the new clock + this.contestClock = newContestClock; + + //pull the relevant values out of the new clock + let timerShouldBeStarted = this.contestClock.isRunning === 'true'; + let elapsedSecs = parseInt(this.contestClock.elapsedSecs); + let contestLengthSecs = parseInt(this.contestClock.contestLengthSecs); + let remainingSecs = contestLengthSecs - elapsedSecs; + + //shut off timer if it is running (otherwise we can't update the elapsed/remaining time values) + if (this.contestTimer.isTimerRunning) { + this.contestTimer.stopTimer(); + } + + //store the new contest time values in the Timer + this.contestTimer.setElapsedSecs(elapsedSecs); + this.contestTimer.setRemainingSecs(remainingSecs); + + //restart the timer if the new contest clock values indicate it should be running + if (timerShouldBeStarted) { + this.contestTimer.startTimer(); + } + } + + enableContestTimerUpdates() { + this.contestTimer.startTimer(); + } + + disableContestTimerUpdates() { + this.contestTimer.stopTimer(); + } + + getElapsedSecs(): number { + return parseInt(this.contestClock.elapsedSecs) ; + } + + getRemainingSecs(): number { + let remainingSecs = parseInt(this.contestClock.contestLengthSecs) - parseInt(this.contestClock.elapsedSecs) ; + return remainingSecs ; + } } From 64af0d2a8672cf3a1b144384802519a1a94e2db0 Mon Sep 17 00:00:00 2001 From: John Clevenger Date: Thu, 26 Dec 2024 12:31:10 -0800 Subject: [PATCH 101/141] i1027: ContestMockService: add mock support for updating contest clock --- .../modules/core/services/contest.mock.service.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/projects/WTI-UI/src/app/modules/core/services/contest.mock.service.ts b/projects/WTI-UI/src/app/modules/core/services/contest.mock.service.ts index ddd73ad30..54bfa19dd 100644 --- a/projects/WTI-UI/src/app/modules/core/services/contest.mock.service.ts +++ b/projects/WTI-UI/src/app/modules/core/services/contest.mock.service.ts @@ -94,6 +94,18 @@ export class ContestMockService extends IContestService { {isRunning:'true', contestLengthSecs:'18000', elapsedSecs:'3600', wallClockStartTime:'1734593847'}); } + updateContestClock (newContestClock: ContestClock) { + //nothing to do -- this is a mock service + } + + getElapsedSecs(): number { + return 3600; //mock: 1 hour has elapsed + } + + getRemainingSecs(): number { + return 18000 - 3600 ; //mock: 5 hour contest with one hour elapsed + } + getStandings(): Observable { //TODO: this method needs to return a legitimate (mock) team standing array! From 133ef89399831effd4eb2e282b3957ad3f44e316 Mon Sep 17 00:00:00 2001 From: John Clevenger Date: Thu, 26 Dec 2024 12:34:18 -0800 Subject: [PATCH 102/141] i1027: AppComponent: add support matching i871-F5-in-WTI. --- projects/WTI-UI/src/app/app.component.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/projects/WTI-UI/src/app/app.component.ts b/projects/WTI-UI/src/app/app.component.ts index b88b48c09..8be3563d0 100644 --- a/projects/WTI-UI/src/app/app.component.ts +++ b/projects/WTI-UI/src/app/app.component.ts @@ -159,7 +159,11 @@ export class AppComponent implements OnInit { console.log (this._contestService); } this._contestService.isContestRunning = val; +<<<<<<< Upstream, based on c4b09354e015df5ecedbdcd004dea9810eecc455 this._contestService.contestClock.next(); +======= + this._contestService.contestClockEvent.next(); +>>>>>>> c622bdd i1027: AppComponent: add support matching i871-F5-in-WTI. }); //get the most recently saved Option values from sessionStorage From 67fa13a352f8c38f62b6b3ec4e1c6fd0faed749b Mon Sep 17 00:00:00 2001 From: John Clevenger Date: Thu, 26 Dec 2024 12:37:53 -0800 Subject: [PATCH 103/141] i1027: moved timer support out of AppHeader (into ContestService); also: replaced use of ngx-countdown for remaining time with ContestService timers. --- .../app-header/app-header.component.html | 12 ++++--- .../app-header/app-header.component.ts | 33 ++++++++----------- 2 files changed, 21 insertions(+), 24 deletions(-) diff --git a/projects/WTI-UI/src/app/modules/shared/components/app-header/app-header.component.html b/projects/WTI-UI/src/app/modules/shared/components/app-header/app-header.component.html index 132082b19..4f3354b3a 100644 --- a/projects/WTI-UI/src/app/modules/shared/components/app-header/app-header.component.html +++ b/projects/WTI-UI/src/app/modules/shared/components/app-header/app-header.component.html @@ -7,10 +7,10 @@
Elapsed:
- {{ elapsedSecs }}
- - {{ getStartDate() | elapsedTime }} + {{ getElapsedSecs() }}
+ + {{ getElapsedSecs() | displayTime }}
@@ -27,7 +27,9 @@
Remaining:
- + + {{ getRemainingSecs() | displayTime }} +
diff --git a/projects/WTI-UI/src/app/modules/shared/components/app-header/app-header.component.ts b/projects/WTI-UI/src/app/modules/shared/components/app-header/app-header.component.ts index 4fcca263a..90373c247 100644 --- a/projects/WTI-UI/src/app/modules/shared/components/app-header/app-header.component.ts +++ b/projects/WTI-UI/src/app/modules/shared/components/app-header/app-header.component.ts @@ -1,6 +1,7 @@ import { Component } from '@angular/core'; import { AuthService } from 'src/app/modules/core/auth/auth.service'; -import { ElapsedTimePipe } from 'src/app/modules/core/services/elapsedTimePipe.service'; +import { IContestService } from 'src/app/modules/core/abstract-services/i-contest.service'; +import { DEBUG_MODE } from 'src/constants' @Component({ selector: 'app-header', @@ -26,26 +27,20 @@ export class AppHeaderComponent { return teamId; } - elapsedSecs = 0 ; - - getElapsedTimeAsDate(): Date { - let newDate = new Date(this.elapsedSecs * 1000); - console.log(newDate.toString()); - return newDate ; + constructor(private _authService: AuthService, private _contestService: IContestService) { + if (DEBUG_MODE ) { + console.log ("Executing AppHeaderComponent constructor") + } } - constructor(private _authService: AuthService) { - - setInterval( - //execute this function at the specified interval: - () => { - //add 1 second to the counter since 1000msec have passed - this.elapsedSecs += 1 ; - console.log(`Seconds: ${this.elapsedSecs}`); - }, - 1000 // 1000 milliseconds = 1 second interval - ); - + getElapsedSecs(): number { + let secs = this._contestService.getElapsedSecs() ; + return secs; + } + + getRemainingSecs(): number { + let secs = this._contestService.getRemainingSecs() ; + return secs; } } From 98ff4371d2984b6c8ffba1707b381bd3e69664a0 Mon Sep 17 00:00:00 2001 From: John Clevenger Date: Thu, 26 Dec 2024 16:00:21 -0800 Subject: [PATCH 104/141] i1027: ContestTimerService: added conditional debugging output. --- .../core/services/contestTimer.service.ts | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/projects/WTI-UI/src/app/modules/core/services/contestTimer.service.ts b/projects/WTI-UI/src/app/modules/core/services/contestTimer.service.ts index 600828df7..09747bcab 100644 --- a/projects/WTI-UI/src/app/modules/core/services/contestTimer.service.ts +++ b/projects/WTI-UI/src/app/modules/core/services/contestTimer.service.ts @@ -14,10 +14,16 @@ export class ContestTimerService { } getElapsedSecs(): number { + if (DEBUG_MODE) { + console.log("ContestTimerService.getElapsedSecs(): returning ", this.elapsedSecs); + } return this.elapsedSecs; } getRemainingSecs(): number { + if (DEBUG_MODE) { + console.log("ContestTimerService.getRemainingSecs(): returning ", this.remainingSecs); + } return this.remainingSecs; } @@ -26,6 +32,9 @@ export class ContestTimerService { console.error ("ContestTimerService.setElapsedSecs(): cannot set elapsed seconds while Timer is running; stop the timer first.") return; } else { + if (DEBUG_MODE) { + console.log ("ContestTimerService.setElapsedSecs(): setting elapsed seconds to ", newElapsedSecs); + } this.elapsedSecs = newElapsedSecs; } } @@ -35,6 +44,9 @@ export class ContestTimerService { console.error ("ContestTimerService.setRemainingSecs(): cannot set remaining seconds while Timer is running; stop the timer first.") return; } else { + if (DEBUG_MODE) { + console.log ("ContestTimerService.setRemainingTime(): setting remaining seconds to ", newRemainingSecs); + } if (newRemainingSecs>=0) { this.remainingSecs = newRemainingSecs; } else { @@ -59,7 +71,9 @@ export class ContestTimerService { //bump the elapsed & remaining counters since 1000msec (one sec) has passed this.elapsedSecs += 1 ; this.remainingSecs -= 1 - console.log(`Elapsed secs: ${this.elapsedSecs}; remaining secs: ${this.remainingSecs}`); + if (DEBUG_MODE) { + console.log(`Elapsed secs: ${this.elapsedSecs}; remaining secs: ${this.remainingSecs}`); + } }, 1000 // 1000 milliseconds = 1 second interval ); From e1558d20b5d51150eaefb4ba4018df2431bbe0cc Mon Sep 17 00:00:00 2001 From: John Clevenger Date: Thu, 26 Dec 2024 16:15:21 -0800 Subject: [PATCH 105/141] i1027: ContestService: added conditional debugging output. --- .../modules/core/services/contest.service.ts | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/projects/WTI-UI/src/app/modules/core/services/contest.service.ts b/projects/WTI-UI/src/app/modules/core/services/contest.service.ts index b95789cab..82f7f0755 100644 --- a/projects/WTI-UI/src/app/modules/core/services/contest.service.ts +++ b/projects/WTI-UI/src/app/modules/core/services/contest.service.ts @@ -98,6 +98,10 @@ export class ContestService extends IContestService { updateContestClock (newContestClock: ContestClock) { //save the new clock + if (DEBUG_MODE) { + console.log("ContestService (id ", this.uniqueId, ").updateContestClock(): replacing Contest Clock with:"); + console.log(newContestClock); + } this.contestClock = newContestClock; //pull the relevant values out of the new clock @@ -108,6 +112,9 @@ export class ContestService extends IContestService { //shut off timer if it is running (otherwise we can't update the elapsed/remaining time values) if (this.contestTimer.isTimerRunning) { + if (DEBUG_MODE) { + console.log("ContestService (id ", this.uniqueId, ").updateContestClock(): stopping timer"); + } this.contestTimer.stopTimer(); } @@ -117,24 +124,39 @@ export class ContestService extends IContestService { //restart the timer if the new contest clock values indicate it should be running if (timerShouldBeStarted) { + if (DEBUG_MODE) { + console.log("ContestService (id ", this.uniqueId, ").updateContestClock(): starting timer"); + } this.contestTimer.startTimer(); } } enableContestTimerUpdates() { + if (DEBUG_MODE) { + console.log("ContestService (id ", this.uniqueId, ").enableContestTimerUpdates(): starting timer"); + } this.contestTimer.startTimer(); } disableContestTimerUpdates() { + if (DEBUG_MODE) { + console.log("ContestService (id ", this.uniqueId, ").disableContestTimerUpdates(): stopping timer"); + } this.contestTimer.stopTimer(); } getElapsedSecs(): number { + if (DEBUG_MODE) { + console.log("ContestService (id ", this.uniqueId, ").getElapsedSecs(): returning ", this.contestClock.elapsedSecs); + } return parseInt(this.contestClock.elapsedSecs) ; } getRemainingSecs(): number { let remainingSecs = parseInt(this.contestClock.contestLengthSecs) - parseInt(this.contestClock.elapsedSecs) ; + if (DEBUG_MODE) { + console.log("ContestService (id ", this.uniqueId, ").getRemainingSecs(): returning ", remainingSecs); + } return remainingSecs ; } From 52a12ebfe5011a05be991825d69b5a78df5cda64 Mon Sep 17 00:00:00 2001 From: John Clevenger Date: Thu, 26 Dec 2024 16:27:15 -0800 Subject: [PATCH 106/141] i1027: ContestService: return elapsed/remainging from Timer. --- .../src/app/modules/core/services/contest.service.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/projects/WTI-UI/src/app/modules/core/services/contest.service.ts b/projects/WTI-UI/src/app/modules/core/services/contest.service.ts index 82f7f0755..49f028c99 100644 --- a/projects/WTI-UI/src/app/modules/core/services/contest.service.ts +++ b/projects/WTI-UI/src/app/modules/core/services/contest.service.ts @@ -146,16 +146,17 @@ export class ContestService extends IContestService { } getElapsedSecs(): number { + let elapsedSecs = this.contestTimer.getElapsedSecs(); if (DEBUG_MODE) { - console.log("ContestService (id ", this.uniqueId, ").getElapsedSecs(): returning ", this.contestClock.elapsedSecs); + console.log("ContestService (id ", this.uniqueId, ").getElapsedSecs(): returning elapsed secs from ContestTimer: ", elapsedSecs); } - return parseInt(this.contestClock.elapsedSecs) ; + return elapsedSecs ; } getRemainingSecs(): number { - let remainingSecs = parseInt(this.contestClock.contestLengthSecs) - parseInt(this.contestClock.elapsedSecs) ; + let remainingSecs = this.contestTimer.getRemainingSecs(); if (DEBUG_MODE) { - console.log("ContestService (id ", this.uniqueId, ").getRemainingSecs(): returning ", remainingSecs); + console.log("ContestService (id ", this.uniqueId, ").getRemainingSecs(): returning remaining secs from ContestTimer: ", remainingSecs); } return remainingSecs ; } From 62cd488d1e764428eb0088aadf29f0ee70d337ea Mon Sep 17 00:00:00 2001 From: John Clevenger Date: Thu, 26 Dec 2024 16:42:03 -0800 Subject: [PATCH 107/141] i1027: LoginPageComponent: added conditional debugging output. --- .../login-page/login-page.component.ts | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/projects/WTI-UI/src/app/modules/login/components/login-page/login-page.component.ts b/projects/WTI-UI/src/app/modules/login/components/login-page/login-page.component.ts index b33b10122..e086327c9 100644 --- a/projects/WTI-UI/src/app/modules/login/components/login-page/login-page.component.ts +++ b/projects/WTI-UI/src/app/modules/login/components/login-page/login-page.component.ts @@ -139,17 +139,37 @@ export class LoginPageComponent implements OnInit, OnDestroy { }); //get the actual contest clock info from the PC2 server via the Contest Service (which gets it via the WTI Server and its PC2 API) + if (DEBUG_MODE) { + console.log(" Invoking ContestService.getContestClock(), subscribing for callback result") + } this._contestService.getContestClock() .subscribe( (data: any) => { if (!data) { - console.error ("Unable to get ContestClock from PC2 API via ContestService!"); + console.error ("LoginPageComponent.onSubmit() ContestClock subscription callback: unable to get ContestClock from PC2 API via ContestService!"); } else { +<<<<<<< Upstream, based on c4b09354e015df5ecedbdcd004dea9810eecc455 //copy the data fields received from the PC2 Server (via the WTI-API) into the local ContestClock object this.contestClock.isRunning = data.running ; this.contestClock.contestLengthSecs = data.contestLengthInSecs ; this.contestClock.elapsedSecs = data.elapsedSecs ; this.contestClock.wallClockStartTime = data.wallClockStartTime ; +======= + //copy the data fields received from the PC2 Server (via the WTI-API) into the ContestService's ContestClock object + if (DEBUG_MODE) { + console.log("LoginPageComponent.onSubmit(): got callback from ContestService.getContestClock() subscription; callback data ="); + console.log(data); + } + let newContestClock = new ContestClock(); + newContestClock.isRunning = data.running ; + newContestClock.contestLengthSecs = data.contestLengthInSecs ; + newContestClock.elapsedSecs = data.elapsedSecs ; + newContestClock.wallClockStartTime = data.wallClockStartTime ; + if (DEBUG_MODE) { + console.log("LoginPageComponent.onSubmit() ContestService.getContestClock() subscription callback: calling ContestService.updateContestClock() with new clock data) "); + } + this._contestService.updateContestClock(newContestClock); +>>>>>>> 95ef304 i1027: LoginPageComponent: added conditional debugging output. } }, (error: any) => { From 8668e00f60a9a6f0d251ca8b91ac501852ae26b9 Mon Sep 17 00:00:00 2001 From: John Clevenger Date: Thu, 26 Dec 2024 16:55:15 -0800 Subject: [PATCH 108/141] i1027: AppHeaderComponent: added conditional debugging output. --- .../shared/components/app-header/app-header.component.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/projects/WTI-UI/src/app/modules/shared/components/app-header/app-header.component.ts b/projects/WTI-UI/src/app/modules/shared/components/app-header/app-header.component.ts index 90373c247..b04fe1b38 100644 --- a/projects/WTI-UI/src/app/modules/shared/components/app-header/app-header.component.ts +++ b/projects/WTI-UI/src/app/modules/shared/components/app-header/app-header.component.ts @@ -36,11 +36,17 @@ export class AppHeaderComponent { getElapsedSecs(): number { let secs = this._contestService.getElapsedSecs() ; + if (DEBUG_MODE) { + console.log("AppHeaderComponent.getElapsedSecs(): returning ", secs); + } return secs; } getRemainingSecs(): number { let secs = this._contestService.getRemainingSecs() ; + if (DEBUG_MODE) { + console.log("AppHeaderComponent.getRemainingSecs(): returning ", secs); + } return secs; } } From 94a8e06fc9114166a0a3bfc45e5dde9bd145586e Mon Sep 17 00:00:00 2001 From: John Clevenger Date: Fri, 27 Dec 2024 12:34:08 -0800 Subject: [PATCH 109/141] i1027: ContestClock: added additional documentation. --- projects/WTI-UI/src/app/modules/core/models/contest-clock.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/projects/WTI-UI/src/app/modules/core/models/contest-clock.ts b/projects/WTI-UI/src/app/modules/core/models/contest-clock.ts index 474f7c012..dd31d6ccc 100644 --- a/projects/WTI-UI/src/app/modules/core/models/contest-clock.ts +++ b/projects/WTI-UI/src/app/modules/core/models/contest-clock.ts @@ -3,6 +3,9 @@ * whether the contest is running or paused right now, and how much time has elapsed on the contest clock since the * contest started (note that this latter value does NOT include any real time which passed while the contest was "paused"). * Essentially, this model class corresponds to the PC2 server class "ContestTime". + * Note however that because the (Java-based) PC2 class "ContestTime" is returned from the WTI Server as a JSON string, + * all of the values in the WTI-UI ContestClock model are type 'string' and must be converted to the appropriate type when + * they are actually used. */ export class ContestClock { isRunning: string = ''; //string 'true' or 'false' From df9f7468f8683aac59e2333607ee9059c0ec7b91 Mon Sep 17 00:00:00 2001 From: John Clevenger Date: Fri, 27 Dec 2024 12:50:33 -0800 Subject: [PATCH 110/141] i1027: commented-out no-longer-needed debug output. --- .../modules/core/services/contest.service.ts | 18 ++++++++++++++---- .../core/services/contestTimer.service.ts | 9 ++++++--- .../app-header/app-header.component.ts | 8 +++++--- 3 files changed, 25 insertions(+), 10 deletions(-) diff --git a/projects/WTI-UI/src/app/modules/core/services/contest.service.ts b/projects/WTI-UI/src/app/modules/core/services/contest.service.ts index 49f028c99..e652623cb 100644 --- a/projects/WTI-UI/src/app/modules/core/services/contest.service.ts +++ b/projects/WTI-UI/src/app/modules/core/services/contest.service.ts @@ -104,12 +104,20 @@ export class ContestService extends IContestService { } this.contestClock = newContestClock; - //pull the relevant values out of the new clock - let timerShouldBeStarted = this.contestClock.isRunning === 'true'; + //pull the values out of the updated clock + let timerShouldBeStarted = this.contestClock.isRunning ; let elapsedSecs = parseInt(this.contestClock.elapsedSecs); let contestLengthSecs = parseInt(this.contestClock.contestLengthSecs); let remainingSecs = contestLengthSecs - elapsedSecs; +/* if (DEBUG_MODE) { + console.log ("ContestService.updateContestClock(): values pulled from newContestClock:"); + console.log (" timerShouldBeStarted = ", timerShouldBeStarted); + console.log (" elapsedSecs = ", elapsedSecs); + console.log (" contestLengthSecs = ", contestLengthSecs); + console.log (" remainingSecs = ", remainingSecs); + } +*/ //shut off timer if it is running (otherwise we can't update the elapsed/remaining time values) if (this.contestTimer.isTimerRunning) { if (DEBUG_MODE) { @@ -147,17 +155,19 @@ export class ContestService extends IContestService { getElapsedSecs(): number { let elapsedSecs = this.contestTimer.getElapsedSecs(); - if (DEBUG_MODE) { +/* if (DEBUG_MODE) { console.log("ContestService (id ", this.uniqueId, ").getElapsedSecs(): returning elapsed secs from ContestTimer: ", elapsedSecs); } +*/ return elapsedSecs ; } getRemainingSecs(): number { let remainingSecs = this.contestTimer.getRemainingSecs(); - if (DEBUG_MODE) { +/* if (DEBUG_MODE) { console.log("ContestService (id ", this.uniqueId, ").getRemainingSecs(): returning remaining secs from ContestTimer: ", remainingSecs); } +*/ return remainingSecs ; } diff --git a/projects/WTI-UI/src/app/modules/core/services/contestTimer.service.ts b/projects/WTI-UI/src/app/modules/core/services/contestTimer.service.ts index 09747bcab..2ae8db0aa 100644 --- a/projects/WTI-UI/src/app/modules/core/services/contestTimer.service.ts +++ b/projects/WTI-UI/src/app/modules/core/services/contestTimer.service.ts @@ -14,16 +14,18 @@ export class ContestTimerService { } getElapsedSecs(): number { - if (DEBUG_MODE) { +/* if (DEBUG_MODE) { console.log("ContestTimerService.getElapsedSecs(): returning ", this.elapsedSecs); } +*/ return this.elapsedSecs; } getRemainingSecs(): number { - if (DEBUG_MODE) { +/* if (DEBUG_MODE) { console.log("ContestTimerService.getRemainingSecs(): returning ", this.remainingSecs); } +*/ return this.remainingSecs; } @@ -71,9 +73,10 @@ export class ContestTimerService { //bump the elapsed & remaining counters since 1000msec (one sec) has passed this.elapsedSecs += 1 ; this.remainingSecs -= 1 - if (DEBUG_MODE) { +/* if (DEBUG_MODE) { console.log(`Elapsed secs: ${this.elapsedSecs}; remaining secs: ${this.remainingSecs}`); } +*/ }, 1000 // 1000 milliseconds = 1 second interval ); diff --git a/projects/WTI-UI/src/app/modules/shared/components/app-header/app-header.component.ts b/projects/WTI-UI/src/app/modules/shared/components/app-header/app-header.component.ts index b04fe1b38..89cb6b543 100644 --- a/projects/WTI-UI/src/app/modules/shared/components/app-header/app-header.component.ts +++ b/projects/WTI-UI/src/app/modules/shared/components/app-header/app-header.component.ts @@ -36,17 +36,19 @@ export class AppHeaderComponent { getElapsedSecs(): number { let secs = this._contestService.getElapsedSecs() ; - if (DEBUG_MODE) { +/* if (DEBUG_MODE) { console.log("AppHeaderComponent.getElapsedSecs(): returning ", secs); } - return secs; +*/ + return secs; } getRemainingSecs(): number { let secs = this._contestService.getRemainingSecs() ; - if (DEBUG_MODE) { +/* if (DEBUG_MODE) { console.log("AppHeaderComponent.getRemainingSecs(): returning ", secs); } +*/ return secs; } } From a9796f1a57c6ea87b6f0a85f214f2438eac1b224 Mon Sep 17 00:00:00 2001 From: John Clevenger Date: Fri, 27 Dec 2024 13:26:51 -0800 Subject: [PATCH 111/141] i1027: updated debug comments; added "todo". --- .../modules/core/abstract-services/i-websocket.service.ts | 6 +++++- .../WTI-UI/src/app/modules/core/services/contest.service.ts | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/projects/WTI-UI/src/app/modules/core/abstract-services/i-websocket.service.ts b/projects/WTI-UI/src/app/modules/core/abstract-services/i-websocket.service.ts index 67b9d527a..caf38abb7 100644 --- a/projects/WTI-UI/src/app/modules/core/abstract-services/i-websocket.service.ts +++ b/projects/WTI-UI/src/app/modules/core/abstract-services/i-websocket.service.ts @@ -44,7 +44,9 @@ export abstract class IWebsocketService { } case 'contest_clock': { if (DEBUG_MODE) { - console.log ("Got '", message.type, "' websocket message in IWebsocketService.incomingMessage()"); + console.log ("IWebsocketService.incomingMessage(): got websocket message '", message.type); + console.log ("IWebsocketService.incomingMessage(): invoking ContestService.getIsContestRunning() and"); + console.log (" subscribing to callback from HTTP GET"); } this._contestService.getIsContestRunning() .subscribe((val: any) => { @@ -59,6 +61,8 @@ export abstract class IWebsocketService { } this._contestService.isContestRunning = val; + //TODO: need to force the ContestService to update the contest clock and set the ContestTimer as appropriate here. + // Possibly: subscribe somewhere to the ContestClockEvent Subject and implement a subscription callback that does this updating? this._contestService.contestClockEvent.next(); }); break; diff --git a/projects/WTI-UI/src/app/modules/core/services/contest.service.ts b/projects/WTI-UI/src/app/modules/core/services/contest.service.ts index e652623cb..3ed41eb5d 100644 --- a/projects/WTI-UI/src/app/modules/core/services/contest.service.ts +++ b/projects/WTI-UI/src/app/modules/core/services/contest.service.ts @@ -57,7 +57,7 @@ export class ContestService extends IContestService { getIsContestRunning(): Observable { if (DEBUG_MODE) { - console.log ("Executing ContestService.getIsContestRunning(): calling HTTP client get(.../contest.isRunning)") ; + console.log ("ContestService.getIsContestRunning(): calling HTTP client get(.../contest.isRunning)") ; } return this._httpClient.get(`${environment.baseUrl}/contest/isRunning`); } From 67133dbe724d394bfad81f2b9a14853d8be3ebba Mon Sep 17 00:00:00 2001 From: John Clevenger Date: Fri, 27 Dec 2024 22:38:09 -0800 Subject: [PATCH 112/141] i1027: refactor updateContestClock() to be a ContestService method. --- .../abstract-services/i-contest.service.ts | 2 +- .../core/services/contest.mock.service.ts | 2 +- .../modules/core/services/contest.service.ts | 93 ++++++++++++------- .../login-page/login-page.component.ts | 26 ++---- 4 files changed, 71 insertions(+), 52 deletions(-) diff --git a/projects/WTI-UI/src/app/modules/core/abstract-services/i-contest.service.ts b/projects/WTI-UI/src/app/modules/core/abstract-services/i-contest.service.ts index 9d3d0171e..f20083c38 100644 --- a/projects/WTI-UI/src/app/modules/core/abstract-services/i-contest.service.ts +++ b/projects/WTI-UI/src/app/modules/core/abstract-services/i-contest.service.ts @@ -70,7 +70,7 @@ export abstract class IContestService { abstract getContestClock(): Observable; //note this refers to a "ContestClock" model object, not the "contestClockEvent" Subject above - abstract updateContestClock (newClock: ContestClock): void; //update the WTI-UI representation of the PC2 contest clock + abstract updateContestClock (): void; //update the WTI-UI representation of the PC2 contest clock abstract getElapsedSecs(): number; diff --git a/projects/WTI-UI/src/app/modules/core/services/contest.mock.service.ts b/projects/WTI-UI/src/app/modules/core/services/contest.mock.service.ts index 54bfa19dd..192dbfe84 100644 --- a/projects/WTI-UI/src/app/modules/core/services/contest.mock.service.ts +++ b/projects/WTI-UI/src/app/modules/core/services/contest.mock.service.ts @@ -94,7 +94,7 @@ export class ContestMockService extends IContestService { {isRunning:'true', contestLengthSecs:'18000', elapsedSecs:'3600', wallClockStartTime:'1734593847'}); } - updateContestClock (newContestClock: ContestClock) { + updateContestClock () { //nothing to do -- this is a mock service } diff --git a/projects/WTI-UI/src/app/modules/core/services/contest.service.ts b/projects/WTI-UI/src/app/modules/core/services/contest.service.ts index 3ed41eb5d..2749ba316 100644 --- a/projects/WTI-UI/src/app/modules/core/services/contest.service.ts +++ b/projects/WTI-UI/src/app/modules/core/services/contest.service.ts @@ -57,7 +57,7 @@ export class ContestService extends IContestService { getIsContestRunning(): Observable { if (DEBUG_MODE) { - console.log ("ContestService.getIsContestRunning(): calling HTTP client get(.../contest.isRunning)") ; + console.log ("ContestService.getIsContestRunning(): invoking HTTP get(.../contest.isRunning) WTI-API endpoint") ; } return this._httpClient.get(`${environment.baseUrl}/contest/isRunning`); } @@ -96,10 +96,56 @@ export class ContestService extends IContestService { return this.standingsAreCurrent ; } - updateContestClock (newContestClock: ContestClock) { + /** This method invokes the local getContestClock() method, which makes an HTTP call to the WTI-API to get the current + * PC2 Server clock (aka "ContestTime"). It subscribes to the Observable returned by the HTTP call, + * and when the subscription callback occurs it uses the received ContestClock data (an instance of + * WTI=UI models/ContestClock) to update the WTI-UI contest clock (including the onscreen displays). + */ + updateContestClock () { + + //get the actual contest clock info from the PC2 server via the Contest Service (which gets it via the WTI Server and its PC2 API) + if (DEBUG_MODE) { + console.log(" Invoking ContestService.getContestClock(), subscribing for HTTP callback result") + } + this.getContestClock() + .subscribe( + (data: any) => { + if (!data) { + console.error ("ContestService.updateContestClock() getContestClock() subscription callback: unable to get ContestClock from PC2 API via ContestService!"); + } else { + if (DEBUG_MODE) { + console.log("ContestService.updateContestClock(): got callback from getContestClock() subscription; callback data ="); + console.log(data); + } + //install the received contest clock data into the ContestService's ContestClock + this.installNewContestClock(data); + } + }, + (error: any) => { + console.error("ContestService.updateContestClock(): getContestClock() subscription callback error: "); + console.error (error); + } + ); + } + + /** This method receives a WTI-UI ContestClock model containing new values which should be used to update the WTI-UI contest clock, + * including the onscreen displays. It constructs a new ContestClock object containing the received data and installs that + * object as the current WTI-UI clock. It then updates the separate "ContestTimer" object with the specified values, and + * if the received data indicates the clock should be running it starts the ContestTimer (which then genrates a "clock tick" + * once per second to update the clock displays). + */ + installNewContestClock(data: any) { + + //copy the data fields (received from the PC2 Server via the WTI-API) into a new ContestService ContestClock object + let newContestClock = new ContestClock(); + newContestClock.isRunning = data.running ; + newContestClock.contestLengthSecs = data.contestLengthInSecs ; + newContestClock.elapsedSecs = data.elapsedSecs ; + newContestClock.wallClockStartTime = data.wallClockStartTime ; + //save the new clock if (DEBUG_MODE) { - console.log("ContestService (id ", this.uniqueId, ").updateContestClock(): replacing Contest Clock with:"); + console.log("ContestService (id", this.uniqueId, ").installNewContestClock(): replacing Contest Clock with:"); console.log(newContestClock); } this.contestClock = newContestClock; @@ -110,18 +156,18 @@ export class ContestService extends IContestService { let contestLengthSecs = parseInt(this.contestClock.contestLengthSecs); let remainingSecs = contestLengthSecs - elapsedSecs; -/* if (DEBUG_MODE) { - console.log ("ContestService.updateContestClock(): values pulled from newContestClock:"); + if (DEBUG_MODE) { + console.log ("ContestService.installNewContestClock(): values pulled from newContestClock:"); console.log (" timerShouldBeStarted = ", timerShouldBeStarted); console.log (" elapsedSecs = ", elapsedSecs); console.log (" contestLengthSecs = ", contestLengthSecs); console.log (" remainingSecs = ", remainingSecs); } -*/ + //shut off timer if it is running (otherwise we can't update the elapsed/remaining time values) if (this.contestTimer.isTimerRunning) { if (DEBUG_MODE) { - console.log("ContestService (id ", this.uniqueId, ").updateContestClock(): stopping timer"); + console.log("ContestService (id", this.uniqueId, ").installNewContestClock(): stopping timer"); } this.contestTimer.stopTimer(); } @@ -133,42 +179,25 @@ export class ContestService extends IContestService { //restart the timer if the new contest clock values indicate it should be running if (timerShouldBeStarted) { if (DEBUG_MODE) { - console.log("ContestService (id ", this.uniqueId, ").updateContestClock(): starting timer"); + console.log("ContestService (id", this.uniqueId, ").installNewContestClock(): starting timer"); } this.contestTimer.startTimer(); } } - enableContestTimerUpdates() { - if (DEBUG_MODE) { - console.log("ContestService (id ", this.uniqueId, ").enableContestTimerUpdates(): starting timer"); - } - this.contestTimer.startTimer(); - } - - disableContestTimerUpdates() { - if (DEBUG_MODE) { - console.log("ContestService (id ", this.uniqueId, ").disableContestTimerUpdates(): stopping timer"); - } - this.contestTimer.stopTimer(); - } - + /** Returns the number of seconds which have elapsed so far in the contest, which it obtains from the separate + * ContestTimer object. + */ getElapsedSecs(): number { - let elapsedSecs = this.contestTimer.getElapsedSecs(); -/* if (DEBUG_MODE) { - console.log("ContestService (id ", this.uniqueId, ").getElapsedSecs(): returning elapsed secs from ContestTimer: ", elapsedSecs); - } -*/ + let elapsedSecs = this.contestTimer.getElapsedSecs(); return elapsedSecs ; } + /** Returns the number of seconds remaining in the contest, which it obtains from the separate + * ContestTimer object. + */ getRemainingSecs(): number { let remainingSecs = this.contestTimer.getRemainingSecs(); -/* if (DEBUG_MODE) { - console.log("ContestService (id ", this.uniqueId, ").getRemainingSecs(): returning remaining secs from ContestTimer: ", remainingSecs); - } -*/ return remainingSecs ; } - } diff --git a/projects/WTI-UI/src/app/modules/login/components/login-page/login-page.component.ts b/projects/WTI-UI/src/app/modules/login/components/login-page/login-page.component.ts index e086327c9..5193b9807 100644 --- a/projects/WTI-UI/src/app/modules/login/components/login-page/login-page.component.ts +++ b/projects/WTI-UI/src/app/modules/login/components/login-page/login-page.component.ts @@ -109,7 +109,7 @@ export class LoginPageComponent implements OnInit, OnDestroy { } this._websocketService.startWebsocket(); if (DEBUG_MODE) { - console.log (" invoking ContestService.getisContestRunning() and subscribing to the result") ; + console.log (" Invoking ContestService.getisContestRunning() and subscribing to the result") ; } this._contestService.getIsContestRunning() .subscribe((val: boolean) => { @@ -148,28 +148,11 @@ export class LoginPageComponent implements OnInit, OnDestroy { if (!data) { console.error ("LoginPageComponent.onSubmit() ContestClock subscription callback: unable to get ContestClock from PC2 API via ContestService!"); } else { -<<<<<<< Upstream, based on c4b09354e015df5ecedbdcd004dea9810eecc455 //copy the data fields received from the PC2 Server (via the WTI-API) into the local ContestClock object this.contestClock.isRunning = data.running ; this.contestClock.contestLengthSecs = data.contestLengthInSecs ; this.contestClock.elapsedSecs = data.elapsedSecs ; this.contestClock.wallClockStartTime = data.wallClockStartTime ; -======= - //copy the data fields received from the PC2 Server (via the WTI-API) into the ContestService's ContestClock object - if (DEBUG_MODE) { - console.log("LoginPageComponent.onSubmit(): got callback from ContestService.getContestClock() subscription; callback data ="); - console.log(data); - } - let newContestClock = new ContestClock(); - newContestClock.isRunning = data.running ; - newContestClock.contestLengthSecs = data.contestLengthInSecs ; - newContestClock.elapsedSecs = data.elapsedSecs ; - newContestClock.wallClockStartTime = data.wallClockStartTime ; - if (DEBUG_MODE) { - console.log("LoginPageComponent.onSubmit() ContestService.getContestClock() subscription callback: calling ContestService.updateContestClock() with new clock data) "); - } - this._contestService.updateContestClock(newContestClock); ->>>>>>> 95ef304 i1027: LoginPageComponent: added conditional debugging output. } }, (error: any) => { @@ -178,6 +161,13 @@ export class LoginPageComponent implements OnInit, OnDestroy { } ); + + //update the WTI-UI representations of the PC2 Contest Clock + if (DEBUG_MODE) { + console.log (" Invoking ContestService.updateContestClock()") ; + } + this._contestService.updateContestClock(); + }, (error: any) => { if (DEBUG_MODE) { console.error ("AuthService.login() subscription returned error:", error) ; From 81ebbd6dc97a1e2825f2dd3737a33440058f583a Mon Sep 17 00:00:00 2001 From: John Clevenger Date: Fri, 27 Dec 2024 23:20:27 -0800 Subject: [PATCH 113/141] i1027: IWebsocketService: invoke updateContestClock() on 'clock' msg. --- .../core/abstract-services/i-websocket.service.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/projects/WTI-UI/src/app/modules/core/abstract-services/i-websocket.service.ts b/projects/WTI-UI/src/app/modules/core/abstract-services/i-websocket.service.ts index caf38abb7..745d422d6 100644 --- a/projects/WTI-UI/src/app/modules/core/abstract-services/i-websocket.service.ts +++ b/projects/WTI-UI/src/app/modules/core/abstract-services/i-websocket.service.ts @@ -52,18 +52,23 @@ export abstract class IWebsocketService { .subscribe((val: any) => { if (DEBUG_MODE) { console.log ("IWebsocketService.incomingMessage(): callback from ContestService.getIsContestRunning() returned '", val, "'"); +<<<<<<< Upstream, based on c4b09354e015df5ecedbdcd004dea9810eecc455 console.log ("Setting ContestService.isContestRunning to '", val, "'") ; <<<<<<< Upstream, based on c4b09354e015df5ecedbdcd004dea9810eecc455 console.log (" and invoking ContestService.contestClock.next()") ; ======= console.log (" and invoking ContestService.contestClockEvent.next()") ; >>>>>>> 5291696 i1027: IWebSocketService: make root-injectable; add debug output. +======= + console.log ("Setting ContestService.isContestRunning to '", val, "',") ; + console.log (" invoking ContestService.contestClockEvent.next()") ; + console.log (" and invoking ContestService.updateContestClock()"); +>>>>>>> d43bfce i1027: IWebsocketService: invoke updateContestClock() on 'clock' msg. } this._contestService.isContestRunning = val; - //TODO: need to force the ContestService to update the contest clock and set the ContestTimer as appropriate here. - // Possibly: subscribe somewhere to the ContestClockEvent Subject and implement a subscription callback that does this updating? this._contestService.contestClockEvent.next(); + this._contestService.updateContestClock(); }); break; } From b8a3e0a05ede977b8d222e234827e4d5d7ef703f Mon Sep 17 00:00:00 2001 From: John Clevenger Date: Fri, 27 Dec 2024 23:21:04 -0800 Subject: [PATCH 114/141] i1027: LoginPageComponent: add comments (no code changes). --- .../login/components/login-page/login-page.component.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/projects/WTI-UI/src/app/modules/login/components/login-page/login-page.component.ts b/projects/WTI-UI/src/app/modules/login/components/login-page/login-page.component.ts index 5193b9807..9a39a9876 100644 --- a/projects/WTI-UI/src/app/modules/login/components/login-page/login-page.component.ts +++ b/projects/WTI-UI/src/app/modules/login/components/login-page/login-page.component.ts @@ -128,7 +128,12 @@ export class LoginPageComponent implements OnInit, OnDestroy { if (DEBUG_MODE) { console.log ("Invoking ContestService.contestClock.next()") ; } +<<<<<<< Upstream, based on c4b09354e015df5ecedbdcd004dea9810eecc455 this._contestService.contestClock.next(); +======= + //trigger a contestClockEvent so the SelectProblems dropdown can decide whether to display the problems or not + this._contestService.contestClockEvent.next(); +>>>>>>> f172644 i1027: LoginPageComponent: add comments (no code changes). }); }, (error: any) => { if (DEBUG_MODE) { From 5f35f42cc238eb5c4f4ae3ea54863586a400a458 Mon Sep 17 00:00:00 2001 From: John Clevenger Date: Sat, 28 Dec 2024 13:09:32 -0800 Subject: [PATCH 115/141] i127: AppHeaderComponent: remove debugging display of raw seconds. --- .../shared/components/app-header/app-header.component.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/projects/WTI-UI/src/app/modules/shared/components/app-header/app-header.component.html b/projects/WTI-UI/src/app/modules/shared/components/app-header/app-header.component.html index 4f3354b3a..d6e482470 100644 --- a/projects/WTI-UI/src/app/modules/shared/components/app-header/app-header.component.html +++ b/projects/WTI-UI/src/app/modules/shared/components/app-header/app-header.component.html @@ -7,8 +7,8 @@
Elapsed:
- {{ getElapsedSecs() }}
- + {{ getElapsedSecs() | displayTime }}
From c2f1b9046c76cbf85f04f086aceef27859190e18 Mon Sep 17 00:00:00 2001 From: John Clevenger Date: Sat, 28 Dec 2024 13:33:12 -0800 Subject: [PATCH 116/141] i1027: WTI-API.ContestController: fix copy/paste typo in response. --- projects/WTI-API/src/main/controllers/ContestController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/projects/WTI-API/src/main/controllers/ContestController.java b/projects/WTI-API/src/main/controllers/ContestController.java index 53aea1dbd..88fd20d46 100644 --- a/projects/WTI-API/src/main/controllers/ContestController.java +++ b/projects/WTI-API/src/main/controllers/ContestController.java @@ -479,7 +479,7 @@ public Response contestClock(@ApiParam(value="token used by logged in users to a catch(NullPointerException e) { logger.severe(e.getMessage()); return Response.status(Response.Status.INTERNAL_SERVER_ERROR) - .entity(new ServerErrorResponseModel(Response.Status.INTERNAL_SERVER_ERROR, "NullPointerException in ContestController.clarifications()")) + .entity(new ServerErrorResponseModel(Response.Status.INTERNAL_SERVER_ERROR, "NullPointerException in ContestController.contestClock()")) .type(MediaType.APPLICATION_JSON).build(); } From ec7c0515f22a74f04ad1697decd9d1377520e45b Mon Sep 17 00:00:00 2001 From: John Clevenger Date: Sat, 28 Dec 2024 13:37:51 -0800 Subject: [PATCH 117/141] i1027: ContestTimer: set default remaining time to zero. Note that... ...all logic paths SHOULD override this by getting the ACTUAL remaining time from the PC2 server and updating the timer accordingly. --- .../src/app/modules/core/services/contestTimer.service.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/projects/WTI-UI/src/app/modules/core/services/contestTimer.service.ts b/projects/WTI-UI/src/app/modules/core/services/contestTimer.service.ts index 2ae8db0aa..39a0b053b 100644 --- a/projects/WTI-UI/src/app/modules/core/services/contestTimer.service.ts +++ b/projects/WTI-UI/src/app/modules/core/services/contestTimer.service.ts @@ -2,8 +2,8 @@ import { DEBUG_MODE } from 'src/constants' export class ContestTimerService { - elapsedSecs = 0 ; //how many seconds the contest has been running (doesn't include any 'paused' time) - remainingSecs = 18000 ; //how many seconds remain in the contest (default initial value: 5 hours = 18,000 secs) + elapsedSecs = 0 ; //how many seconds the contest has been running (doesn't include any 'paused' time) + remainingSecs = 0 ; //how many seconds remain in the contest (default default=0 until specified otherwise) intervalId: ReturnType ; //the id of the JavaScript "interval" used to generate timer ticks isTimerRunning: boolean = false; From 9b8f0eaf1e5047c4504f618ac05f13d5da89c5cd Mon Sep 17 00:00:00 2001 From: John Clevenger Date: Wed, 1 Jan 2025 14:08:16 -0800 Subject: [PATCH 118/141] i1027: added comments (no code changes). --- projects/WTI-API/src/main/models/ContestClockModel.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/projects/WTI-API/src/main/models/ContestClockModel.java b/projects/WTI-API/src/main/models/ContestClockModel.java index 260c1c9f5..41211e35e 100644 --- a/projects/WTI-API/src/main/models/ContestClockModel.java +++ b/projects/WTI-API/src/main/models/ContestClockModel.java @@ -14,7 +14,8 @@ public class ContestClockModel { private boolean isRunning; private long contestLengthInSecs; private long elapsedSecs; //total time in seconds that the contest has been running -- does NOT include time during any "pauses" - private long wallClockStartTime; //unix timestamp when the contest actually started -- msec since the Epoch. Does not change due to "pauses" + private long wallClockStartTime; //unix timestamp when the contest actually started (msec since the Epoch), + // or zero if contest has not ever been started. Does not change due to "pauses". public ContestClockModel(boolean isRunning, long contestLengthInSecs, long elapsedSecs, long wallClockStartTime) { this.isRunning = isRunning; From 3454d60a80f4edcfe628d344bda12ad61bca7896 Mon Sep 17 00:00:00 2001 From: John Clevenger Date: Wed, 1 Jan 2025 14:12:53 -0800 Subject: [PATCH 119/141] i1027: added toString() method. This is important because... ...the class already defines a hashCode() method, and that hashCode() method references the local variable "elementId" -- which is NULL! The result is that anyone who called toString() (expecting to be able to use the toString() inherited from Object) was getting a NullPointerException (because Object's toString() invokes the hashCode() method, which attempts to dereference elementId). A separate question is "why does ContestTimeImplementation have a null elementId?" I'll file a separate issue for that.. --- .../implementation/ContestTimeImplementation.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/edu/csus/ecs/pc2/api/implementation/ContestTimeImplementation.java b/src/edu/csus/ecs/pc2/api/implementation/ContestTimeImplementation.java index 46d3c09de..79ca58038 100644 --- a/src/edu/csus/ecs/pc2/api/implementation/ContestTimeImplementation.java +++ b/src/edu/csus/ecs/pc2/api/implementation/ContestTimeImplementation.java @@ -70,4 +70,16 @@ public int hashCode() { return elementId.toString().hashCode(); } + @Override + public String toString() { + String retStr = "ContestTimeImplementation (aka 'ContestClockImplementation'): ["; + retStr += "elementId:" + this.elementId + ";"; + retStr += "contestLengthSecs:" + contestTime.getContestLengthSecs() + ";"; + retStr += "elapsedSecs:" + contestTime.getElapsedSecs() + ";"; + retStr += "remainingSecs:" + contestTime.getRemainingSecs(); + retStr += "]"; + return retStr ; + + } + } From b11da0be898a0269347cfbafbcd74a80f1048ead Mon Sep 17 00:00:00 2001 From: John Clevenger Date: Wed, 1 Jan 2025 20:08:51 -0800 Subject: [PATCH 120/141] i1027: handle wallClockStartTime when contest hasn't been started; also, added error logging to support debugging. --- .../main/controllers/ContestController.java | 31 +++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/projects/WTI-API/src/main/controllers/ContestController.java b/projects/WTI-API/src/main/controllers/ContestController.java index 88fd20d46..8d6f69600 100644 --- a/projects/WTI-API/src/main/controllers/ContestController.java +++ b/projects/WTI-API/src/main/controllers/ContestController.java @@ -5,6 +5,7 @@ import java.net.URISyntaxException; import java.util.ArrayList; import java.util.Arrays; +import java.util.Calendar; import java.util.HashMap; import java.util.List; import java.util.Properties; @@ -25,6 +26,7 @@ import config.ServerInit; import edu.csus.ecs.pc2.api.IClarification; import edu.csus.ecs.pc2.api.IClient; +import edu.csus.ecs.pc2.api.IContest; import edu.csus.ecs.pc2.api.IContestClock; import edu.csus.ecs.pc2.api.IJudgement; import edu.csus.ecs.pc2.api.ILanguage; @@ -440,6 +442,11 @@ public Response isRunning( public Response contestClock(@ApiParam(value="token used by logged in users to access teams information", required = true) @HeaderParam("team_id")String key) { + if (connections == null) { + System.err.println ("SEVERE: ContestController.contestClock(): team connections map in MainController is null!"); + logger.severe("ContestController.contestClock(): team connections map in MainController is null!"); + throw new NullPointerException("Team connections map in MainController is null in ContestController.contestClock()"); + } ServerConnection userInformation = connections.get(key); //verify the user is logged in @@ -459,13 +466,33 @@ public Response contestClock(@ApiParam(value="token used by logged in users to a try { //get the contest clock from the PC2 Server via the PC2 API ServerConnection - IContestClock contestClock = userInformation.getContest().getContestClock(); +// IContestClock contestClock = userInformation.getContest().getContestClock(); + IContest contest = userInformation.getContest(); + if (contest == null) { + System.err.println ("SEVERE: ContestController.contestClock(): ServerConnection.getContest() returned null!"); + logger.severe("ContestController.contestClock(): ServerConnection.getContest() returned null!"); + throw new NullPointerException("ServerConnection.getContest() returned null in ContestController.contestClock()"); + } + IContestClock contestClock = contest.getContestClock(); + if (contestClock == null) { + System.err.println ("SEVERE: ContestController.contestClock(): ServerConnection.getContest().getContestClock() returned null!"); + logger.severe("ContestController.contestClock(): ServerConnection.getContest().getContestClock() returned null!"); + throw new NullPointerException("ServerConnection.getContest().getContestClock() returned null in ContestController.contestClock()"); + } + //retrieve the relevant fields from the PC2 contest clock boolean isRunning = contestClock.isContestClockRunning(); long contestLengthInSecs = contestClock.getContestLengthSecs(); long elapsedSecs = contestClock.getElapsedSecs(); - long wallClockStartTime = contestClock.getContestStartTime().getTimeInMillis(); + long wallClockStartTime ; + //"contest start time" is a Calendar object and might be null if the contest has never been started + Calendar startTimeCalendar = contestClock.getContestStartTime(); + if (startTimeCalendar == null) { + wallClockStartTime = 0; + } else { + wallClockStartTime = startTimeCalendar.getTimeInMillis(); + } //construct a ContestClock containing the PC2 Server clock values returnableContestClock = new ContestClockModel(isRunning,contestLengthInSecs, elapsedSecs, wallClockStartTime); From 18f2ce1cacaf416c45514af69f105fc40878579f Mon Sep 17 00:00:00 2001 From: John Clevenger Date: Thu, 2 Jan 2025 16:39:00 -0800 Subject: [PATCH 121/141] i1027: update contest clocks on F5 refresh/reload. --- projects/WTI-UI/src/app/app.component.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/projects/WTI-UI/src/app/app.component.ts b/projects/WTI-UI/src/app/app.component.ts index 8be3563d0..64aa84b1c 100644 --- a/projects/WTI-UI/src/app/app.component.ts +++ b/projects/WTI-UI/src/app/app.component.ts @@ -184,6 +184,9 @@ export class AppComponent implements OnInit { } } + //restore the contest clock on-screen display values (elapsed and remaining) + this._contestService.updateContestClock(); + // transfer to the (former) "current page". let page = getCurrentPage(); if (DEBUG_MODE) { From 4d05cf0d32c02cbcf8214f1464bfa7e762e49619 Mon Sep 17 00:00:00 2001 From: johnc Date: Sat, 4 Jan 2025 16:58:30 -0800 Subject: [PATCH 122/141] i1027, i1029: update "contest is running" req for viewing clars & probs: This commit removes the requirement that the contest be running in order for the WTI-API /clarifications endpoint to return clars; now the only requirement for returning clars is that the client "is-logged-in". The commit also changes the requirement in the /problems endpoint: previously the contest had to be RUNNING in order to see problems; now, it just has to have been STARTED (i.e., there must have been greater than zero "elapsed time". This allows teams to see problems during a pause. Note that this commit is part of a PR for https://github.com/pc2ccs/pc2v9/issues/1027, but it solves https://github.com/pc2ccs/pc2v9/issues/1029 as well. --- .../src/main/controllers/ContestController.java | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/projects/WTI-API/src/main/controllers/ContestController.java b/projects/WTI-API/src/main/controllers/ContestController.java index 8d6f69600..e8e3f9da0 100644 --- a/projects/WTI-API/src/main/controllers/ContestController.java +++ b/projects/WTI-API/src/main/controllers/ContestController.java @@ -329,8 +329,9 @@ public Response problems( if (userInformation == null) { throw new NotLoggedInException(); } else { - // make sure that the contest is running (problems are not allowed to be seen when the contest is not running) - if (!userInformation.getContest().isContestClockRunning()) { + // make sure that the contest has been started (problems are not allowed to be seen before the contest starts -- + // i.e., before the contest clock has started accumulating elapsed time) + if (!(userInformation.getContest().getContestClock().getElapsedSecs()>0)) { return Response.status(Response.Status.UNAUTHORIZED).entity( new ServerErrorResponseModel(Response.Status.UNAUTHORIZED, "Unauthorized user request")) .type(MediaType.APPLICATION_JSON).build(); @@ -549,13 +550,6 @@ public Response clarifications(@ApiParam(value="token used by logged in users to // make sure we have connection information for this user (i.e. that the user is logged in) if (userInformation == null) { throw new NotLoggedInException(); - } else { - // make sure that the contest is running (problems are not allowed to be seen when the contest is not running) - if (!userInformation.getContest().isContestClockRunning()) { - return Response.status(Response.Status.UNAUTHORIZED).entity( - new ServerErrorResponseModel(Response.Status.UNAUTHORIZED, "Unauthorized user request")) - .type(MediaType.APPLICATION_JSON).build(); - } } } catch (NotLoggedInException e1) { return Response.status(Response.Status.UNAUTHORIZED) From 6d19050e806b486d2d92c1b2b14413ce96e19e14 Mon Sep 17 00:00:00 2001 From: johnc Date: Sat, 4 Jan 2025 17:43:06 -0800 Subject: [PATCH 123/141] i1027: add code to resync UI clocks with PC2 periodically. --- .../modules/core/services/contest.service.ts | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/projects/WTI-UI/src/app/modules/core/services/contest.service.ts b/projects/WTI-UI/src/app/modules/core/services/contest.service.ts index 2749ba316..d95e18587 100644 --- a/projects/WTI-UI/src/app/modules/core/services/contest.service.ts +++ b/projects/WTI-UI/src/app/modules/core/services/contest.service.ts @@ -9,6 +9,7 @@ import { ContestClock } from '../models/contest-clock'; import { ContestTimerService } from './contestTimer.service' ; import { Clarification } from '../models/clarification'; import { DEBUG_MODE } from 'src/constants'; +import { RESYNC_INTERVAL_IN_MINUTES } from 'src/constants'; @Injectable({ providedIn: 'root' //forces the service to be a singleton across all app components ('root' == "root injector") @@ -18,7 +19,7 @@ export class ContestService extends IContestService { standingsAreCurrent: boolean ; cachedStandings: Observable ; - //the WTI-UI timer service which updates elapsed and remaining time when started + //the WTI-UI timer service which updates on-screen elapsed and remaining time when started (enabled) contestTimer: ContestTimerService = new ContestTimerService() ; constructor(private _httpClient: HttpClient) { @@ -36,7 +37,22 @@ export class ContestService extends IContestService { console.log ("Executing ContestService constructor; instance ID = ", this.uniqueId) ; } this.standingsAreCurrent = false; +<<<<<<< Upstream, based on c4b09354e015df5ecedbdcd004dea9810eecc455 >>>>>>> ab9b09a i1027: ContestService: make root-injectable; add ContestTimer; also: +======= + + //set a timer to auto-refresh the contest clock displays, at a rate defined in src/constants + setInterval( + //execute this function at the following-specified interval: + () => { + if (DEBUG_MODE) { + console.log("ContestService: resyncing clocks with PC2 Server"); + } + this.updateContestClock(); + }, + RESYNC_INTERVAL_IN_MINUTES * 60 * 1000 // timer interval in msec: minutes * (secs-per-min) * (msec-per-sec) + ); +>>>>>>> b069333 i1027: add code to resync UI clocks with PC2 periodically. } getLanguages(): Observable { From 7c7298a2cabff2f9b4cd3c7a40e5c4ffcc0daa00 Mon Sep 17 00:00:00 2001 From: johnc Date: Sat, 4 Jan 2025 17:45:19 -0800 Subject: [PATCH 124/141] i1027: constants.ts: set UI clock resync interval to every 15 mins. --- projects/WTI-UI/src/constants.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/projects/WTI-UI/src/constants.ts b/projects/WTI-UI/src/constants.ts index 2e055ede0..86f936706 100644 --- a/projects/WTI-UI/src/constants.ts +++ b/projects/WTI-UI/src/constants.ts @@ -7,6 +7,11 @@ */ export let DEBUG_MODE = true ; +/** + * Interval in minutes at which the WTI-UI will resync its clock displays with PC2 + */ + export const RESYNC_INTERVAL_IN_MINUTES = 15 ; + /** * The key under which the currently-active WTI page is stored in sessionStorage. */ From 9fbef222e9de0b45d8273a14823b418209012362 Mon Sep 17 00:00:00 2001 From: johnc Date: Sat, 4 Jan 2025 18:10:38 -0800 Subject: [PATCH 125/141] i1027: source code cleanup (remove outdated comments, etc.) --- projects/WTI-UI/src/app/app.component.ts | 1 - .../app/modules/core/abstract-services/i-contest.service.ts | 1 - projects/WTI-UI/src/app/modules/core/core.module.ts | 3 +++ .../WTI-UI/src/app/modules/core/services/contest.service.ts | 1 - 4 files changed, 3 insertions(+), 3 deletions(-) diff --git a/projects/WTI-UI/src/app/app.component.ts b/projects/WTI-UI/src/app/app.component.ts index 64aa84b1c..3118ab469 100644 --- a/projects/WTI-UI/src/app/app.component.ts +++ b/projects/WTI-UI/src/app/app.component.ts @@ -194,7 +194,6 @@ export class AppComponent implements OnInit { } //navigate to the most recently saved page - // TODO: consider whether using history.pushState()/popState() is a better solution for this... this.router.navigate([page]) .then(nav => { if (DEBUG_MODE) { diff --git a/projects/WTI-UI/src/app/modules/core/abstract-services/i-contest.service.ts b/projects/WTI-UI/src/app/modules/core/abstract-services/i-contest.service.ts index f20083c38..c167cd60a 100644 --- a/projects/WTI-UI/src/app/modules/core/abstract-services/i-contest.service.ts +++ b/projects/WTI-UI/src/app/modules/core/abstract-services/i-contest.service.ts @@ -31,7 +31,6 @@ export abstract class IContestService { isContestRunning = false; //the WTI-UI representation of the PC2 Server's ContestClock (PC2 class ContestTime) - //todo: where does this get updated when the PC2 clock changes? ; contestClock: ContestClock = new ContestClock(); //give each instance of IContestService a unique ID for debugging purposes diff --git a/projects/WTI-UI/src/app/modules/core/core.module.ts b/projects/WTI-UI/src/app/modules/core/core.module.ts index 4832194e0..d5a59b229 100644 --- a/projects/WTI-UI/src/app/modules/core/core.module.ts +++ b/projects/WTI-UI/src/app/modules/core/core.module.ts @@ -100,10 +100,13 @@ export function WebsocketServiceFactory(injector: Injector, DisplayTimePipe, //TODO: should the following two still be declared here since they are now listed in the above "deps" list? UiHelperService, +<<<<<<< Upstream, based on c4b09354e015df5ecedbdcd004dea9810eecc455 ContestService, //TODO: should the following two still be declared here since they are now listed in the above "deps" list? UiHelperService, >>>>>>> 7a71e77 i1027: update for compatibility with PR for i871. +======= +>>>>>>> db34af4 i1027: source code cleanup (remove outdated comments, etc.) ContestService ], imports: [ diff --git a/projects/WTI-UI/src/app/modules/core/services/contest.service.ts b/projects/WTI-UI/src/app/modules/core/services/contest.service.ts index d95e18587..b8840c210 100644 --- a/projects/WTI-UI/src/app/modules/core/services/contest.service.ts +++ b/projects/WTI-UI/src/app/modules/core/services/contest.service.ts @@ -80,7 +80,6 @@ export class ContestService extends IContestService { /** This method returns an Observable "ContestClock" object -- a WTI-UI model corresponding to the PC2 "ContestTime" class, * which itself encapsulates the "contest clock" on the PC2 server. - * TODO: should this method return the local copy of the ContestClock? */ getContestClock(): Observable { return this._httpClient.get(`${environment.baseUrl}/contest/contestclock`); From 41f828980636f84c7f3ba9b10936eeaab3ca9363 Mon Sep 17 00:00:00 2001 From: johnc Date: Sat, 4 Jan 2025 23:12:56 -0800 Subject: [PATCH 126/141] i1027: added documentation. No code changes. --- projects/WTI-UI/src/app/app-routing.module.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/projects/WTI-UI/src/app/app-routing.module.ts b/projects/WTI-UI/src/app/app-routing.module.ts index 0c781a8f8..87992d380 100644 --- a/projects/WTI-UI/src/app/app-routing.module.ts +++ b/projects/WTI-UI/src/app/app-routing.module.ts @@ -8,6 +8,17 @@ import { LogoutComponent } from './modules/login/components/logout/logout.compon import { ClarificationsPageComponent } from './modules/clarifications/components/clarifications-page/clarifications-page.component'; import { ScoreboardPageComponent } from './modules/scoreboard/components/scoreboard-page/scoreboard-page.component'; +/** + * This class defines the "routes" which the Angular application (specifically, the Angular Router) knows how to follow. + * Note that ORDER MATTERS; the Router searches the 'Routes' array from the beginning to find a route which matches the + * path it has been told to attempt to follow. For this reason, the "login" page will always be the default route. + * Note that remaining routes are protected by a "canActivate: [AuthGuard]" clause; the effect of this is that if the Router + * decides to attempt to follow that route, it first invokes the "AuthGuard" class, which determines whether following that + * route is 'allowed'. (The criterion defined in "AuthGuard" is that the user is logged in.) + * If the route specified by the user does not match any other route defined here, the Router defaults to the LAST route, + * the one with a "path" of "**". The effect of this is that any attempt by the user to enter any undefined route in the + * browser will cause a transfer to the "/runs" page. + */ const routes: Routes = [ { path: 'login', From 76ae5e2e62d5527a92ec68e495b544d9233ffb56 Mon Sep 17 00:00:00 2001 From: johnc Date: Sun, 5 Jan 2025 18:38:56 -0800 Subject: [PATCH 127/141] i1027: use "start time" not elapsed secs to determine "started"; also: added additional exception handling and updated HTTP error response messages. --- .../main/controllers/ContestController.java | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/projects/WTI-API/src/main/controllers/ContestController.java b/projects/WTI-API/src/main/controllers/ContestController.java index e8e3f9da0..11b0bcd96 100644 --- a/projects/WTI-API/src/main/controllers/ContestController.java +++ b/projects/WTI-API/src/main/controllers/ContestController.java @@ -329,17 +329,23 @@ public Response problems( if (userInformation == null) { throw new NotLoggedInException(); } else { - // make sure that the contest has been started (problems are not allowed to be seen before the contest starts -- - // i.e., before the contest clock has started accumulating elapsed time) - if (!(userInformation.getContest().getContestClock().getElapsedSecs()>0)) { + // make sure that the contest has been started (problems are not allowed to be seen before the contest starts) + // Note that starting the contest results in setting the "contest start time" in the ContestClock object to + // the Unix Epoch time at which the contest was started; prior to starting the contest that value will be null. + if (userInformation.getContest().getContestClock().getContestStartTime()==null) { return Response.status(Response.Status.UNAUTHORIZED).entity( - new ServerErrorResponseModel(Response.Status.UNAUTHORIZED, "Unauthorized user request")) + new ServerErrorResponseModel(Response.Status.UNAUTHORIZED, "Unauthorized user request - contest has not started.")) .type(MediaType.APPLICATION_JSON).build(); } } } catch (NotLoggedInException e1) { return Response.status(Response.Status.UNAUTHORIZED) - .entity(new ServerErrorResponseModel(Response.Status.UNAUTHORIZED, "Unauthorized user request")) + .entity(new ServerErrorResponseModel(Response.Status.UNAUTHORIZED, "Unauthorized user request -- not logged in.")) + .type(MediaType.APPLICATION_JSON).build(); + } catch (Exception e2) { + logger.severe("Exception in ContestController /problems endppoint: " + e2.getMessage()); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity(new ServerErrorResponseModel(Response.Status.INTERNAL_SERVER_ERROR, "Exception in ContestController /problems endppoint: " + e2.getMessage())) .type(MediaType.APPLICATION_JSON).build(); } @@ -553,7 +559,7 @@ public Response clarifications(@ApiParam(value="token used by logged in users to } } catch (NotLoggedInException e1) { return Response.status(Response.Status.UNAUTHORIZED) - .entity(new ServerErrorResponseModel(Response.Status.UNAUTHORIZED, "Unauthorized user request")) + .entity(new ServerErrorResponseModel(Response.Status.UNAUTHORIZED, "Unauthorized user request - not logged in.")) .type(MediaType.APPLICATION_JSON).build(); } From b41033fd1cc11429cadf98e79e926227ba0bf141 Mon Sep 17 00:00:00 2001 From: johnc Date: Tue, 7 Jan 2025 12:17:54 -0800 Subject: [PATCH 128/141] i1027: allow problem display if contest started (not just if running). --- .../problem-selector.component.ts | 207 +++++++++++------- 1 file changed, 131 insertions(+), 76 deletions(-) diff --git a/projects/WTI-UI/src/app/modules/shared/components/problem-selector/problem-selector.component.ts b/projects/WTI-UI/src/app/modules/shared/components/problem-selector/problem-selector.component.ts index 910b85650..fefda144d 100644 --- a/projects/WTI-UI/src/app/modules/shared/components/problem-selector/problem-selector.component.ts +++ b/projects/WTI-UI/src/app/modules/shared/components/problem-selector/problem-selector.component.ts @@ -2,43 +2,54 @@ import { Component, forwardRef, OnInit, OnDestroy, Input } from '@angular/core'; import { NG_VALUE_ACCESSOR, ControlValueAccessor } from '@angular/forms'; import { ContestProblem } from 'src/app/modules/core/models/contest-problem'; import { IContestService } from 'src/app/modules/core/abstract-services/i-contest.service'; +import { ContestClock } from 'src/app/modules/core/models/contest-clock'; import { Subject } from 'rxjs'; import { takeUntil } from 'rxjs/operators'; import { DEBUG_MODE } from 'src/constants'; +/** + * This class defines a component which displays a drop-down list containing the current contest problems. + * Each time the component is initialized (that is, on each rendering during which its "ngOnInit(" method is invoked) + * it invokes its "loadProblems()" method, which examines the current contest clock to determine + * if the contest has been started. If so, it fetches the current problem list from the server + * and displays it in the dropdown list; if the contest has not been started it sets the dropdown list to be empty. + * Method loadProblems() is also invoked any time there is a change in the contest clock state (such as stopping + * or restarting the contest clock), and the same process happens (the list is repopulated if the contest has been + * started, otherwise it is set to empty). + */ @Component({ - selector: 'app-problem-selector', - templateUrl: './problem-selector.component.html', - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => ProblemSelectorComponent), - multi: true - } - ] +selector: 'app-problem-selector', +templateUrl: './problem-selector.component.html', +providers: [ +{ +provide: NG_VALUE_ACCESSOR, +useExisting: forwardRef(() => ProblemSelectorComponent), +multi: true +} +] }) export class ProblemSelectorComponent implements OnInit, OnDestroy, ControlValueAccessor { - private _unsubscribe = new Subject(); - @Input() allowGeneral = false; - problems: ContestProblem[] = []; - value: string; - onChange = (event: any) => { }; - onTouched = (event: any) => { }; + private _unsubscribe = new Subject(); + @Input() allowGeneral = false; + problems: ContestProblem[] = []; + value: string; + onChange = (event: any) => { }; + onTouched = (event: any) => { }; - constructor(private _contestService: IContestService) { - if (DEBUG_MODE) { - console.log ("Executing ProblemSelectorComponent constructor; IContestService id = ", this._contestService.uniqueId) ; + constructor(private _contestService: IContestService) { + if (DEBUG_MODE) { + console.log ("Executing ProblemSelectorComponent constructor; IContestService id = ", this._contestService.uniqueId) ; + } } - } - ngOnInit(): void { - if (DEBUG_MODE) { - console.log ("Executing ProblemSelectorComponent.ngOnInit()") ; - } - this.loadProblems(); - if (this.allowGeneral) { - this.writeValue('general'); - } + ngOnInit(): void { + if (DEBUG_MODE) { + console.log ("Executing ProblemSelectorComponent.ngOnInit()") ; + } + this.loadProblems(); + if (this.allowGeneral) { + this.writeValue('general'); + } //Listen for (subscribe to) contest start/stop events to show/hide contest problems. //The contestClockEvent "Subject" (defined in IContestService) is used as a toggle to indicate when @@ -51,58 +62,102 @@ export class ProblemSelectorComponent implements OnInit, OnDestroy, ControlValue //PC2 contest clock. This causes the Component to execute its "loadProblems()" method; that method in turn checks //whether the contest is currently RUNNING; if so, it loads the problem names; if not, it blanks out problem names. - this._contestService.contestClockEvent - .pipe(takeUntil(this._unsubscribe)) - .subscribe(_ => this.loadProblems()); - } - - ngOnDestroy(): void { - this._unsubscribe.next(); - this._unsubscribe.complete(); - } + this._contestService.contestClockEvent + .pipe(takeUntil(this._unsubscribe)) + .subscribe(_ => this.loadProblems()); + } - registerOnChange(fn: (event: any) => void) { - this.onChange = fn; - } + ngOnDestroy(): void { + this._unsubscribe.next(); + this._unsubscribe.complete(); + } - registerOnTouched(fn: (event: any) => void) { - this.onTouched = fn; - } + registerOnChange(fn: (event: any) => void) { + this.onChange = fn; + } - writeValue(value: string) { - this.value = (value === null) ? undefined : value; - } + registerOnTouched(fn: (event: any) => void) { + this.onTouched = fn; + } - private loadProblems(): void { - if (DEBUG_MODE) { - console.log ("Executing ProblemSelectorComponent.loadProblems()") ; - } - if (this._contestService.isContestRunning) { - if (DEBUG_MODE) { - console.log ("ProblemSelectorComponent.loadProblems(): ContestService.isContestRunning() returned positive Truthy value") ; - } - this._contestService.getProblems() - .pipe(takeUntil(this._unsubscribe)) - .subscribe((data: ContestProblem[]) => { - if (DEBUG_MODE) { - console.log ("ProblemSelectorComponent.loadProblems(): subscription callback from ContestService.getProblems() returned data:"); - console.log (data) ; - } - this.problems = data; - }, (error: any) => { - if (DEBUG_MODE) { - console.log ("ProblemSelectorComponent.loadProblems(): subscription callback from ContestService.getProblems() returned error"); - console.log (" Setting contest problem list to empty array." ) ; - } - this.problems = []; - }); - } else { - if (DEBUG_MODE) { - console.log ("ProblemSelectorComponent.loadProblems(): ContestService.isContestRunning() returned negative Truthy value") ; - console.log (" Setting contest problem list to empty array." ) ; + writeValue(value: string) { + this.value = (value === null) ? undefined : value; + } - } - this.problems = []; - } - } + /** + * This method determines whether the contest problems should be displayed in a "ProblemSelectorComponent". + * Problems are only allowed to be displayed when the contest has been started, which is determined by looking + * at the "wallClockStartTime" value in the current ContestClock. The method fetches the current clock from the WTI-API server, + * and if wallClockStartTime is greater than zero it fetches the current problem list from the server and displays it in the + * component; otherwise it sets the list of displayed problems to an empty list (array). + */ + private loadProblems(): void { + if (DEBUG_MODE) { + console.log ("Executing ProblemSelectorComponent.loadProblems()") ; + } + //get the contest clock to see whether problems should be loaded/displayed + this._contestService.getContestClock() + .subscribe( + (contestClock: ContestClock) => { + if (!contestClock) { + console.error ("ProblemSelectorComponent.loadProblems(): ContestService.getContestClock() subscription callback: unable to get ContestClock from PC2 API via ContestService!"); + } else { + if (DEBUG_MODE) { + console.log("ProblemSelectorComponent.loadProblems(): got callback from ContestService.getContestClock() subscription; callback data ="); + console.log(contestClock); + } + //we got back a contest clock; check if the contest has been started + if (this.contestHasBeenStarted(contestClock)) { + //yes, contest has been started; get the problems and display them + this._contestService.getProblems() + .subscribe( + (data: ContestProblem[]) => { + if (DEBUG_MODE) { + console.log ("ProblemSelectorComponent.loadProblems(): subscription callback from ContestService.getProblems() returned data:"); + console.log (data) ; + } + this.problems = data; + }, + (error: any) => { + if (DEBUG_MODE) { + console.log ("ProblemSelectorComponent.loadProblems(): subscription callback from ContestService.getProblems() returned error"); + console.log (" Setting contest problem list to empty array." ) ; + } + console.warn("ProblemSelectorComponent.loadProblems(): error getting contest problems:"); + console.warn(error); + this.problems = []; + } + ); + } else { + if (DEBUG_MODE) { + console.log ("ProblemSelectorComponent.loadProblems(): contest has not been started; setting contest problem list to empty array.") ; + console.log (" Setting contest problem list to empty array." ) ; + } + this.problems = []; + } + + } + }, + (error: any) => { + console.error("ProblemSelectorComponent.loadProblems(): ContestService.getContestClock() subscription callback error: "); + console.error (error); + } + ); + } //end loadProblems() + + /** + * Returns true if the wallClockStartTime in the specified ContestClock is greater than zero (in other words, + * if the contest has been started); false otherwise. + * (Note that wallClockStartTime is the Unix Epoch time at which the contest was first started (if it has been started); + * otherwise (i.e. if the contest has never been started), wallClockStartTime is null in PC2 ContestTime objects + * and such a null value is converted to zero by the WTI-API "/contestclock" endpoint.) + * + */ + private contestHasBeenStarted (contestClock: ContestClock) : boolean { + if (parseInt(contestClock.wallClockStartTime) > 0) { + return true; + } else { + return false; + } + } } From 51ab222b758012d9d8ed971c7c768c062951bc44 Mon Sep 17 00:00:00 2001 From: johnc Date: Tue, 7 Jan 2025 12:19:32 -0800 Subject: [PATCH 129/141] i1027: pass ContestService to ContestTimer constructor; add "TODO" note. --- .../WTI-UI/src/app/modules/core/services/contest.service.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/projects/WTI-UI/src/app/modules/core/services/contest.service.ts b/projects/WTI-UI/src/app/modules/core/services/contest.service.ts index b8840c210..0fbb893ef 100644 --- a/projects/WTI-UI/src/app/modules/core/services/contest.service.ts +++ b/projects/WTI-UI/src/app/modules/core/services/contest.service.ts @@ -20,7 +20,7 @@ export class ContestService extends IContestService { cachedStandings: Observable ; //the WTI-UI timer service which updates on-screen elapsed and remaining time when started (enabled) - contestTimer: ContestTimerService = new ContestTimerService() ; + contestTimer: ContestTimerService = new ContestTimerService(this) ; constructor(private _httpClient: HttpClient) { <<<<<<< Upstream, based on c4b09354e015df5ecedbdcd004dea9810eecc455 @@ -42,6 +42,8 @@ export class ContestService extends IContestService { ======= //set a timer to auto-refresh the contest clock displays, at a rate defined in src/constants + //TODO: what happens to the timing of this update when the browser is minimized (because setInterval() runs slower when + // minimized)? Need to track "most recent update"?? setInterval( //execute this function at the following-specified interval: () => { From 92eefb5f103f49b5e4ecb09964189f69a0a5350e Mon Sep 17 00:00:00 2001 From: johnc Date: Tue, 7 Jan 2025 12:24:12 -0800 Subject: [PATCH 130/141] i1027: Add code to handle time elapsed while browser is minimized; also: added documentation --- .../core/services/contestTimer.service.ts | 69 ++++++++++++++++--- 1 file changed, 60 insertions(+), 9 deletions(-) diff --git a/projects/WTI-UI/src/app/modules/core/services/contestTimer.service.ts b/projects/WTI-UI/src/app/modules/core/services/contestTimer.service.ts index 39a0b053b..7dce5860a 100644 --- a/projects/WTI-UI/src/app/modules/core/services/contestTimer.service.ts +++ b/projects/WTI-UI/src/app/modules/core/services/contestTimer.service.ts @@ -1,5 +1,21 @@ +import { IContestService } from 'src/app/modules/core/abstract-services/i-contest.service'; import { DEBUG_MODE } from 'src/constants' +/** + * This class defines a Timer which updates at a (nominal) rate of once per second. The Timer maintains + * two values: elapsed time and remaining time; these values default to initial values of zero but their + * values are intended to be initialized via external calls to methods setElapsedSec() and setRemainingSecs(). + * Each time the Timer "ticks" these values are updated (incremented, for elapsed time, and decremented, + * for remaining time). + * The Timer uses a JavaScript "setInterval()" to generate update events at the nominal once-per-second + * rate. Since setInterval() is documented to not be guaranteed to operate at its specified rate when + * the browser has lost focus or is minimized, each update event checks to see if there has been a recent + * update; if not, it resets the clocks by invoking the ContestService updateContestClock() method, which + * resynchronizes the Timer's elapsed and remaining time from the server. + * + * The Timer is started by calling method startTimer(); it can be stopped by calling method stopTimer(). + * When stopped and the restarted the Timer picks up counting where it left off. + */ export class ContestTimerService { elapsedSecs = 0 ; //how many seconds the contest has been running (doesn't include any 'paused' time) @@ -7,9 +23,11 @@ export class ContestTimerService { intervalId: ReturnType ; //the id of the JavaScript "interval" used to generate timer ticks isTimerRunning: boolean = false; - constructor() { + mostRecentTimerUpdate: Date = null; + + constructor(private _contestService: IContestService) { if (DEBUG_MODE) { - console.log ("Executing ContestTimerService constructor"); + console.log ("Executing ContestTimerService constructor using IContestService with id ", _contestService.uniqueId); } } @@ -58,6 +76,14 @@ export class ContestTimerService { } } + /** + * This method starts a Timer which fires once per second, updating the "elapsedSecs" and "remainingSecs" variables each second. + * The method uses the JavaScript "setInterval()" method to detect passage of each second (1000 msec). + * Note that setInterval() is not guaranteed to continue operating at the correct rate if the browser is minimized; + * the method compensates for this by saving the current time at each update and then checking to be sure the + * current update is happening less than two seconds since the last update; if not, it invokes the + * ContestService updateContestClock() method to resync the local contest clock with the server. + */ startTimer() { if (this.intervalId) { console.error("ContestTimerService.startTimer(): call to startTimer() when timer is already running"); @@ -70,13 +96,38 @@ export class ContestTimerService { this.intervalId = setInterval( //execute this function at the specified interval: () => { - //bump the elapsed & remaining counters since 1000msec (one sec) has passed - this.elapsedSecs += 1 ; - this.remainingSecs -= 1 -/* if (DEBUG_MODE) { - console.log(`Elapsed secs: ${this.elapsedSecs}; remaining secs: ${this.remainingSecs}`); - } -*/ + + //update the tracking of when the last timer update happened + let now: Date = new Date(); + if (DEBUG_MODE) { + console.log("ContestTimer.startTimer().setInterval() callback: now = ", now.getTime()); + } + if (this.mostRecentTimerUpdate == null) { + //we've never updated the timer; save current update time + if (DEBUG_MODE) { + console.log ("Contest.startTimer().setInterval() callback: mostRecentTimerUpate is null, setting to 'now'"); + } + this.mostRecentTimerUpdate = now ; + } + + //check how long it's been since a timer update has happened (it could be much longer than the 1-second implied by the setInterval() + // rate because that rate can be significantly slowed if the browser has been minimized) + let timeSpan = now.getTime() - this.mostRecentTimerUpdate.getTime(); //epoch times, in milliseconds + if (timeSpan > 1999) { + + //last update was more than 2 seconds ago; update the contest clock (which also updates the timer) + if (DEBUG_MODE) { + console.log("ContestTimer.startTimer().setInterval() callback: timeSpan since last update is ", timeSpan, "; clock is off by more than 2 seconds; calling ContestService.updateContestClock() to update clock from server"); + } + this._contestService.updateContestClock(); + } else { + //we've seen an update within the last two seconds; just update by one second + this.elapsedSecs += 1 ; + this.remainingSecs -= 1 + if (DEBUG_MODE) { + console.log(`Elapsed: ${this.elapsedSecs}; Remaining: ${this.remainingSecs}; intervalId: ${this.intervalId}`); + } + } }, 1000 // 1000 milliseconds = 1 second interval ); From 09163e6872ce39391359fdfc164764b884c90144 Mon Sep 17 00:00:00 2001 From: John Clevenger Date: Tue, 7 Jan 2025 17:08:29 -0800 Subject: [PATCH 131/141] i1027: rename method to updateLocalContestClockFromServer() --- projects/WTI-UI/src/app/app.component.ts | 2 +- .../core/abstract-services/i-contest.service.ts | 2 +- .../core/abstract-services/i-websocket.service.ts | 6 +++++- .../app/modules/core/services/contest.mock.service.ts | 2 +- .../src/app/modules/core/services/contest.service.ts | 10 +++++----- .../components/login-page/login-page.component.ts | 4 ++-- 6 files changed, 15 insertions(+), 11 deletions(-) diff --git a/projects/WTI-UI/src/app/app.component.ts b/projects/WTI-UI/src/app/app.component.ts index 3118ab469..f7cdfdf28 100644 --- a/projects/WTI-UI/src/app/app.component.ts +++ b/projects/WTI-UI/src/app/app.component.ts @@ -185,7 +185,7 @@ export class AppComponent implements OnInit { } //restore the contest clock on-screen display values (elapsed and remaining) - this._contestService.updateContestClock(); + this._contestService.updateLocalContestClockFromServer(); // transfer to the (former) "current page". let page = getCurrentPage(); diff --git a/projects/WTI-UI/src/app/modules/core/abstract-services/i-contest.service.ts b/projects/WTI-UI/src/app/modules/core/abstract-services/i-contest.service.ts index c167cd60a..d4f6102b8 100644 --- a/projects/WTI-UI/src/app/modules/core/abstract-services/i-contest.service.ts +++ b/projects/WTI-UI/src/app/modules/core/abstract-services/i-contest.service.ts @@ -69,7 +69,7 @@ export abstract class IContestService { abstract getContestClock(): Observable; //note this refers to a "ContestClock" model object, not the "contestClockEvent" Subject above - abstract updateContestClock (): void; //update the WTI-UI representation of the PC2 contest clock + abstract updateLocalContestClockFromServer (): void; //update the WTI-UI representation of the PC2 contest clock abstract getElapsedSecs(): number; diff --git a/projects/WTI-UI/src/app/modules/core/abstract-services/i-websocket.service.ts b/projects/WTI-UI/src/app/modules/core/abstract-services/i-websocket.service.ts index 745d422d6..3e25c637e 100644 --- a/projects/WTI-UI/src/app/modules/core/abstract-services/i-websocket.service.ts +++ b/projects/WTI-UI/src/app/modules/core/abstract-services/i-websocket.service.ts @@ -62,13 +62,17 @@ export abstract class IWebsocketService { ======= console.log ("Setting ContestService.isContestRunning to '", val, "',") ; console.log (" invoking ContestService.contestClockEvent.next()") ; +<<<<<<< Upstream, based on c4b09354e015df5ecedbdcd004dea9810eecc455 console.log (" and invoking ContestService.updateContestClock()"); >>>>>>> d43bfce i1027: IWebsocketService: invoke updateContestClock() on 'clock' msg. +======= + console.log (" and invoking ContestService.updateLocalContestClockFromServer()"); +>>>>>>> 7274551 i1027: rename method to updateLocalContestClockFromServer() } this._contestService.isContestRunning = val; this._contestService.contestClockEvent.next(); - this._contestService.updateContestClock(); + this._contestService.updateLocalContestClockFromServer(); }); break; } diff --git a/projects/WTI-UI/src/app/modules/core/services/contest.mock.service.ts b/projects/WTI-UI/src/app/modules/core/services/contest.mock.service.ts index 192dbfe84..0e9d458ae 100644 --- a/projects/WTI-UI/src/app/modules/core/services/contest.mock.service.ts +++ b/projects/WTI-UI/src/app/modules/core/services/contest.mock.service.ts @@ -94,7 +94,7 @@ export class ContestMockService extends IContestService { {isRunning:'true', contestLengthSecs:'18000', elapsedSecs:'3600', wallClockStartTime:'1734593847'}); } - updateContestClock () { + updateLocalContestClockFromServer () { //nothing to do -- this is a mock service } diff --git a/projects/WTI-UI/src/app/modules/core/services/contest.service.ts b/projects/WTI-UI/src/app/modules/core/services/contest.service.ts index 0fbb893ef..ae0ea1fc7 100644 --- a/projects/WTI-UI/src/app/modules/core/services/contest.service.ts +++ b/projects/WTI-UI/src/app/modules/core/services/contest.service.ts @@ -50,7 +50,7 @@ export class ContestService extends IContestService { if (DEBUG_MODE) { console.log("ContestService: resyncing clocks with PC2 Server"); } - this.updateContestClock(); + this.updateLocalContestClockFromServer(); }, RESYNC_INTERVAL_IN_MINUTES * 60 * 1000 // timer interval in msec: minutes * (secs-per-min) * (msec-per-sec) ); @@ -118,7 +118,7 @@ export class ContestService extends IContestService { * and when the subscription callback occurs it uses the received ContestClock data (an instance of * WTI=UI models/ContestClock) to update the WTI-UI contest clock (including the onscreen displays). */ - updateContestClock () { + updateLocalContestClockFromServer () { //get the actual contest clock info from the PC2 server via the Contest Service (which gets it via the WTI Server and its PC2 API) if (DEBUG_MODE) { @@ -128,10 +128,10 @@ export class ContestService extends IContestService { .subscribe( (data: any) => { if (!data) { - console.error ("ContestService.updateContestClock() getContestClock() subscription callback: unable to get ContestClock from PC2 API via ContestService!"); + console.error ("ContestService.updateLocalContestClockFromServer() getContestClock() subscription callback: unable to get ContestClock from PC2 API via ContestService!"); } else { if (DEBUG_MODE) { - console.log("ContestService.updateContestClock(): got callback from getContestClock() subscription; callback data ="); + console.log("ContestService.updateLocalContestClockFromServer(): got callback from getContestClock() subscription; callback data ="); console.log(data); } //install the received contest clock data into the ContestService's ContestClock @@ -139,7 +139,7 @@ export class ContestService extends IContestService { } }, (error: any) => { - console.error("ContestService.updateContestClock(): getContestClock() subscription callback error: "); + console.error("ContestService.updateLocalContestClockFromServer(): getContestClock() subscription callback error: "); console.error (error); } ); diff --git a/projects/WTI-UI/src/app/modules/login/components/login-page/login-page.component.ts b/projects/WTI-UI/src/app/modules/login/components/login-page/login-page.component.ts index 9a39a9876..39da71d29 100644 --- a/projects/WTI-UI/src/app/modules/login/components/login-page/login-page.component.ts +++ b/projects/WTI-UI/src/app/modules/login/components/login-page/login-page.component.ts @@ -169,9 +169,9 @@ export class LoginPageComponent implements OnInit, OnDestroy { //update the WTI-UI representations of the PC2 Contest Clock if (DEBUG_MODE) { - console.log (" Invoking ContestService.updateContestClock()") ; + console.log (" Invoking ContestService.updateLocalContestClockFromServer()") ; } - this._contestService.updateContestClock(); + this._contestService.updateLocalContestClockFromServer(); }, (error: any) => { if (DEBUG_MODE) { From 17a081993207c3861a2315bf54a5b2272503bace Mon Sep 17 00:00:00 2001 From: John Clevenger Date: Tue, 7 Jan 2025 17:13:46 -0800 Subject: [PATCH 132/141] i1027: ContestTimer: always update "mostRecentTimerUpdate"; also: apply renaming of updateContestClock() to updateLocalContestClockFromServer(). --- .../modules/core/services/contestTimer.service.ts | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/projects/WTI-UI/src/app/modules/core/services/contestTimer.service.ts b/projects/WTI-UI/src/app/modules/core/services/contestTimer.service.ts index 7dce5860a..db9f90297 100644 --- a/projects/WTI-UI/src/app/modules/core/services/contestTimer.service.ts +++ b/projects/WTI-UI/src/app/modules/core/services/contestTimer.service.ts @@ -10,7 +10,7 @@ import { DEBUG_MODE } from 'src/constants' * The Timer uses a JavaScript "setInterval()" to generate update events at the nominal once-per-second * rate. Since setInterval() is documented to not be guaranteed to operate at its specified rate when * the browser has lost focus or is minimized, each update event checks to see if there has been a recent - * update; if not, it resets the clocks by invoking the ContestService updateContestClock() method, which + * update; if not, it resets the clocks by invoking the ContestService updateLocalContestClockFromServer() method, which * resynchronizes the Timer's elapsed and remaining time from the server. * * The Timer is started by calling method startTimer(); it can be stopped by calling method stopTimer(). @@ -82,7 +82,7 @@ export class ContestTimerService { * Note that setInterval() is not guaranteed to continue operating at the correct rate if the browser is minimized; * the method compensates for this by saving the current time at each update and then checking to be sure the * current update is happening less than two seconds since the last update; if not, it invokes the - * ContestService updateContestClock() method to resync the local contest clock with the server. + * ContestService updateLocalContestClockFromServer() method to resync the local contest clock with the server. */ startTimer() { if (this.intervalId) { @@ -117,9 +117,10 @@ export class ContestTimerService { //last update was more than 2 seconds ago; update the contest clock (which also updates the timer) if (DEBUG_MODE) { - console.log("ContestTimer.startTimer().setInterval() callback: timeSpan since last update is ", timeSpan, "; clock is off by more than 2 seconds; calling ContestService.updateContestClock() to update clock from server"); + console.log("ContestTimer.startTimer().setInterval() callback: timeSpan since last update is ", timeSpan ); + console.log ("clock is off by more than 2 seconds; calling ContestService.updateLocalContestClockFromServer() to update clock"); } - this._contestService.updateContestClock(); + this._contestService.updateLocalContestClockFromServer(); } else { //we've seen an update within the last two seconds; just update by one second this.elapsedSecs += 1 ; @@ -128,6 +129,10 @@ export class ContestTimerService { console.log(`Elapsed: ${this.elapsedSecs}; Remaining: ${this.remainingSecs}; intervalId: ${this.intervalId}`); } } + + //record that we've now updated the contest clock display + this.mostRecentTimerUpdate = now ; + }, 1000 // 1000 milliseconds = 1 second interval ); From f0d9ee3c6594f5714f2fb279dd97e510c65d4f70 Mon Sep 17 00:00:00 2001 From: johnc Date: Fri, 10 Jan 2025 19:09:54 -0800 Subject: [PATCH 133/141] i1027: removed multiple no-longer-needed debugging outputs. --- .../modules/core/services/contest.service.ts | 11 +------- .../core/services/contestTimer.service.ts | 28 ++----------------- 2 files changed, 4 insertions(+), 35 deletions(-) diff --git a/projects/WTI-UI/src/app/modules/core/services/contest.service.ts b/projects/WTI-UI/src/app/modules/core/services/contest.service.ts index ae0ea1fc7..1c4f93c68 100644 --- a/projects/WTI-UI/src/app/modules/core/services/contest.service.ts +++ b/projects/WTI-UI/src/app/modules/core/services/contest.service.ts @@ -131,8 +131,7 @@ export class ContestService extends IContestService { console.error ("ContestService.updateLocalContestClockFromServer() getContestClock() subscription callback: unable to get ContestClock from PC2 API via ContestService!"); } else { if (DEBUG_MODE) { - console.log("ContestService.updateLocalContestClockFromServer(): got callback from getContestClock() subscription; callback data ="); - console.log(data); + console.log("ContestService.updateLocalContestClockFromServer(): got callback from getContestClock() subscription."); } //install the received contest clock data into the ContestService's ContestClock this.installNewContestClock(data); @@ -172,14 +171,6 @@ export class ContestService extends IContestService { let elapsedSecs = parseInt(this.contestClock.elapsedSecs); let contestLengthSecs = parseInt(this.contestClock.contestLengthSecs); let remainingSecs = contestLengthSecs - elapsedSecs; - - if (DEBUG_MODE) { - console.log ("ContestService.installNewContestClock(): values pulled from newContestClock:"); - console.log (" timerShouldBeStarted = ", timerShouldBeStarted); - console.log (" elapsedSecs = ", elapsedSecs); - console.log (" contestLengthSecs = ", contestLengthSecs); - console.log (" remainingSecs = ", remainingSecs); - } //shut off timer if it is running (otherwise we can't update the elapsed/remaining time values) if (this.contestTimer.isTimerRunning) { diff --git a/projects/WTI-UI/src/app/modules/core/services/contestTimer.service.ts b/projects/WTI-UI/src/app/modules/core/services/contestTimer.service.ts index db9f90297..2a8175059 100644 --- a/projects/WTI-UI/src/app/modules/core/services/contestTimer.service.ts +++ b/projects/WTI-UI/src/app/modules/core/services/contestTimer.service.ts @@ -32,18 +32,10 @@ export class ContestTimerService { } getElapsedSecs(): number { -/* if (DEBUG_MODE) { - console.log("ContestTimerService.getElapsedSecs(): returning ", this.elapsedSecs); - } -*/ return this.elapsedSecs; } getRemainingSecs(): number { -/* if (DEBUG_MODE) { - console.log("ContestTimerService.getRemainingSecs(): returning ", this.remainingSecs); - } -*/ return this.remainingSecs; } @@ -99,35 +91,21 @@ export class ContestTimerService { //update the tracking of when the last timer update happened let now: Date = new Date(); - if (DEBUG_MODE) { - console.log("ContestTimer.startTimer().setInterval() callback: now = ", now.getTime()); - } if (this.mostRecentTimerUpdate == null) { //we've never updated the timer; save current update time - if (DEBUG_MODE) { - console.log ("Contest.startTimer().setInterval() callback: mostRecentTimerUpate is null, setting to 'now'"); - } this.mostRecentTimerUpdate = now ; } //check how long it's been since a timer update has happened (it could be much longer than the 1-second implied by the setInterval() // rate because that rate can be significantly slowed if the browser has been minimized) let timeSpan = now.getTime() - this.mostRecentTimerUpdate.getTime(); //epoch times, in milliseconds - if (timeSpan > 1999) { - - //last update was more than 2 seconds ago; update the contest clock (which also updates the timer) - if (DEBUG_MODE) { - console.log("ContestTimer.startTimer().setInterval() callback: timeSpan since last update is ", timeSpan ); - console.log ("clock is off by more than 2 seconds; calling ContestService.updateLocalContestClockFromServer() to update clock"); - } + if (timeSpan > 4999) { + //last update was more than 5 seconds ago; update the contest clock from the server (which also updates the timer) this._contestService.updateLocalContestClockFromServer(); } else { - //we've seen an update within the last two seconds; just update by one second + //we've seen an update within the last five seconds; just update by one second this.elapsedSecs += 1 ; this.remainingSecs -= 1 - if (DEBUG_MODE) { - console.log(`Elapsed: ${this.elapsedSecs}; Remaining: ${this.remainingSecs}; intervalId: ${this.intervalId}`); - } } //record that we've now updated the contest clock display From bc5bf55f307533c9c10dbd7b0cbd8f7679e1dfbe Mon Sep 17 00:00:00 2001 From: johnc Date: Fri, 10 Jan 2025 19:10:21 -0800 Subject: [PATCH 134/141] i1027: added debug output. --- projects/WTI-UI/src/app/app.component.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/projects/WTI-UI/src/app/app.component.ts b/projects/WTI-UI/src/app/app.component.ts index f7cdfdf28..6845d3ae5 100644 --- a/projects/WTI-UI/src/app/app.component.ts +++ b/projects/WTI-UI/src/app/app.component.ts @@ -185,6 +185,9 @@ export class AppComponent implements OnInit { } //restore the contest clock on-screen display values (elapsed and remaining) + if (DEBUG_MODE) { + console.log ('Updating local contest clock values from server'); + } this._contestService.updateLocalContestClockFromServer(); // transfer to the (former) "current page". From e0ff6d1ab58f94703e1506421854261445d0f908 Mon Sep 17 00:00:00 2001 From: johnc Date: Fri, 10 Jan 2025 20:37:02 -0800 Subject: [PATCH 135/141] i1027: added documentation (no code changes) --- .../abstract-services/i-websocket.service.ts | 5 +++++ .../modules/core/services/contest.service.ts | 16 ++++++++++++++++ .../core/services/contestTimer.service.ts | 7 +++++-- .../core/services/displayTimePipe.service.ts | 13 +++++++------ .../app-header/app-header.component.ts | 17 +++++++++-------- 5 files changed, 42 insertions(+), 16 deletions(-) diff --git a/projects/WTI-UI/src/app/modules/core/abstract-services/i-websocket.service.ts b/projects/WTI-UI/src/app/modules/core/abstract-services/i-websocket.service.ts index 3e25c637e..855e4049e 100644 --- a/projects/WTI-UI/src/app/modules/core/abstract-services/i-websocket.service.ts +++ b/projects/WTI-UI/src/app/modules/core/abstract-services/i-websocket.service.ts @@ -43,6 +43,11 @@ export abstract class IWebsocketService { break; } case 'contest_clock': { + /* This case is invoked when the WTI Server gets a "Contest_Clock" message from the PC2 Server + * (via the PC2 API "ConfigurationItemUpdated()" listener in the WTI class "ConfigurationService"). + * Such a message from the PC2 Server causes the WTI Server to send a "contest_clock" message through + * the websocket to this client. + */ if (DEBUG_MODE) { console.log ("IWebsocketService.incomingMessage(): got websocket message '", message.type); console.log ("IWebsocketService.incomingMessage(): invoking ContestService.getIsContestRunning() and"); diff --git a/projects/WTI-UI/src/app/modules/core/services/contest.service.ts b/projects/WTI-UI/src/app/modules/core/services/contest.service.ts index 1c4f93c68..539e70404 100644 --- a/projects/WTI-UI/src/app/modules/core/services/contest.service.ts +++ b/projects/WTI-UI/src/app/modules/core/services/contest.service.ts @@ -11,6 +11,22 @@ import { Clarification } from '../models/clarification'; import { DEBUG_MODE } from 'src/constants'; import { RESYNC_INTERVAL_IN_MINUTES } from 'src/constants'; +/** + * This class provides a variety of "contest-related" services for clients. + * It provides methods for initiating HTTP calls to the WTI Server to obtain contest + * information (such as languages, problems, clarifications, standings, and the current + * contest clock value); each of these services returns an "Observable" to which the invoking + * client can "subscribe" to receive a callback when the actual data is available from the HTTP call. + * + * The class also provides localized services such as: + * + * - keeping track of whether the current WTI-UI understanding of the contest standings (scoreboard) + * are current (it maintains a cache of standings and keeps track of events which could cause the cache to be out-of-date); + * - a set of methods for providing clients with the current contest elapsed and remaining times, together with methods + * to force the local copy of the contest clock to be updated from the server and methods to load a new value into the + * local contest clock; + * - a local timer which forces the local (on-screen) contest clock to be resynced with the server periodically. + */ @Injectable({ providedIn: 'root' //forces the service to be a singleton across all app components ('root' == "root injector") }) diff --git a/projects/WTI-UI/src/app/modules/core/services/contestTimer.service.ts b/projects/WTI-UI/src/app/modules/core/services/contestTimer.service.ts index 2a8175059..e90df4354 100644 --- a/projects/WTI-UI/src/app/modules/core/services/contestTimer.service.ts +++ b/projects/WTI-UI/src/app/modules/core/services/contestTimer.service.ts @@ -13,8 +13,11 @@ import { DEBUG_MODE } from 'src/constants' * update; if not, it resets the clocks by invoking the ContestService updateLocalContestClockFromServer() method, which * resynchronizes the Timer's elapsed and remaining time from the server. * - * The Timer is started by calling method startTimer(); it can be stopped by calling method stopTimer(). - * When stopped and the restarted the Timer picks up counting where it left off. + * The Timer is started by calling method startTimer(); it can be stopped by calling method stopTimer(); + * these methods are intended to be invoked by external code which detects when the PC2 Admin starts/stops + * the PC2 contest clock. + * + * When stopped and then restarted the Timer picks up counting where it left off. */ export class ContestTimerService { diff --git a/projects/WTI-UI/src/app/modules/core/services/displayTimePipe.service.ts b/projects/WTI-UI/src/app/modules/core/services/displayTimePipe.service.ts index 8307df7ad..eef6b9d32 100644 --- a/projects/WTI-UI/src/app/modules/core/services/displayTimePipe.service.ts +++ b/projects/WTI-UI/src/app/modules/core/services/displayTimePipe.service.ts @@ -1,12 +1,13 @@ import { Pipe, PipeTransform } from '@angular/core'; import { DEBUG_MODE } from 'src/constants' -//This class defines a "pipe" which accepts a time, represented as a number of seconds, and returns -// a formatted string representing the number of days/hours/minutes/seconds represented by the input number of seconds. -//"Days" are only included in the output if they are greater than zero. -//The "pipe" is suitable for use, for example, in an Angular HTML template wanting to display an elapsed number of seconds -// in terms of hours/mins/secs (see app-header.component.html). - +/** This class defines a "pipe" which accepts a time, represented as a number of seconds, and returns + * a formatted string representing the number of days/hours/minutes/seconds represented by the input number of seconds. + * "Days" are only included in the output if they are greater than zero. + * The "pipe" is suitable for use, for example, in an Angular HTML template wanting to display an elapsed number of seconds + * in terms of hours/mins/secs (see app-header.component.html). + */ + @Pipe({ name: 'displayTime', standalone: true diff --git a/projects/WTI-UI/src/app/modules/shared/components/app-header/app-header.component.ts b/projects/WTI-UI/src/app/modules/shared/components/app-header/app-header.component.ts index 89cb6b543..2023a9fca 100644 --- a/projects/WTI-UI/src/app/modules/shared/components/app-header/app-header.component.ts +++ b/projects/WTI-UI/src/app/modules/shared/components/app-header/app-header.component.ts @@ -3,6 +3,15 @@ import { AuthService } from 'src/app/modules/core/auth/auth.service'; import { IContestService } from 'src/app/modules/core/abstract-services/i-contest.service'; import { DEBUG_MODE } from 'src/constants' +/** + * This class defines a component which acts as the "header" for all WTI-UI pages. Together with the + * corresponding app-header.component.html file, the component displays header images (such as the PC2 Balloon Logo). + * If there is currently a team logged in, then the header component also shows + * links to the various WTI-UI pages (such as Runs, Clarifications, Scoreboard, etc.), the currently logged-in team's ID, + * and clocks showing the current contest elapsed time and remaining time (whose values it gets from the + * injected ContestService). + * + */ @Component({ selector: 'app-header', templateUrl: './app-header.component.html', @@ -36,19 +45,11 @@ export class AppHeaderComponent { getElapsedSecs(): number { let secs = this._contestService.getElapsedSecs() ; -/* if (DEBUG_MODE) { - console.log("AppHeaderComponent.getElapsedSecs(): returning ", secs); - } -*/ return secs; } getRemainingSecs(): number { let secs = this._contestService.getRemainingSecs() ; -/* if (DEBUG_MODE) { - console.log("AppHeaderComponent.getRemainingSecs(): returning ", secs); - } -*/ return secs; } } From 25a7f217e574a423db62ac1fd4c58776c5455a24 Mon Sep 17 00:00:00 2001 From: johnc Date: Sat, 11 Jan 2025 11:39:18 -0800 Subject: [PATCH 136/141] i1027: update core.module.ts as part of rebase onto develop. --- .../src/app/modules/core/core.module.ts | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/projects/WTI-UI/src/app/modules/core/core.module.ts b/projects/WTI-UI/src/app/modules/core/core.module.ts index d5a59b229..28972de67 100644 --- a/projects/WTI-UI/src/app/modules/core/core.module.ts +++ b/projects/WTI-UI/src/app/modules/core/core.module.ts @@ -16,11 +16,8 @@ import { IWebsocketService } from './abstract-services/i-websocket.service'; import { UiHelperService } from './services/ui-helper.service'; import { SharedModule } from '../shared/shared.module'; import { DEBUG_MODE } from 'src/constants'; -<<<<<<< Upstream, based on c4b09354e015df5ecedbdcd004dea9810eecc455 import { CommonModule } from '@angular/common'; -======= import { DisplayTimePipe } from './services/displayTimePipe.service'; ->>>>>>> 7a71e77 i1027: update for compatibility with PR for i871. export function TeamsServiceFactory(http: HttpClient) { if (DEBUG_MODE) { @@ -93,34 +90,18 @@ export function WebsocketServiceFactory(injector: Injector, { provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true }, { provide: IWebsocketService, useFactory: WebsocketServiceFactory, deps: [Injector, UiHelperService, IContestService, ITeamsService, AuthService] }, AuthGuard, - //TODO: should the following two still be declared here since they are now listed in the above "deps" list? - UiHelperService, -<<<<<<< Upstream, based on c4b09354e015df5ecedbdcd004dea9810eecc455 -======= DisplayTimePipe, //TODO: should the following two still be declared here since they are now listed in the above "deps" list? UiHelperService, -<<<<<<< Upstream, based on c4b09354e015df5ecedbdcd004dea9810eecc455 - ContestService, - //TODO: should the following two still be declared here since they are now listed in the above "deps" list? - UiHelperService, ->>>>>>> 7a71e77 i1027: update for compatibility with PR for i871. -======= ->>>>>>> db34af4 i1027: source code cleanup (remove outdated comments, etc.) ContestService ], imports: [ HttpClientModule, -<<<<<<< Upstream, based on c4b09354e015df5ecedbdcd004dea9810eecc455 - SharedModule - ], -======= SharedModule, DisplayTimePipe ], exports: [ DisplayTimePipe ] ->>>>>>> 7a71e77 i1027: update for compatibility with PR for i871. }) export class CoreModule { } From b98c8374a340493c07b787f57351bdcc74225df7 Mon Sep 17 00:00:00 2001 From: johnc Date: Sat, 11 Jan 2025 12:17:50 -0800 Subject: [PATCH 137/141] i1027: fix conflict errors during rebase. --- projects/WTI-UI/src/app/app.component.ts | 4 ---- .../core/abstract-services/i-contest.service.ts | 12 ------------ .../core/abstract-services/i-websocket.service.ts | 13 ------------- .../modules/core/services/contest.mock.service.ts | 1 - .../app/modules/core/services/contest.service.ts | 11 ----------- .../components/login-page/login-page.component.ts | 15 ++------------- 6 files changed, 2 insertions(+), 54 deletions(-) diff --git a/projects/WTI-UI/src/app/app.component.ts b/projects/WTI-UI/src/app/app.component.ts index 6845d3ae5..c64f3ce91 100644 --- a/projects/WTI-UI/src/app/app.component.ts +++ b/projects/WTI-UI/src/app/app.component.ts @@ -159,11 +159,7 @@ export class AppComponent implements OnInit { console.log (this._contestService); } this._contestService.isContestRunning = val; -<<<<<<< Upstream, based on c4b09354e015df5ecedbdcd004dea9810eecc455 - this._contestService.contestClock.next(); -======= this._contestService.contestClockEvent.next(); ->>>>>>> c622bdd i1027: AppComponent: add support matching i871-F5-in-WTI. }); //get the most recently saved Option values from sessionStorage diff --git a/projects/WTI-UI/src/app/modules/core/abstract-services/i-contest.service.ts b/projects/WTI-UI/src/app/modules/core/abstract-services/i-contest.service.ts index d4f6102b8..6c4d89c94 100644 --- a/projects/WTI-UI/src/app/modules/core/abstract-services/i-contest.service.ts +++ b/projects/WTI-UI/src/app/modules/core/abstract-services/i-contest.service.ts @@ -38,18 +38,6 @@ export abstract class IContestService { public uniqueId: number; - constructor () { - this.uniqueId = IContestService.nextId++; - if (DEBUG_MODE) { - console.log ("Executing IContestService constructor for unique instance ", this.uniqueId) ; - } - } - - //give each instance of IContestService a unique ID for debugging purposes - private static nextId: number = 1; - public uniqueId: number; - - constructor () { this.uniqueId = IContestService.nextId++; if (DEBUG_MODE) { diff --git a/projects/WTI-UI/src/app/modules/core/abstract-services/i-websocket.service.ts b/projects/WTI-UI/src/app/modules/core/abstract-services/i-websocket.service.ts index 855e4049e..ee61ea0ac 100644 --- a/projects/WTI-UI/src/app/modules/core/abstract-services/i-websocket.service.ts +++ b/projects/WTI-UI/src/app/modules/core/abstract-services/i-websocket.service.ts @@ -57,22 +57,9 @@ export abstract class IWebsocketService { .subscribe((val: any) => { if (DEBUG_MODE) { console.log ("IWebsocketService.incomingMessage(): callback from ContestService.getIsContestRunning() returned '", val, "'"); -<<<<<<< Upstream, based on c4b09354e015df5ecedbdcd004dea9810eecc455 - console.log ("Setting ContestService.isContestRunning to '", val, "'") ; -<<<<<<< Upstream, based on c4b09354e015df5ecedbdcd004dea9810eecc455 - console.log (" and invoking ContestService.contestClock.next()") ; -======= - console.log (" and invoking ContestService.contestClockEvent.next()") ; ->>>>>>> 5291696 i1027: IWebSocketService: make root-injectable; add debug output. -======= console.log ("Setting ContestService.isContestRunning to '", val, "',") ; console.log (" invoking ContestService.contestClockEvent.next()") ; -<<<<<<< Upstream, based on c4b09354e015df5ecedbdcd004dea9810eecc455 - console.log (" and invoking ContestService.updateContestClock()"); ->>>>>>> d43bfce i1027: IWebsocketService: invoke updateContestClock() on 'clock' msg. -======= console.log (" and invoking ContestService.updateLocalContestClockFromServer()"); ->>>>>>> 7274551 i1027: rename method to updateLocalContestClockFromServer() } this._contestService.isContestRunning = val; diff --git a/projects/WTI-UI/src/app/modules/core/services/contest.mock.service.ts b/projects/WTI-UI/src/app/modules/core/services/contest.mock.service.ts index 0e9d458ae..b23399d32 100644 --- a/projects/WTI-UI/src/app/modules/core/services/contest.mock.service.ts +++ b/projects/WTI-UI/src/app/modules/core/services/contest.mock.service.ts @@ -4,7 +4,6 @@ import { Observable, of } from 'rxjs'; import { ContestLanguage } from '../models/contest-language'; import { ContestProblem } from '../models/contest-problem'; import { Clarification } from '../models/clarification'; -import { DEBUG_MODE } from 'src/constants'; import { ContestClock } from '../models/contest-clock'; import { DEBUG_MODE } from 'src/constants'; diff --git a/projects/WTI-UI/src/app/modules/core/services/contest.service.ts b/projects/WTI-UI/src/app/modules/core/services/contest.service.ts index 539e70404..5e6203a8e 100644 --- a/projects/WTI-UI/src/app/modules/core/services/contest.service.ts +++ b/projects/WTI-UI/src/app/modules/core/services/contest.service.ts @@ -39,7 +39,6 @@ export class ContestService extends IContestService { contestTimer: ContestTimerService = new ContestTimerService(this) ; constructor(private _httpClient: HttpClient) { -<<<<<<< Upstream, based on c4b09354e015df5ecedbdcd004dea9810eecc455 super(); if (DEBUG_MODE) { @@ -47,15 +46,6 @@ export class ContestService extends IContestService { } this.standingsAreCurrent = false; -======= - super(); - if (DEBUG_MODE) { - console.log ("Executing ContestService constructor; instance ID = ", this.uniqueId) ; - } - this.standingsAreCurrent = false; -<<<<<<< Upstream, based on c4b09354e015df5ecedbdcd004dea9810eecc455 ->>>>>>> ab9b09a i1027: ContestService: make root-injectable; add ContestTimer; also: -======= //set a timer to auto-refresh the contest clock displays, at a rate defined in src/constants //TODO: what happens to the timing of this update when the browser is minimized (because setInterval() runs slower when @@ -70,7 +60,6 @@ export class ContestService extends IContestService { }, RESYNC_INTERVAL_IN_MINUTES * 60 * 1000 // timer interval in msec: minutes * (secs-per-min) * (msec-per-sec) ); ->>>>>>> b069333 i1027: add code to resync UI clocks with PC2 periodically. } getLanguages(): Observable { diff --git a/projects/WTI-UI/src/app/modules/login/components/login-page/login-page.component.ts b/projects/WTI-UI/src/app/modules/login/components/login-page/login-page.component.ts index 39da71d29..03f06fd24 100644 --- a/projects/WTI-UI/src/app/modules/login/components/login-page/login-page.component.ts +++ b/projects/WTI-UI/src/app/modules/login/components/login-page/login-page.component.ts @@ -128,12 +128,10 @@ export class LoginPageComponent implements OnInit, OnDestroy { if (DEBUG_MODE) { console.log ("Invoking ContestService.contestClock.next()") ; } -<<<<<<< Upstream, based on c4b09354e015df5ecedbdcd004dea9810eecc455 - this._contestService.contestClock.next(); -======= + //trigger a contestClockEvent so the SelectProblems dropdown can decide whether to display the problems or not this._contestService.contestClockEvent.next(); ->>>>>>> f172644 i1027: LoginPageComponent: add comments (no code changes). + }); }, (error: any) => { if (DEBUG_MODE) { @@ -165,21 +163,12 @@ export class LoginPageComponent implements OnInit, OnDestroy { console.error (error); } ); - //update the WTI-UI representations of the PC2 Contest Clock if (DEBUG_MODE) { console.log (" Invoking ContestService.updateLocalContestClockFromServer()") ; } this._contestService.updateLocalContestClockFromServer(); - - }, (error: any) => { - if (DEBUG_MODE) { - console.error ("AuthService.login() subscription returned error:", error) ; - } - this.invalidCreds = true; - this.loginStarted = false; - }); } private buildForm(): void { From 515bfb2c1ebc56ace75d4b192c0c5e11ca6cae84 Mon Sep 17 00:00:00 2001 From: John Clevenger Date: Thu, 6 Feb 2025 13:53:33 -0800 Subject: [PATCH 138/141] i1027: updated data types as requested in PR review. --- .../app/modules/core/services/contest.service.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/projects/WTI-UI/src/app/modules/core/services/contest.service.ts b/projects/WTI-UI/src/app/modules/core/services/contest.service.ts index 5e6203a8e..c0528a271 100644 --- a/projects/WTI-UI/src/app/modules/core/services/contest.service.ts +++ b/projects/WTI-UI/src/app/modules/core/services/contest.service.ts @@ -131,7 +131,7 @@ export class ContestService extends IContestService { } this.getContestClock() .subscribe( - (data: any) => { + (data: ContestClock) => { if (!data) { console.error ("ContestService.updateLocalContestClockFromServer() getContestClock() subscription callback: unable to get ContestClock from PC2 API via ContestService!"); } else { @@ -142,7 +142,7 @@ export class ContestService extends IContestService { this.installNewContestClock(data); } }, - (error: any) => { + (error: unknown) => { console.error("ContestService.updateLocalContestClockFromServer(): getContestClock() subscription callback error: "); console.error (error); } @@ -172,10 +172,10 @@ export class ContestService extends IContestService { this.contestClock = newContestClock; //pull the values out of the updated clock - let timerShouldBeStarted = this.contestClock.isRunning ; - let elapsedSecs = parseInt(this.contestClock.elapsedSecs); - let contestLengthSecs = parseInt(this.contestClock.contestLengthSecs); - let remainingSecs = contestLengthSecs - elapsedSecs; + const timerShouldBeStarted = this.contestClock.isRunning ; + const elapsedSecs = parseInt(this.contestClock.elapsedSecs); + const contestLengthSecs = parseInt(this.contestClock.contestLengthSecs); + const remainingSecs = contestLengthSecs - elapsedSecs; //shut off timer if it is running (otherwise we can't update the elapsed/remaining time values) if (this.contestTimer.isTimerRunning) { @@ -202,7 +202,7 @@ export class ContestService extends IContestService { * ContestTimer object. */ getElapsedSecs(): number { - let elapsedSecs = this.contestTimer.getElapsedSecs(); + const elapsedSecs = this.contestTimer.getElapsedSecs(); return elapsedSecs ; } @@ -210,7 +210,7 @@ export class ContestService extends IContestService { * ContestTimer object. */ getRemainingSecs(): number { - let remainingSecs = this.contestTimer.getRemainingSecs(); + const remainingSecs = this.contestTimer.getRemainingSecs(); return remainingSecs ; } } From 8af6b4c9874060b376ee512a1f54bd8df596d760 Mon Sep 17 00:00:00 2001 From: John Clevenger Date: Thu, 6 Feb 2025 13:55:58 -0800 Subject: [PATCH 139/141] i1027: change data types, remove debug prints for clarity. --- .../login-page/login-page.component.ts | 66 ++----------------- 1 file changed, 6 insertions(+), 60 deletions(-) diff --git a/projects/WTI-UI/src/app/modules/login/components/login-page/login-page.component.ts b/projects/WTI-UI/src/app/modules/login/components/login-page/login-page.component.ts index b36c0df44..f940d20a3 100644 --- a/projects/WTI-UI/src/app/modules/login/components/login-page/login-page.component.ts +++ b/projects/WTI-UI/src/app/modules/login/components/login-page/login-page.component.ts @@ -49,123 +49,69 @@ export class LoginPageComponent implements OnInit, OnDestroy { private _router: Router, private _contestService: IContestService, private _appTitleService: AppTitleService) { - - if (DEBUG_MODE) { - console.log ("Executing LoginPageComponent constructor") ; - } - } + } ngOnInit(): void { - if (DEBUG_MODE) { - console.log ("Executing LoginPageComponent.ngOnInit()") ; - } - this._appTitleService.setTitleWithTeamId("Login"); if (this._authService.token) { - if (DEBUG_MODE) { - console.log (" AuthService.token returns positive Truthy value; invoking Router to navigate to ") ; - console.log (" AuthService.defaultRoute '", this._authService.defaultRoute, "'") ; - } this._router.navigateByUrl(this._authService.defaultRoute); } - if (DEBUG_MODE) { - console.log (" Invoking buildForm() " ) ; - } this.buildForm(); } ngOnDestroy(): void { - if (DEBUG_MODE) { - console.log ("Executing LoginPageComponent.ngOnDestroy(); invoking _unsubscribe.next() and then _unsubscribe.complete()") ; - } this._unsubscribe.next(); this._unsubscribe.complete(); } onSubmit(): void { - if (DEBUG_MODE) { - console.log ("Executing LoginPageComponent.onSubmit()...") ; - } this.loginStarted = true; const loginCreds = new LoginCredentials(); loginCreds.teamName = this.formGroup.get('username').value; loginCreds.password = this.formGroup.get('password').value; - if (DEBUG_MODE) { - console.log ("Invoking AuthService.login()") ; - } this._authService.login(loginCreds) .pipe(takeUntil(this._unsubscribe)) .subscribe((result: TeamsLoginResponse) => { - if (DEBUG_MODE) { - console.log ("Received callback from subscribing to AuthService.login();" ) ; - console.log (" Invoking AuthService.completeLogin()") ; - } this._authService.completeLogin(result.teamId, result.teamName); - if (DEBUG_MODE) { - console.log (" Invoking WebsocketService.startWebsocket()") ; - } this._websocketService.startWebsocket(); - if (DEBUG_MODE) { - console.log (" Invoking ContestService.getisContestRunning() and subscribing to the result") ; - } this._contestService.getIsContestRunning() .subscribe((val: boolean) => { - if (DEBUG_MODE) { - console.log (" Subscription callback from ContestService.getIsContestRunning() returned: ", val); - console.log (" ContestService object has uniqueId", this._contestService.uniqueId); - //JSON.stringify() can't handle recursive objects like Angular Subjects - //console.log (JSON.stringify(this._contestService,null,2)); //obj, replacerFunction, indent - console.log (" and contents:"); - console.log (this._contestService); - } - if (DEBUG_MODE) { - console.log ("Setting ContestService.isContestRunning to '", val, "'") ; - } this._contestService.isContestRunning = val; - if (DEBUG_MODE) { - console.log ("Invoking ContestService.contestClockEvent.next()") ; - } //trigger a contestClockEvent so the SelectProblems dropdown can decide whether to display the problems or not this._contestService.contestClockEvent.next(); }); //get the actual contest clock info from the PC2 server via the Contest Service (which gets it via the WTI Server and its PC2 API) - if (DEBUG_MODE) { - console.log(" Invoking ContestService.getContestClock(), subscribing for callback result") - } this._contestService.getContestClock() .subscribe( - (data: any) => { + (data: ContestClock) => { if (!data) { console.error ("LoginPageComponent.onSubmit() ContestClock subscription callback: unable to get ContestClock from PC2 API via ContestService!"); } else { //copy the data fields received from the PC2 Server (via the WTI-API) into the local ContestClock object - this.contestClock.isRunning = data.running ; - this.contestClock.contestLengthSecs = data.contestLengthInSecs ; + this.contestClock.isRunning = data.isRunning ; + this.contestClock.contestLengthSecs = data.contestLengthSecs ; this.contestClock.elapsedSecs = data.elapsedSecs ; this.contestClock.wallClockStartTime = data.wallClockStartTime ; } }, - (error: any) => { + (error: unknown) => { console.error("LoginPageComponent.onSubmit(): getContestClock() subscription callback error: "); console.error (error); } ); //update the WTI-UI representations of the PC2 Contest Clock - if (DEBUG_MODE) { - console.log (" Invoking ContestService.updateLocalContestClockFromServer()") ; - } this._contestService.updateLocalContestClockFromServer(); - }, (error: any) => { + }, (error: unknown) => { console.error ("LoginPageComponent.onSubmit(): AuthService.login() subscription callback error: "); console.error(error); this.invalidCreds = true; From 89e39529a48db203d4ac60913e7ee7a13e26b7cb Mon Sep 17 00:00:00 2001 From: John Clevenger Date: Thu, 6 Feb 2025 13:59:13 -0800 Subject: [PATCH 140/141] i1027: fix data types, remove debug prints, minor reformatting. --- .../problem-selector.component.ts | 44 +++++-------------- 1 file changed, 10 insertions(+), 34 deletions(-) diff --git a/projects/WTI-UI/src/app/modules/shared/components/problem-selector/problem-selector.component.ts b/projects/WTI-UI/src/app/modules/shared/components/problem-selector/problem-selector.component.ts index fefda144d..5c1885a51 100644 --- a/projects/WTI-UI/src/app/modules/shared/components/problem-selector/problem-selector.component.ts +++ b/projects/WTI-UI/src/app/modules/shared/components/problem-selector/problem-selector.component.ts @@ -5,7 +5,6 @@ import { IContestService } from 'src/app/modules/core/abstract-services/i-contes import { ContestClock } from 'src/app/modules/core/models/contest-clock'; import { Subject } from 'rxjs'; import { takeUntil } from 'rxjs/operators'; -import { DEBUG_MODE } from 'src/constants'; /** * This class defines a component which displays a drop-down list containing the current contest problems. @@ -21,31 +20,27 @@ import { DEBUG_MODE } from 'src/constants'; selector: 'app-problem-selector', templateUrl: './problem-selector.component.html', providers: [ -{ -provide: NG_VALUE_ACCESSOR, -useExisting: forwardRef(() => ProblemSelectorComponent), -multi: true -} -] + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => ProblemSelectorComponent), + multi: true + } + ] }) export class ProblemSelectorComponent implements OnInit, OnDestroy, ControlValueAccessor { private _unsubscribe = new Subject(); @Input() allowGeneral = false; problems: ContestProblem[] = []; + + //fields required to implement "ControlValueAccessor" value: string; onChange = (event: any) => { }; onTouched = (event: any) => { }; constructor(private _contestService: IContestService) { - if (DEBUG_MODE) { - console.log ("Executing ProblemSelectorComponent constructor; IContestService id = ", this._contestService.uniqueId) ; - } } ngOnInit(): void { - if (DEBUG_MODE) { - console.log ("Executing ProblemSelectorComponent.ngOnInit()") ; - } this.loadProblems(); if (this.allowGeneral) { this.writeValue('general'); @@ -92,9 +87,6 @@ export class ProblemSelectorComponent implements OnInit, OnDestroy, ControlValue * component; otherwise it sets the list of displayed problems to an empty list (array). */ private loadProblems(): void { - if (DEBUG_MODE) { - console.log ("Executing ProblemSelectorComponent.loadProblems()") ; - } //get the contest clock to see whether problems should be loaded/displayed this._contestService.getContestClock() .subscribe( @@ -102,43 +94,27 @@ export class ProblemSelectorComponent implements OnInit, OnDestroy, ControlValue if (!contestClock) { console.error ("ProblemSelectorComponent.loadProblems(): ContestService.getContestClock() subscription callback: unable to get ContestClock from PC2 API via ContestService!"); } else { - if (DEBUG_MODE) { - console.log("ProblemSelectorComponent.loadProblems(): got callback from ContestService.getContestClock() subscription; callback data ="); - console.log(contestClock); - } //we got back a contest clock; check if the contest has been started if (this.contestHasBeenStarted(contestClock)) { //yes, contest has been started; get the problems and display them this._contestService.getProblems() .subscribe( (data: ContestProblem[]) => { - if (DEBUG_MODE) { - console.log ("ProblemSelectorComponent.loadProblems(): subscription callback from ContestService.getProblems() returned data:"); - console.log (data) ; - } this.problems = data; }, - (error: any) => { - if (DEBUG_MODE) { - console.log ("ProblemSelectorComponent.loadProblems(): subscription callback from ContestService.getProblems() returned error"); - console.log (" Setting contest problem list to empty array." ) ; - } + (error: unknown) => { console.warn("ProblemSelectorComponent.loadProblems(): error getting contest problems:"); console.warn(error); this.problems = []; } ); } else { - if (DEBUG_MODE) { - console.log ("ProblemSelectorComponent.loadProblems(): contest has not been started; setting contest problem list to empty array.") ; - console.log (" Setting contest problem list to empty array." ) ; - } this.problems = []; } } }, - (error: any) => { + (error: unknown) => { console.error("ProblemSelectorComponent.loadProblems(): ContestService.getContestClock() subscription callback error: "); console.error (error); } From 012426ee2d9878599f2f7cc8596046fa14792b60 Mon Sep 17 00:00:00 2001 From: John Clevenger Date: Thu, 20 Feb 2025 13:28:43 -0800 Subject: [PATCH 141/141] i1027: requested updates per PR review, including: - use concrete data types rather than "any". - remove debug statements. - use "const" instead of "let" in variable declarations where applicable. --- .../modules/core/services/contest.service.ts | 51 +++---------------- .../app-header/app-header.component.ts | 8 +-- 2 files changed, 11 insertions(+), 48 deletions(-) diff --git a/projects/WTI-UI/src/app/modules/core/services/contest.service.ts b/projects/WTI-UI/src/app/modules/core/services/contest.service.ts index c0528a271..a3f1fac95 100644 --- a/projects/WTI-UI/src/app/modules/core/services/contest.service.ts +++ b/projects/WTI-UI/src/app/modules/core/services/contest.service.ts @@ -8,7 +8,6 @@ import { ContestProblem } from '../models/contest-problem'; import { ContestClock } from '../models/contest-clock'; import { ContestTimerService } from './contestTimer.service' ; import { Clarification } from '../models/clarification'; -import { DEBUG_MODE } from 'src/constants'; import { RESYNC_INTERVAL_IN_MINUTES } from 'src/constants'; /** @@ -41,10 +40,6 @@ export class ContestService extends IContestService { constructor(private _httpClient: HttpClient) { super(); - if (DEBUG_MODE) { - console.log ("Executing ContestService constructor; instance ID = ", this.uniqueId) ; - } - this.standingsAreCurrent = false; //set a timer to auto-refresh the contest clock displays, at a rate defined in src/constants @@ -53,9 +48,6 @@ export class ContestService extends IContestService { setInterval( //execute this function at the following-specified interval: () => { - if (DEBUG_MODE) { - console.log("ContestService: resyncing clocks with PC2 Server"); - } this.updateLocalContestClockFromServer(); }, RESYNC_INTERVAL_IN_MINUTES * 60 * 1000 // timer interval in msec: minutes * (secs-per-min) * (msec-per-sec) @@ -79,9 +71,6 @@ export class ContestService extends IContestService { } getIsContestRunning(): Observable { - if (DEBUG_MODE) { - console.log ("ContestService.getIsContestRunning(): invoking HTTP get(.../contest.isRunning) WTI-API endpoint") ; - } return this._httpClient.get(`${environment.baseUrl}/contest/isRunning`); } @@ -93,20 +82,10 @@ export class ContestService extends IContestService { } getStandings(): Observable { - if (DEBUG_MODE) { - console.log("ContestService.getStandings():") - } if (!this.standingsAreCurrent) { - if (DEBUG_MODE) { - console.log ("Standings are out of date; fetching new standings"); - } this.cachedStandings = this._httpClient.get(`${environment.baseUrl}/contest/scoreboard`); this.standingsAreCurrent = true ; - } else { - if (DEBUG_MODE) { - console.log("Returning cached standings"); - } - } + } return this.cachedStandings ; } @@ -126,20 +105,14 @@ export class ContestService extends IContestService { updateLocalContestClockFromServer () { //get the actual contest clock info from the PC2 server via the Contest Service (which gets it via the WTI Server and its PC2 API) - if (DEBUG_MODE) { - console.log(" Invoking ContestService.getContestClock(), subscribing for HTTP callback result") - } this.getContestClock() .subscribe( - (data: ContestClock) => { - if (!data) { + (contestClock: ContestClock) => { + if (!contestClock) { console.error ("ContestService.updateLocalContestClockFromServer() getContestClock() subscription callback: unable to get ContestClock from PC2 API via ContestService!"); } else { - if (DEBUG_MODE) { - console.log("ContestService.updateLocalContestClockFromServer(): got callback from getContestClock() subscription."); - } //install the received contest clock data into the ContestService's ContestClock - this.installNewContestClock(data); + this.installNewContestClock(contestClock); } }, (error: unknown) => { @@ -155,20 +128,16 @@ export class ContestService extends IContestService { * if the received data indicates the clock should be running it starts the ContestTimer (which then genrates a "clock tick" * once per second to update the clock displays). */ - installNewContestClock(data: any) { + installNewContestClock(data: ContestClock) { //copy the data fields (received from the PC2 Server via the WTI-API) into a new ContestService ContestClock object let newContestClock = new ContestClock(); - newContestClock.isRunning = data.running ; - newContestClock.contestLengthSecs = data.contestLengthInSecs ; + newContestClock.isRunning = data.isRunning ; + newContestClock.contestLengthSecs = data.contestLengthSecs ; newContestClock.elapsedSecs = data.elapsedSecs ; newContestClock.wallClockStartTime = data.wallClockStartTime ; //save the new clock - if (DEBUG_MODE) { - console.log("ContestService (id", this.uniqueId, ").installNewContestClock(): replacing Contest Clock with:"); - console.log(newContestClock); - } this.contestClock = newContestClock; //pull the values out of the updated clock @@ -179,9 +148,6 @@ export class ContestService extends IContestService { //shut off timer if it is running (otherwise we can't update the elapsed/remaining time values) if (this.contestTimer.isTimerRunning) { - if (DEBUG_MODE) { - console.log("ContestService (id", this.uniqueId, ").installNewContestClock(): stopping timer"); - } this.contestTimer.stopTimer(); } @@ -191,9 +157,6 @@ export class ContestService extends IContestService { //restart the timer if the new contest clock values indicate it should be running if (timerShouldBeStarted) { - if (DEBUG_MODE) { - console.log("ContestService (id", this.uniqueId, ").installNewContestClock(): starting timer"); - } this.contestTimer.startTimer(); } } diff --git a/projects/WTI-UI/src/app/modules/shared/components/app-header/app-header.component.ts b/projects/WTI-UI/src/app/modules/shared/components/app-header/app-header.component.ts index 2023a9fca..2787af872 100644 --- a/projects/WTI-UI/src/app/modules/shared/components/app-header/app-header.component.ts +++ b/projects/WTI-UI/src/app/modules/shared/components/app-header/app-header.component.ts @@ -31,8 +31,8 @@ export class AppHeaderComponent { /* Return a string containing the "team id" -- that is, the PC2 team account number with the leading "team" removed */ get teamId(): string { - let acctId = this._authService.username; - let teamId = acctId.substr(4); + const acctId = this._authService.username; + const teamId = acctId.substr(4); return teamId; } @@ -44,12 +44,12 @@ export class AppHeaderComponent { getElapsedSecs(): number { - let secs = this._contestService.getElapsedSecs() ; + const secs = this._contestService.getElapsedSecs() ; return secs; } getRemainingSecs(): number { - let secs = this._contestService.getRemainingSecs() ; + const secs = this._contestService.getRemainingSecs() ; return secs; } }