import {
Component, ElementRef, OnDestroy, OnInit, ViewChild
} from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { Sort } from '@angular/material/sort';
import { MatSidenav } from '@angular/material/sidenav';
import { interval, Observable, Subscription } from 'rxjs';
import { MatDialog } from '@angular/material/dialog';
import { ConfirmDialogComponent, ConfirmDialogData, CustomtextService } from 'iqb-components';
import { MatSlideToggleChange } from '@angular/material/slide-toggle';
import { MatCheckboxChange } from '@angular/material/checkbox';
import { switchMap } from 'rxjs/operators';
import { BackendService } from './backend.service';
import {
GroupData,
TestViewDisplayOptions,
TestViewDisplayOptionKey, Selected, TestSession, TestSessionSetStats, CommandResponse, UIMessage, isBooklet
} from './group-monitor.interfaces';
import { TestSessionManager } from './test-session-manager/test-session-manager.service';
import { ConnectionStatus } from '../shared/websocket-backend.service';
import { BookletUtil } from './booklet/booklet.util';
import { MainDataService } from '../maindata.service';
@Component({
selector: 'app-group-monitor',
templateUrl: './group-monitor.component.html',
styleUrls: ['./group-monitor.component.css']
})
export class GroupMonitorComponent implements OnInit, OnDestroy {
connectionStatus$: Observable<ConnectionStatus>;
ownGroup$: Observable<GroupData>;
private ownGroupName = '';
selectedElement: Selected;
markedElement: Selected;
displayOptions: TestViewDisplayOptions = {
view: 'medium',
groupColumn: 'hide',
bookletColumn: 'show',
blockColumn: 'show',
unitColumn: 'hide',
highlightSpecies: false,
manualChecking: false
};
isScrollable = false;
isClosing = false;
messages: UIMessage[] = [];
private subscriptions: Subscription[] = [];
@ViewChild('adminbackground') mainElem: ElementRef;
@ViewChild('sidenav', { static: true }) sidenav: MatSidenav;
constructor(
public dialog: MatDialog,
private route: ActivatedRoute,
private bs: BackendService,
public tsm: TestSessionManager,
private router: Router,
private cts: CustomtextService,
public mds: MainDataService
) {}
ngOnInit(): void {
this.subscriptions = [
this.route.params.subscribe(params => {
this.ownGroup$ = this.bs.getGroupData(params['group-name']);
this.ownGroupName = params['group-name'];
this.tsm.connect(params['group-name']);
}),
this.tsm.sessionsStats$.subscribe(stats => {
this.onSessionsUpdate(stats);
}),
this.tsm.checkedStats$.subscribe(stats => {
this.onCheckedChange(stats);
}),
this.tsm.commandResponses$.subscribe(commandResponse => {
this.messages.push(this.commandResponseToMessage(commandResponse));
}),
this.tsm.commandResponses$
.pipe(switchMap(() => interval(7000)))
.subscribe(() => this.messages.shift())
];
this.connectionStatus$ = this.bs.connectionStatus$;
this.mds.appSubTitle$.next(this.cts.getCustomText('gm_headline'));
}
private commandResponseToMessage(commandResponse: CommandResponse): UIMessage {
const command = this.cts.getCustomText(`gm_control_${commandResponse.commandType}`) || commandResponse.commandType;
const successWarning = this.cts.getCustomText(`gm_control_${commandResponse.commandType}_success_warning`) || '';
if (!commandResponse.testIds.length) {
return {
level: 'warning',
text: 'Keine Tests Betroffen von: `%s`',
customtext: 'gm_message_no_session_affected_by_command',
replacements: [command, commandResponse.testIds.length.toString(10)]
};
}
return {
level: successWarning ? 'warning' : 'info',
text: '`%s` an `%s` tests gesendet! %s',
customtext: 'gm_message_command_sent_n_sessions',
replacements: [command, commandResponse.testIds.length.toString(10), successWarning]
};
}
ngOnDestroy(): void {
this.tsm.disconnect();
this.subscriptions.forEach(subscription => subscription.unsubscribe());
}
ngAfterViewChecked(): void {
this.isScrollable = this.mainElem.nativeElement.clientHeight < this.mainElem.nativeElement.scrollHeight;
}
private onSessionsUpdate(stats: TestSessionSetStats): void {
this.displayOptions.highlightSpecies = (stats.differentBookletSpecies > 1);
if (!this.tsm.checkingOptions.enableAutoCheckAll) {
this.displayOptions.manualChecking = true;
}
}
private onCheckedChange(stats: TestSessionSetStats): void {
if (stats.differentBookletSpecies > 1) {
this.selectedElement = null;
}
}
trackSession = (index: number, session: TestSession): number => session.data.testId;
setTableSorting(sort: Sort): void {
if (!sort.active || sort.direction === '') {
return;
}
this.tsm.sortBy$.next(sort);
}
setDisplayOption(option: string, value: TestViewDisplayOptions[TestViewDisplayOptionKey]): void {
this.displayOptions[option] = value;
}
scrollDown(): void {
this.mainElem.nativeElement.scrollTo(0, this.mainElem.nativeElement.scrollHeight);
}
updateScrollHint(): void {
const elem = this.mainElem.nativeElement;
const reachedBottom = (elem.scrollTop + elem.clientHeight === elem.scrollHeight);
elem.classList[reachedBottom ? 'add' : 'remove']('hide-scroll-hint');
}
getSessionColor(session: TestSession): string {
const stripes = (c1, c2) => `repeating-linear-gradient(45deg, ${c1}, ${c1} 10px, ${c2} 10px, ${c2} 20px)`;
const hsl = (h, s, l) => `hsl(${h}, ${s}%, ${l}%)`;
const colorful = this.displayOptions.highlightSpecies && session.booklet.species;
const h = colorful ? (
session.booklet.species.length *
session.booklet.species.charCodeAt(0) *
session.booklet.species.charCodeAt(session.booklet.species.length / 4) *
session.booklet.species.charCodeAt(session.booklet.species.length / 4) *
session.booklet.species.charCodeAt(session.booklet.species.length / 2) *
session.booklet.species.charCodeAt(3 * (session.booklet.species.length / 4)) *
session.booklet.species.charCodeAt(session.booklet.species.length - 1)
) % 360 : 0;
switch (session.state) {
case 'paused':
return hsl(h, colorful ? 45 : 0, 90);
case 'pending':
return stripes(hsl(h, colorful ? 75 : 0, 95), hsl(h, 0, 98));
case 'locked':
return stripes(hsl(h, colorful ? 75 : 0, 95), hsl(0, 0, 92));
case 'error':
return stripes(hsl(h, colorful ? 75 : 0, 95), hsl(0, 30, 95));
default:
return hsl(h, colorful ? 75 : 0, colorful ? 95 : 100);
}
}
markElement(marking: Selected): void {
this.markedElement = marking;
}
selectElement(selected: Selected): void {
this.tsm.checkSessionsBySelection(selected);
this.selectedElement = selected;
}
finishEverythingCommand(): void {
const dialogRef = this.dialog.open(ConfirmDialogComponent, {
width: 'auto',
data: <ConfirmDialogData>{
title: 'Testdurchführung Beenden',
content: 'Achtung! Diese Aktion sperrt und beendet sämtliche Tests dieser Sitzung.',
confirmbuttonlabel: 'Ja, ich möchte die Testdurchführung Beenden',
showcancel: true
}
});
dialogRef.afterClosed().subscribe((confirmed: boolean) => {
if (confirmed) {
this.isClosing = true;
this.tsm.commandFinishEverything()
.subscribe(() => {
setTimeout(() => { this.router.navigateByUrl('/r/login'); }, 5000); // go away
});
}
});
}
testCommandGoto(): void {
if (!this.selectedElement?.element?.blockId) {
this.messages.push({
level: 'warning',
customtext: 'gm_test_command_no_selected_block',
text: 'Kein Zielblock ausgewählt'
});
} else {
this.tsm.testCommandGoto(this.selectedElement)
.subscribe(() => this.selectNextBlock());
}
}
private selectNextBlock(): void {
if (!isBooklet(this.selectedElement.originSession.booklet)) {
return;
}
this.selectedElement = {
element: this.selectedElement.element.nextBlockId ?
BookletUtil.getBlockById(
this.selectedElement.element.nextBlockId,
this.selectedElement.originSession.booklet
) : null,
inversion: false,
originSession: this.selectedElement.originSession,
spreading: this.selectedElement.spreading
};
}
unlockCommand(): void {
this.tsm.testCommandUnlock();
}
toggleChecked(checked: boolean, session: TestSession): void {
if (!this.tsm.isChecked(session)) {
this.tsm.checkSession(session);
} else {
this.tsm.uncheckSession(session);
}
}
invertChecked(event: Event): boolean {
event.preventDefault();
this.tsm.invertChecked();
return false;
}
toggleAlwaysCheckAll(event: MatSlideToggleChange): void {
if (this.tsm.checkingOptions.enableAutoCheckAll && event.checked) {
this.tsm.checkAll();
this.displayOptions.manualChecking = false;
this.tsm.checkingOptions.autoCheckAll = true;
} else {
this.tsm.checkNone();
this.displayOptions.manualChecking = true;
this.tsm.checkingOptions.autoCheckAll = false;
}
}
toggleCheckAll(event: MatCheckboxChange): void {
if (event.checked) {
this.tsm.checkAll();
} else {
this.tsm.checkNone();
}
}
}
<div class="page-header">
<p>
{{mds.appTitle$ | async}} {{mds.appSubTitle$ | async}} -
<span *ngIf="ownGroup$ | async as ownGroup">{{ownGroup.label}}</span>
</p>
<span class="fill-remaining-space"></span>
<p>
<mat-chip-list *ngIf="connectionStatus$ | async as connectionStatus">
<mat-chip [class]="connectionStatus + ' connection-status'">
<mat-icon>
{{connectionStatus === 'error' ? 'error' : ''}}
{{connectionStatus === 'polling-fetch' ? 'loop' : ''}}
{{connectionStatus === 'polling-sleep' ? 'loop' : ''}}
{{connectionStatus === 'ws-offline' ? 'loop' : ''}}
{{connectionStatus === 'ws-online' ? 'wifi_tethering' : ''}}
</mat-icon>
{{connectionStatus === 'error' ? 'Offline' : ''}}
{{connectionStatus === 'polling-fetch' ? 'Online' : ''}}
{{connectionStatus === 'polling-sleep' ? 'Online' : ''}}
{{connectionStatus === 'ws-offline' ? 'Reconn.' : ''}}
{{connectionStatus === 'ws-online' ? 'Live' : ''}}
</mat-chip>
</mat-chip-list>
</p>
</div>
<mat-menu #rootMenu="matMenu">
<button mat-menu-item [matMenuTriggerFor]="filters">
{{'Sitzungen ausblenden' | customtext:'gm_menu_filter' | async}}
</button>
<button mat-menu-item [matMenuTriggerFor]="group">
{{'Spalten' | customtext:'gm_menu_cols' | async}}
</button>
<button mat-menu-item [matMenuTriggerFor]="activity">
{{'Aktivität' | customtext:'gm_menu_activity' | async}}
</button>
</mat-menu>
<mat-menu #filters="matMenu">
<button mat-menu-item *ngFor="let filterOption of tsm.filterOptions; let i = index" (click)="tsm.switchFilter(i)">
<mat-icon *ngIf="filterOption.selected">check</mat-icon>
<span>{{filterOption.label | customtext:filterOption.label | async}}</span>
</button>
</mat-menu>
<mat-menu #group="matMenu">
<button mat-menu-item (click)="setDisplayOption('groupColumn', (displayOptions.groupColumn === 'hide') ? 'show' : 'hide')">
<mat-icon *ngIf="displayOptions.groupColumn === 'show'">check</mat-icon>
<span>{{'Gruppe' | customtext:'gm_col_group' | async}}</span>
</button>
<button mat-menu-item (click)="setDisplayOption('bookletColumn', (displayOptions.bookletColumn === 'hide') ? 'show' : 'hide')">
<mat-icon *ngIf="displayOptions.bookletColumn === 'show'">check</mat-icon>
<span>{{'Testheft' | customtext:'gm_col_booklet' | async}}</span>
</button>
<button mat-menu-item (click)="setDisplayOption('blockColumn', (displayOptions.blockColumn === 'hide') ? 'show' : 'hide')">
<mat-icon *ngIf="displayOptions.blockColumn === 'show'">check</mat-icon>
<span>{{'Block' | customtext:'gm_col_testlet' | async}}</span>
</button>
<button mat-menu-item (click)="setDisplayOption('unitColumn', (displayOptions.unitColumn === 'hide') ? 'show' : 'hide')">
<mat-icon *ngIf="displayOptions.unitColumn === 'show'">check</mat-icon>
<span>{{'Aufgabe' | customtext:'gm_col_unit' | async}}</span>
</button>
</mat-menu>
<mat-menu #activity="matMenu">
<button mat-menu-item (click)="setDisplayOption('view', 'full')">
<mat-icon *ngIf="displayOptions.view === 'full'">check</mat-icon>
<span>{{'Vollständig' | customtext:'gm_view_full' | async}}</span>
</button>
<button mat-menu-item (click)="setDisplayOption('view', 'medium')">
<mat-icon *ngIf="displayOptions.view === 'medium'">check</mat-icon>
<span>{{'Nur Blöcke' | customtext:'gm_view_medium' | async}}</span>
</button>
<button mat-menu-item (click)="setDisplayOption('view', 'small')">
<mat-icon *ngIf="displayOptions.view === 'small'">check</mat-icon>
<span>{{'Kurz' | customtext:'gm_view_small' | async}}</span>
</button>
</mat-menu>
<div class="page-body">
<mat-sidenav-container>
<mat-sidenav #sidenav opened="true" mode="side" class="toolbar" fixedInViewport="true" fixedTopGap="55">
<h2>{{'Test-Steuerung' | customtext:'gm_controls' | async}}</h2>
<div class="toolbar-section" *ngIf="tsm.sessionsStats$ | async as sessionsStats">
<mat-slide-toggle
color="accent"
(change)="toggleAlwaysCheckAll($event)"
[disabled]="!tsm.checkingOptions.enableAutoCheckAll"
[checked]="tsm.checkingOptions.autoCheckAll"
[matTooltip]="(sessionsStats.differentBookletSpecies > 1) ? (
'Die verwendeten Booklets sind zu unterschiedlich, um gemeinsam gesteuert zu werden.'
| customtext:'gm_multiple_booklet_species_warning'
| async
) : null"
>
{{'Alle Tests gleichzeitig steuern' | customtext:'gm_auto_checkall' | async }}
</mat-slide-toggle>
</div>
<div class="toolbar-section min-height-section">
<ng-container *ngIf="displayOptions.manualChecking">
<ng-container *ngIf="tsm.checkedStats$ | async as checkedStats">
<alert
*ngIf="checkedStats.number; else noCheckedSession"
level="info"
customtext="gm_selection_info"
text="%s %s Test%s mit %s Testheft%s ausgewählt."
[replacements]="[
(checkedStats.all ? ' Alle' : ''),
checkedStats.number.toString(10),
(checkedStats.number !== 1 ? 's' : ''),
checkedStats.differentBooklets.toString(10),
(checkedStats.differentBooklets !== 1 ? 'en' : '')
]"
></alert>
<ng-template #noCheckedSession>
<alert level="info" customtext="gm_selection_info_none" text="Kein Test ausgewählt."></alert>
</ng-template>
</ng-container>
</ng-container>
</div>
<div class="toolbar-section">
<button mat-raised-button class="control" color="primary" (click)="tsm.testCommandResume()">
<mat-icon>play_arrow</mat-icon>
{{'weiter' | customtext:'gm_control_resume' | async}}
</button>
<button mat-raised-button class="control" color="primary" (click)="tsm.testCommandPause()">
<mat-icon>pause</mat-icon>
{{'pause' | customtext:'gm_control_pause' | async}}
</button>
</div>
<div class="toolbar-section">
<button
mat-raised-button
class="control"
color="primary"
(click)="testCommandGoto()"
[matTooltip]="selectedElement?.element?.blockId ? null : ('Bitte Block auswählen' | customtext:'gm_control_goto_tooltip' | async)"
>
<mat-icon>arrow_forward</mat-icon>
{{'Springe zu' | customtext:'gm_control_goto' | async}}
<span class="emph">{{selectedElement?.element?.blockId}}</span>
</button>
</div>
<div class="toolbar-section">
<button
mat-raised-button
class="control"
color="primary"
(click)="unlockCommand()"
[matTooltip]="'Freigeben' | customtext:'gm_control_unlock_tooltip' | async"
>
<mat-icon>lock_open</mat-icon>
{{'Test Entsperren' | customtext:'gm_control_unlock' | async}}
</button>
</div>
<div id="message-panel" class="toolbar-section">
<alert
*ngFor="let m of messages"
[text]="m.text"
[level]="m.level"
customtext="m.customtext"
[replacements]="m.replacements"
></alert>
</div>
<div class="toolbar-section toolbar-section-bottom">
<button mat-raised-button class="control" color="primary" (click)="finishEverythingCommand()">
<mat-icon>stop</mat-icon>{{'Testung beenden' | customtext:'gm_control_finish_everything' | async}}
</button>
</div>
</mat-sidenav>
<mat-sidenav-content>
<div #adminbackground class="adminbackground" (scroll)="updateScrollHint()">
<div class="corner-menu">
<button
class="settings-button"
mat-icon-button
[matMenuTriggerFor]="rootMenu"
[matTooltip]="'Ansicht' | customtext:'gm_settings_tooltip' | async"
matTooltipPosition="above"
>
<mat-icon>settings</mat-icon>
</button>
</div>
<div class="scroll-hint" *ngIf="isScrollable">
<button
mat-icon-button
(click)="scrollDown()"
[matTooltip]="'Ganz nach unten' | customtext:'gm_scroll_down' | async"
matTooltipPosition="above"
>
<mat-icon>keyboard_arrow_down</mat-icon>
</button>
</div>
<div class="test-session-table-wrapper">
<table class="test-session-table" matSort (matSortChange)="setTableSorting($event)">
<thead>
<tr class="mat-sort-container">
<td mat-sort-header="_checked" *ngIf="displayOptions.manualChecking">
<mat-checkbox
*ngIf="tsm.checkedStats$ | async as checkedStats"
(click)="$event.stopPropagation()"
(change)="toggleCheckAll($event)"
[checked]="checkedStats.all"
(contextmenu)="invertChecked($event)"
></mat-checkbox>
</td>
<td mat-sort-header="_superState">
<mat-icon>person</mat-icon>
</td>
<td mat-sort-header="groupLabel" *ngIf="displayOptions.groupColumn === 'show'">
{{'Gruppe' | customtext:'gm_col_group' | async}}
</td>
<td mat-sort-header="personLabel">
{{'Teilnehmer' | customtext:'gm_col_person' | async}}
</td>
<td mat-sort-header="bookletName" *ngIf="displayOptions.bookletColumn === 'show'">
{{'Testheft' | customtext:'gm_col_booklet' | async}}
</td>
<td mat-sort-header="_currentBlock" *ngIf="displayOptions.blockColumn === 'show'">
{{'Block' | customtext:'gm_col_testlet' | async}}
</td>
<td mat-sort-header="timestamp">
{{'Aktivität' | customtext:'gm_col_activity' | async}}
</td>
<td mat-sort-header="_currentUnit" *ngIf="displayOptions.unitColumn === 'show'">
{{'Aufgabe' | customtext:'gm_col_unit' | async}}
</td>
</tr>
</thead>
<ng-container *ngFor="let session of tsm.sessions$ | async; trackBy: trackSession">
<tc-test-session
[testSession]="session"
[displayOptions]="displayOptions"
[marked]="markedElement"
(markedElement$)="markElement($event)"
[selected]="selectedElement"
(selectedElement$)="selectElement($event)"
[checked]="tsm.isChecked(session)"
(checked$)="toggleChecked($event, session)"
[ngStyle]="{background: getSessionColor(session)}"
>
</tc-test-session>
</ng-container>
</table>
</div>
</div>
</mat-sidenav-content>
</mat-sidenav-container>
<button
class="drawer-button-close"
mat-icon-button
(click)="sidenav.toggle()"
matTooltip=""
matTooltipPosition="right"
>
<mat-icon>chevron_right</mat-icon>
</button>
<button
*ngIf="sidenav.opened"
class="drawer-button-open"
mat-icon-button
(click)="sidenav.toggle()"
matTooltip="{{'Test-Steuerung verbergen' | customtext:'gm_hide_controls_tooltip' | async}}"
matTooltipPosition="above"
>
<mat-icon>chevron_left</mat-icon>
</button>
</div>
<div id="shield" *ngIf="isClosing"></div>
.page-body {
overflow-x: hidden;
}
.test-session-table {
border-collapse: collapse;
display: table;
width: 100%;
}
.test-session-table thead tr td {
position: sticky;
top: 0;
z-index: 2;
background: rgba(255, 255, 255, 0.8);
}
.test-session-table td {
padding-top: 15px;
margin-right: 1em;
}
.test-session-table td[mat-sort-header="_checked"] {
padding-left: 5px;
}
.adminbackground {
box-shadow: 5px 10px 20px black;
background-color: white;
margin: 0 0 0 15px;
padding: 0 25px 25px 25px;
height: 100%;
overflow: auto;
scroll-behavior: smooth;
}
.page-header {
display: flex;
flex-wrap: wrap;
}
.page-header > p:last-child {
margin-right: 15px;
}
.page-header > div {
text-align: right;
vertical-align: middle;
}
.page-header .fill-remaining-space {
flex: 1 1 auto;
}
.connection-status {
text-transform: uppercase;
color: white;
font-size: 80%;
justify-content: space-between;
width: 120px
}
.connection-status mat-icon {
font-size: 100%;
padding: 0;
top: 2px;
}
.connection-status.error {
background: #821123
}
.connection-status.ws-offline {
background: orange;
}
.connection-status.ws-online {
animation-name: pulse;
animation-duration: 2s;
animation-iteration-count: infinite;
}
.connection-status.polling-fetch,
.connection-status.polling-sleep {
position: relative;
background-image: linear-gradient(90deg, rgba(0,146,0,1) 0%, rgba(0,199,0,1) 100%, rgba(0,146,0,1) 200%);
background-repeat: repeat-x;
background-position: 0;
animation-name: move-gradient;
animation-duration: 5s;
animation-iteration-count: infinite;
}
@keyframes move-gradient {
0% {
background-position: 0;
}
100% {
background-position: 120px;
}
}
@keyframes pulse {
0% {
background: rgba(0,199,0,1);
}
50% {
background: rgba(0,146,0,1);
}
100% {
background: rgba(0,199,0,1);
}
}
.toolbar {
padding: 15px;
}
.toolbar h2 {
margin-top: 0;
font-size: 1.5em;
}
.toolbar .mat-radio-button ~ .mat-radio-button {
margin-left: 16px;
}
.toolbar-section {
margin-bottom: 1em
}
.toolbar button {
text-transform: uppercase;
}
.toolbar .control ~ .control {
margin-left: 15px;
}
.min-height-section {
min-height: 3em;
}
.toolbar-section-bottom {
position: absolute;
bottom: 0;
}
mat-sidenav {
width: 350px;
}
.mat-drawer-content {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 25px;
overflow: initial;
height: initial;
}
.mat-drawer-container {
position: initial;
background: none;
}
.mat-drawer {
margin: 15px 0 0 0;
}
.corner-menu {
position: fixed;
right: 10px;
top: 72px;
z-index: 10000;
border-bottom-left-radius: 10px;
}
.scroll-hint {
position: fixed;
right: 15px;
bottom: 15px;
background: #b2ff59;
box-shadow: 0 3px 1px -2px rgba(0, 0, 0, 0.2), 0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 1px 5px 0 rgba(0, 0, 0, 0.12);
border-radius: 20Px;
height: 40px;
width: 40px;
line-height: 20px;
text-align: center;
z-index: 9000;
color: #003333;
}
.hide-scroll-hint .scroll-hint {
animation: fade-and-shrink 0.3s reverse forwards;
}
@keyframes fade-and-shrink {
0% {
opacity: 0;
transform: scale(0)
}
100% {
opacity: 1;
transform: scale(1)
}
}
.drawer-button-open {
top: 50%;
left: 320px;
position: fixed;
z-index: 10000;
}
.drawer-button-close {
top: 50%;
left: -12px;
color: white;
position: fixed;
z-index: 10000;
}
.emph {
color: #b2ff59;
font-style: italic;
text-transform: uppercase;
}
[disabled] .emph {
color: #821123
}
#message-panel alert:first-of-type {
background: #b2ff59;
animation: fade 7s reverse forwards;
}
@keyframes fade {
0% {
opacity: 0.1;
}
100% {
opacity: 1;
}
}
#shield {
position: fixed;
background: white;
top: 0;
left: 0;
bottom: 0;
right: 0;
animation: fade 6s forwards;
z-index: 100000;
}