diff --git a/projects/gameboard-mks/src/app/api.service.ts b/projects/gameboard-mks/src/app/api.service.ts index 4da1de47..2f69622d 100644 --- a/projects/gameboard-mks/src/app/api.service.ts +++ b/projects/gameboard-mks/src/app/api.service.ts @@ -5,9 +5,9 @@ import { PlatformLocation } from '@angular/common'; import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { interval, Observable, of } from 'rxjs'; -import { catchError, filter, map, switchMap } from 'rxjs/operators'; +import { catchError, map, switchMap } from 'rxjs/operators'; import { environment } from '../environments/environment'; -import { ConsoleActionResponse, ConsoleActor, ConsoleRequest, ConsoleSummary, KeyValuePair, VmAnswer, VmOperation, VmOptions } from './api.models'; +import { ConsoleActionResponse, ConsoleActor, ConsoleRequest, ConsoleSummary, KeyValuePair, VmAnswer, VmOptions } from './api.models'; import { UserActivityListenerEventType } from './components/user-activity-listener/user-activity-listener.component'; @Injectable({ providedIn: 'root' }) diff --git a/projects/gameboard-mks/src/app/components/console/console.component.ts b/projects/gameboard-mks/src/app/components/console/console.component.ts index 673ecc0a..6a440e3a 100644 --- a/projects/gameboard-mks/src/app/components/console/console.component.ts +++ b/projects/gameboard-mks/src/app/components/console/console.component.ts @@ -18,6 +18,7 @@ import { ClipboardService } from '../../clipboard.service'; import { HubService } from '../../hub.service'; import { UserActivityListenerEventType } from '../user-activity-listener/user-activity-listener.component'; import { ConfigService } from '@/utility/config.service'; +import { LogService } from '@/services/log.service'; @Component({ selector: 'app-console', @@ -63,6 +64,7 @@ export class ConsoleComponent implements AfterViewInit, OnDestroy { private config: ConfigService, private injector: Injector, private api: ApiService, + private logService: LogService, private titleSvc: Title, private clipSvc: ClipboardService, private renderer: Renderer2 @@ -134,6 +136,7 @@ export class ConsoleComponent implements AfterViewInit, OnDestroy { } this.state = state; + this.logService.logInfo("State to", state); this.shadowState(state); this.isConnected = state === 'connected'; @@ -181,6 +184,8 @@ export class ConsoleComponent implements AfterViewInit, OnDestroy { (info: ConsoleSummary) => this.create(info), (err) => { const msg = err?.error?.message || err?.message || err; + this.logService.logError(`Console reload error: ${msg || "[no error resolved]"}`); + this.changeState( msg.match(/forbidden/i) ? 'forbidden' : 'failed' ); @@ -338,12 +343,6 @@ export class ConsoleComponent implements AfterViewInit, OnDestroy { } } - @HostListener('window:blur', ['$event']) - onBlur(): void { - // don't set actor map on blur - // this.api.blur(this.request); - } - @HostListener('document:mouseup', ['$event']) dragged(): void { this.audiencePos = null; diff --git a/projects/gameboard-ui/src/app/api/challenges.service.ts b/projects/gameboard-ui/src/app/api/challenges.service.ts index e27ed200..a9e3f8ae 100644 --- a/projects/gameboard-ui/src/app/api/challenges.service.ts +++ b/projects/gameboard-ui/src/app/api/challenges.service.ts @@ -10,7 +10,7 @@ import { PlayerMode } from './player-models'; @Injectable({ providedIn: 'root' }) export class ChallengesService { - private _challengeDeployStateChanged$ = new Subject(); + private _challengeDeployStateChanged$ = new Subject(); public readonly challengeDeployStateChanged$ = this._challengeDeployStateChanged$.asObservable(); private _challengeGraded$ = new Subject(); @@ -67,13 +67,13 @@ export class ChallengesService { public deploy(challenge: { id: string }): Observable { return this.http.put(this.apiUrl.build("challenge/start"), challenge).pipe(tap(challenge => { - this._challengeDeployStateChanged$.next(challenge.id); + this._challengeDeployStateChanged$.next(challenge); })); } public undeploy(challenge: { id: string }): Observable { return this.http.put(this.apiUrl.build("challenge/stop"), challenge).pipe(tap(challenge => { - this._challengeDeployStateChanged$.next(challenge.id); + this._challengeDeployStateChanged$.next(challenge); })); } diff --git a/projects/gameboard-ui/src/app/api/team.service.ts b/projects/gameboard-ui/src/app/api/team.service.ts index 97dbf28c..ee7407c3 100644 --- a/projects/gameboard-ui/src/app/api/team.service.ts +++ b/projects/gameboard-ui/src/app/api/team.service.ts @@ -82,8 +82,9 @@ export class TeamService { this._teamSessionEndedManually$.next(request.teamId); } - public extendSession(model: SessionExtendRequest): Observable { - return from(this.updateSession(model)); + public async extendSession(model: SessionExtendRequest): Promise { + await this.updateSession(model); + this._teamSessionExtended$.next([model.teamId]); } public resetSession(request: { teamId: string, resetType?: TeamSessionResetType }): Observable { @@ -97,11 +98,8 @@ export class TeamService { } private async updateSession(request: SessionExtendRequest | SessionEndRequest): Promise { - await firstValueFrom( - this.http.put(this.apiUrl.build("/team/session"), request).pipe( - tap(_ => this._teamSessionsChanged$.next([request.teamId])), - tap(_ => this._playerSessionChanged$.next(request.teamId)), - ) - ); + await firstValueFrom(this.http.put(this.apiUrl.build("/team/session"), request)); + this._playerSessionChanged$.next(request.teamId); + this._teamSessionsChanged$.next([request.teamId]); } } diff --git a/projects/gameboard-ui/src/app/game/components/play/play.component.html b/projects/gameboard-ui/src/app/game/components/play/play.component.html index 39ba2947..189032fb 100644 --- a/projects/gameboard-ui/src/app/game/components/play/play.component.html +++ b/projects/gameboard-ui/src/app/game/components/play/play.component.html @@ -89,7 +89,7 @@

Challenge Questions

-
@@ -100,13 +100,13 @@

Challenge Questions

-
+
diff --git a/projects/gameboard-ui/src/app/prac/components/practice-challenge-state-summary/practice-challenge-state-summary.component.ts b/projects/gameboard-ui/src/app/prac/components/practice-challenge-state-summary/practice-challenge-state-summary.component.ts index 58afee29..781fffa7 100644 --- a/projects/gameboard-ui/src/app/prac/components/practice-challenge-state-summary/practice-challenge-state-summary.component.ts +++ b/projects/gameboard-ui/src/app/prac/components/practice-challenge-state-summary/practice-challenge-state-summary.component.ts @@ -66,7 +66,7 @@ export class PracticeChallengeStateSummaryComponent { // update timers to accurately reflect the active challenge this.msElapsed$ = this._timer$.pipe( - map(tick => + map(() => this.userActivePracticeChallenge?.session.start ? DateTime.now().diff(this.userActivePracticeChallenge.session.start).toMillis() : undefined @@ -82,10 +82,10 @@ export class PracticeChallengeStateSummaryComponent { async extendSession(practiceChallenge: LocalActiveChallenge): Promise { this.isChangingSessionEnd = true; const teamId = practiceChallenge.teamId; - await firstValueFrom(this.teamService.extendSession({ + await this.teamService.extendSession({ teamId, sessionEnd: new Date() - })); + }); this.isChangingSessionEnd = false; this.showExtensionToast(DateTime.now().plus({ minutes: 60 })); diff --git a/projects/gameboard-ui/src/app/stores/active-challenges.store.ts b/projects/gameboard-ui/src/app/stores/active-challenges.store.ts index 6c9c7434..9e32453b 100644 --- a/projects/gameboard-ui/src/app/stores/active-challenges.store.ts +++ b/projects/gameboard-ui/src/app/stores/active-challenges.store.ts @@ -1,11 +1,10 @@ import { Injectable, OnDestroy } from "@angular/core"; import { createStore, withProps } from "@ngneat/elf"; import { DateTime } from "luxon"; -import { Observable, Subject, Subscription, combineLatest, firstValueFrom, interval, map, merge, of, startWith, switchMap } from "rxjs"; +import { Observable, Subject, Subscription, firstValueFrom, interval, merge, of, startWith, switchMap } from "rxjs"; import { LocalActiveChallenge } from "@/api/challenges.models"; import { SimpleEntity } from "@/api/models"; import { ChallengesService } from "@/api/challenges.service"; -import { PlayerService } from "@/api/player.service"; import { Challenge } from "@/api/board-models"; import { TeamService } from "@/api/team.service"; import { UserService as LocalUserService } from "@/utility/user.service"; @@ -44,28 +43,23 @@ export class ActiveChallengesRepo implements OnDestroy { constructor( challengesService: ChallengesService, - playerService: PlayerService, localUser: LocalUserService, teamService: TeamService) { this._subs.push( - combineLatest([ + merge([ of(challengesService), localUser.user$, merge([ challengesService.challengeGraded$, - challengesService.challengeDeployStateChanged$, - playerService.playerSessionReset$, - teamService.playerSessionChanged$, - teamService.teamSessionsChanged$ + challengesService.challengeDeployStateChanged$ ]) - ]).pipe( - map(([challengesService]) => ({ challengesService })), - ).subscribe(ctx => this._initState(ctx.challengesService, localUser.user$.value?.id || null)), + ]).subscribe(() => this._initState(challengesService, localUser.user$.value?.id || null)), interval(1000).subscribe(() => this.checkActiveChallengesForEnd()), challengesService.challengeGraded$.subscribe(challenge => this.handleChallengeGraded(challenge)), - teamService.teamSessionEndedManually$.subscribe(tId => this.handleTeamSessionEndedManually(tId)) - + challengesService.challengeDeployStateChanged$.subscribe(challenge => this.handleChallengeDeployStateChanged(challenge)), + teamService.teamSessionEndedManually$.subscribe(tId => this.handleTeamSessionEndedManually(tId)), + teamService.teamSessionsChanged$.subscribe(tId => this._initState(challengesService, localUser.user$.value?.id || null)) ); this._initState(challengesService, localUser.user$.value?.id || null); @@ -81,6 +75,15 @@ export class ActiveChallengesRepo implements OnDestroy { return this.resolveActivePracticeChallenge(activeChallengesStore.value); } + private buildChallengeDeployment(challenge: Challenge) { + return { + challengeId: challenge.id, + isDeployed: challenge.hasDeployedGamespace, + markdown: challenge.state.markdown || "", + vms: challenge.state.vms, + }; + } + private checkActiveChallengesForEnd() { const challenges = [...activeChallengesStore.state.practice]; @@ -96,6 +99,31 @@ export class ActiveChallengesRepo implements OnDestroy { } } + private handleChallengeDeployStateChanged(challengeDeployStateChange: Challenge) { + activeChallengesStore.update(state => { + const competitive = [...state.competition]; + const practice = [...state.practice]; + + for (const challenge of competitive) { + if (challenge.id == challengeDeployStateChange.id) { + challenge.challengeDeployment = this.buildChallengeDeployment(challengeDeployStateChange); + } + } + + for (const challenge of practice) { + if (challenge.id === challengeDeployStateChange.id) { + challenge.challengeDeployment = this.buildChallengeDeployment(challengeDeployStateChange); + } + } + + return { + ...state, + competition: [...competitive], + practice: [...practice] + }; + }); + } + private handleChallengeGraded(challenge: Challenge) { // check if the graded challenge is the active practice challenge, and if it is, evaluate it for completeness // and grading attempts, and notify appropriate subjects @@ -103,7 +131,12 @@ export class ActiveChallengesRepo implements OnDestroy { if (activePracticeChallenge?.challengeDeployment.challengeId === challenge.id) { // no matter what, update the activeChallenge thing in state with the deploy state of the // challenge's gamespace - activePracticeChallenge.challengeDeployment.isDeployed = challenge.state.isActive; + activePracticeChallenge.challengeDeployment = { + challengeId: challenge.id, + isDeployed: challenge.hasDeployedGamespace, + markdown: challenge.state.markdown || "", + vms: challenge.state.vms, + }; let removeChallengeFromState = false; if (challenge.score >= challenge.points) { @@ -128,13 +161,6 @@ export class ActiveChallengesRepo implements OnDestroy { this.removeFromActiveWithPredicate(c => c.teamId == teamId); } - private getAllChallenges() { - return [ - ...activeChallengesStore.state.competition, - ...activeChallengesStore.state.practice - ]; - } - private removeFromActive(endedChallenge: Challenge | LocalActiveChallenge) { this.removeFromActiveWithPredicate(c => c.id === endedChallenge.id); } @@ -151,7 +177,7 @@ export class ActiveChallengesRepo implements OnDestroy { private async _initState(challengesService: ChallengesService, localUserId: string | null) { if (!localUserId) { - activeChallengesStore.update(state => DEFAULT_STATE); + activeChallengesStore.update(() => DEFAULT_STATE); return; }