Migrate checks-model to new pattern

- Rename service to model on appContext.
- Merge ...-model.ts and ...-service.ts.
- Rename ...Service to ...Model.
- Move all observables onto ...Model.
- Inject ...Model in the models and components that were directly
  accessing the observables.

Google-Bug-Id: b/206459178
Change-Id: I65d722a58f264444f0dd1b1a5380187b91c06528
diff --git a/polygerrit-ui/app/elements/change/gr-change-summary/gr-change-summary.ts b/polygerrit-ui/app/elements/change/gr-change-summary/gr-change-summary.ts
index f59fdec..e8c5916 100644
--- a/polygerrit-ui/app/elements/change/gr-change-summary/gr-change-summary.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-summary/gr-change-summary.ts
@@ -20,15 +20,9 @@
 import {sharedStyles} from '../../../styles/shared-styles';
 import {getAppContext} from '../../../services/app-context';
 import {
-  allRunsLatestPatchsetLatestAttempt$,
-  aPluginHasRegistered$,
   CheckResult,
   CheckRun,
   ErrorMessages,
-  errorMessagesLatest$,
-  loginCallbackLatest$,
-  someProvidersAreLoadingFirstTime$,
-  topLevelActionsLatest$,
 } from '../../../services/checks/checks-model';
 import {Action, Category, Link, RunStatus} from '../../../api/checks';
 import {fireShowPrimaryTab} from '../../../utils/event-util';
@@ -413,20 +407,40 @@
 
   private userModel = getAppContext().userModel;
 
-  private checksService = getAppContext().checksService;
+  private checksModel = getAppContext().checksModel;
 
   constructor() {
     super();
-    subscribe(this, allRunsLatestPatchsetLatestAttempt$, x => (this.runs = x));
-    subscribe(this, aPluginHasRegistered$, x => (this.showChecksSummary = x));
     subscribe(
       this,
-      someProvidersAreLoadingFirstTime$,
+      this.checksModel.allRunsLatestPatchsetLatestAttempt$,
+      x => (this.runs = x)
+    );
+    subscribe(
+      this,
+      this.checksModel.aPluginHasRegistered$,
+      x => (this.showChecksSummary = x)
+    );
+    subscribe(
+      this,
+      this.checksModel.someProvidersAreLoadingFirstTime$,
       x => (this.someProvidersAreLoading = x)
     );
-    subscribe(this, errorMessagesLatest$, x => (this.errorMessages = x));
-    subscribe(this, loginCallbackLatest$, x => (this.loginCallback = x));
-    subscribe(this, topLevelActionsLatest$, x => (this.actions = x));
+    subscribe(
+      this,
+      this.checksModel.errorMessagesLatest$,
+      x => (this.errorMessages = x)
+    );
+    subscribe(
+      this,
+      this.checksModel.loginCallbackLatest$,
+      x => (this.loginCallback = x)
+    );
+    subscribe(
+      this,
+      this.checksModel.topLevelActionsLatest$,
+      x => (this.actions = x)
+    );
     subscribe(this, changeComments$, x => (this.changeComments = x));
     subscribe(this, threads$, x => (this.commentThreads = x));
     subscribe(this, this.userModel.account$, x => (this.selfAccount = x));
@@ -560,7 +574,7 @@
   }
 
   private handleAction(e: CustomEvent<Action>) {
-    this.checksService.triggerAction(e.detail);
+    this.checksModel.triggerAction(e.detail);
   }
 
   private renderOverflow(items: DropdownLink[], disabledIds: string[] = []) {
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts
index 8b530f6..d8ce665 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts
@@ -181,7 +181,6 @@
   fireTitleChange,
 } from '../../../utils/event-util';
 import {GerritView, routerView$} from '../../../services/router/router-model';
-import {aPluginHasRegistered$} from '../../../services/checks/checks-model';
 import {
   debounce,
   DelayedTask,
@@ -286,6 +285,8 @@
 
   private readonly changeService = getAppContext().changeService;
 
+  private readonly checksModel = getAppContext().checksModel;
+
   /**
    * URL params passed from the router.
    */
@@ -633,7 +634,7 @@
   override ready() {
     super.ready();
     this.subscriptions.push(
-      aPluginHasRegistered$.subscribe(b => {
+      this.checksModel.aPluginHasRegistered$.subscribe(b => {
         this._showChecksTab = b;
       })
     );
diff --git a/polygerrit-ui/app/elements/change/gr-submit-requirements/gr-submit-requirements.ts b/polygerrit-ui/app/elements/change/gr-submit-requirements/gr-submit-requirements.ts
index 7ef4487..7a50bc1 100644
--- a/polygerrit-ui/app/elements/change/gr-submit-requirements/gr-submit-requirements.ts
+++ b/polygerrit-ui/app/elements/change/gr-submit-requirements/gr-submit-requirements.ts
@@ -44,10 +44,7 @@
 import {fontStyles} from '../../../styles/gr-font-styles';
 import {charsOnly} from '../../../utils/string-util';
 import {subscribe} from '../../lit/subscription-controller';
-import {
-  allRunsLatestPatchsetLatestAttempt$,
-  CheckRun,
-} from '../../../services/checks/checks-model';
+import {CheckRun} from '../../../services/checks/checks-model';
 import {
   firstPrimaryLink,
   getResultsOf,
@@ -57,6 +54,7 @@
 import '../../shared/gr-vote-chip/gr-vote-chip';
 import {fireShowPrimaryTab} from '../../../utils/event-util';
 import {PrimaryTab} from '../../../constants/constants';
+import {getAppContext} from '../../../services/app-context';
 
 /**
  * @attr {Boolean} suppress-title - hide titles, currently for hovercard view
@@ -149,9 +147,15 @@
     ];
   }
 
+  private readonly checksModel = getAppContext().checksModel;
+
   constructor() {
     super();
-    subscribe(this, allRunsLatestPatchsetLatestAttempt$, x => (this.runs = x));
+    subscribe(
+      this,
+      this.checksModel.allRunsLatestPatchsetLatestAttempt$,
+      x => (this.runs = x)
+    );
   }
 
   override render() {
diff --git a/polygerrit-ui/app/elements/checks/gr-checks-action.ts b/polygerrit-ui/app/elements/checks/gr-checks-action.ts
index b213fa6..74d0e30 100644
--- a/polygerrit-ui/app/elements/checks/gr-checks-action.ts
+++ b/polygerrit-ui/app/elements/checks/gr-checks-action.ts
@@ -28,7 +28,7 @@
   @property({type: Object})
   eventTarget: HTMLElement | null = null;
 
-  private checksService = getAppContext().checksService;
+  private checksModel = getAppContext().checksModel;
 
   override connectedCallback() {
     super.connectedCallback();
@@ -80,7 +80,7 @@
 
   handleClick(e: Event) {
     e.stopPropagation();
-    this.checksService.triggerAction(this.action);
+    this.checksModel.triggerAction(this.action);
   }
 }
 
diff --git a/polygerrit-ui/app/elements/checks/gr-checks-results.ts b/polygerrit-ui/app/elements/checks/gr-checks-results.ts
index ca53d28..24c06e0 100644
--- a/polygerrit-ui/app/elements/checks/gr-checks-results.ts
+++ b/polygerrit-ui/app/elements/checks/gr-checks-results.ts
@@ -32,14 +32,7 @@
   Tag,
 } from '../../api/checks';
 import {sharedStyles} from '../../styles/shared-styles';
-import {
-  CheckRun,
-  checksSelectedPatchsetNumber$,
-  RunResult,
-  someProvidersAreLoadingSelected$,
-  topLevelActionsSelected$,
-  topLevelLinksSelected$,
-} from '../../services/checks/checks-model';
+import {CheckRun, RunResult} from '../../services/checks/checks-model';
 import {
   allResults,
   firstPrimaryLink,
@@ -95,7 +88,7 @@
   @state()
   labels?: LabelNameToInfoMap;
 
-  private checksService = getAppContext().checksService;
+  private checksModel = getAppContext().checksModel;
 
   constructor() {
     super();
@@ -493,7 +486,7 @@
   }
 
   private handleAction(e: CustomEvent<Action>) {
-    this.checksService.triggerAction(e.detail);
+    this.checksModel.triggerAction(e.detail);
   }
 
   private renderAction(action?: Action) {
@@ -733,21 +726,29 @@
    */
   private isSectionExpandedByUser = new Map<Category, boolean>();
 
-  private readonly checksService = getAppContext().checksService;
+  private readonly checksModel = getAppContext().checksModel;
 
   constructor() {
     super();
-    subscribe(this, topLevelActionsSelected$, x => (this.actions = x));
-    subscribe(this, topLevelLinksSelected$, x => (this.links = x));
     subscribe(
       this,
-      checksSelectedPatchsetNumber$,
+      this.checksModel.topLevelActionsSelected$,
+      x => (this.actions = x)
+    );
+    subscribe(
+      this,
+      this.checksModel.topLevelLinksSelected$,
+      x => (this.links = x)
+    );
+    subscribe(
+      this,
+      this.checksModel.checksSelectedPatchsetNumber$,
       x => (this.checksPatchsetNumber = x)
     );
     subscribe(this, latestPatchNum$, x => (this.latestPatchsetNumber = x));
     subscribe(
       this,
-      someProvidersAreLoadingSelected$,
+      this.checksModel.someProvidersAreLoadingSelected$,
       x => (this.someProvidersAreLoading = x)
     );
   }
@@ -1101,7 +1102,7 @@
   }
 
   private handleAction(e: CustomEvent<Action>) {
-    this.checksService.triggerAction(e.detail);
+    this.checksModel.triggerAction(e.detail);
   }
 
   private renderAction(action?: Action) {
@@ -1112,11 +1113,11 @@
   private onPatchsetSelected(e: CustomEvent<{value: string}>) {
     const patchset = Number(e.detail.value);
     check(!isNaN(patchset), 'selected patchset must be a number');
-    this.checksService.setPatchset(patchset as PatchSetNumber);
+    this.checksModel.setPatchset(patchset as PatchSetNumber);
   }
 
   private goToLatestPatchset() {
-    this.checksService.setPatchset(undefined);
+    this.checksModel.setPatchset(undefined);
   }
 
   private createPatchsetDropdownItems() {
diff --git a/polygerrit-ui/app/elements/checks/gr-checks-runs.ts b/polygerrit-ui/app/elements/checks/gr-checks-runs.ts
index 474d2f2..20041de 100644
--- a/polygerrit-ui/app/elements/checks/gr-checks-runs.ts
+++ b/polygerrit-ui/app/elements/checks/gr-checks-runs.ts
@@ -33,11 +33,11 @@
   worstCategory,
 } from '../../services/checks/checks-util';
 import {
-  allRunsSelectedPatchset$,
   CheckRun,
   ChecksPatchset,
   ErrorMessages,
-  errorMessagesLatest$,
+} from '../../services/checks/checks-model';
+import {
   fakeActions,
   fakeLinks,
   fakeRun0,
@@ -45,9 +45,7 @@
   fakeRun2,
   fakeRun3,
   fakeRun4Att,
-  loginCallbackLatest$,
-  updateStateSetResults,
-} from '../../services/checks/checks-model';
+} from '../../services/checks/checks-fakes';
 import {assertIsDefined} from '../../utils/common-util';
 import {modifierPressed, whenVisible} from '../../utils/dom-util';
 import {
@@ -391,13 +389,25 @@
 
   private flagService = getAppContext().flagsService;
 
-  private checksService = getAppContext().checksService;
+  private checksModel = getAppContext().checksModel;
 
   constructor() {
     super();
-    subscribe(this, allRunsSelectedPatchset$, x => (this.runs = x));
-    subscribe(this, errorMessagesLatest$, x => (this.errorMessages = x));
-    subscribe(this, loginCallbackLatest$, x => (this.loginCallback = x));
+    subscribe(
+      this,
+      this.checksModel.allRunsSelectedPatchset$,
+      x => (this.runs = x)
+    );
+    subscribe(
+      this,
+      this.checksModel.errorMessagesLatest$,
+      x => (this.errorMessages = x)
+    );
+    subscribe(
+      this,
+      this.checksModel.loginCallbackLatest$,
+      x => (this.loginCallback = x)
+    );
   }
 
   static override get styles() {
@@ -619,7 +629,7 @@
           link
           ?disabled=${runButtonDisabled}
           @click="${() => {
-            actions.forEach(action => this.checksService.triggerAction(action));
+            actions.forEach(action => this.checksModel.triggerAction(action));
           }}"
           >Run Selected</gr-button
         >
@@ -659,25 +669,79 @@
   }
 
   none() {
-    updateStateSetResults('f0', [], [], [], ChecksPatchset.LATEST);
-    updateStateSetResults('f1', [], [], [], ChecksPatchset.LATEST);
-    updateStateSetResults('f2', [], [], [], ChecksPatchset.LATEST);
-    updateStateSetResults('f3', [], [], [], ChecksPatchset.LATEST);
-    updateStateSetResults('f4', [], [], [], ChecksPatchset.LATEST);
+    this.checksModel.updateStateSetResults(
+      'f0',
+      [],
+      [],
+      [],
+      ChecksPatchset.LATEST
+    );
+    this.checksModel.updateStateSetResults(
+      'f1',
+      [],
+      [],
+      [],
+      ChecksPatchset.LATEST
+    );
+    this.checksModel.updateStateSetResults(
+      'f2',
+      [],
+      [],
+      [],
+      ChecksPatchset.LATEST
+    );
+    this.checksModel.updateStateSetResults(
+      'f3',
+      [],
+      [],
+      [],
+      ChecksPatchset.LATEST
+    );
+    this.checksModel.updateStateSetResults(
+      'f4',
+      [],
+      [],
+      [],
+      ChecksPatchset.LATEST
+    );
   }
 
   all() {
-    updateStateSetResults(
+    this.checksModel.updateStateSetResults(
       'f0',
       [fakeRun0],
       fakeActions,
       fakeLinks,
       ChecksPatchset.LATEST
     );
-    updateStateSetResults('f1', [fakeRun1], [], [], ChecksPatchset.LATEST);
-    updateStateSetResults('f2', [fakeRun2], [], [], ChecksPatchset.LATEST);
-    updateStateSetResults('f3', [fakeRun3], [], [], ChecksPatchset.LATEST);
-    updateStateSetResults('f4', fakeRun4Att, [], [], ChecksPatchset.LATEST);
+    this.checksModel.updateStateSetResults(
+      'f1',
+      [fakeRun1],
+      [],
+      [],
+      ChecksPatchset.LATEST
+    );
+    this.checksModel.updateStateSetResults(
+      'f2',
+      [fakeRun2],
+      [],
+      [],
+      ChecksPatchset.LATEST
+    );
+    this.checksModel.updateStateSetResults(
+      'f3',
+      [fakeRun3],
+      [],
+      [],
+      ChecksPatchset.LATEST
+    );
+    this.checksModel.updateStateSetResults(
+      'f4',
+      fakeRun4Att,
+      [],
+      [],
+      ChecksPatchset.LATEST
+    );
   }
 
   toggle(
@@ -687,7 +751,7 @@
     links: Link[] = []
   ) {
     const newRuns = this.runs.includes(runs[0]) ? [] : runs;
-    updateStateSetResults(
+    this.checksModel.updateStateSetResults(
       plugin,
       newRuns,
       actions,
diff --git a/polygerrit-ui/app/elements/checks/gr-checks-tab.ts b/polygerrit-ui/app/elements/checks/gr-checks-tab.ts
index d1ccd11..16c9bfe 100644
--- a/polygerrit-ui/app/elements/checks/gr-checks-tab.ts
+++ b/polygerrit-ui/app/elements/checks/gr-checks-tab.ts
@@ -17,13 +17,7 @@
 import {LitElement, css, html, PropertyValues} from 'lit';
 import {customElement, property, state} from 'lit/decorators';
 import {Action} from '../../api/checks';
-import {
-  CheckResult,
-  CheckRun,
-  allResultsSelected$,
-  checksSelectedPatchsetNumber$,
-  allRunsSelectedPatchset$,
-} from '../../services/checks/checks-model';
+import {CheckResult, CheckRun} from '../../services/checks/checks-model';
 import './gr-checks-runs';
 import './gr-checks-results';
 import {changeNum$, latestPatchNum$} from '../../services/change/change-model';
@@ -68,15 +62,23 @@
     number | undefined
   >();
 
-  private readonly checksService = getAppContext().checksService;
+  private readonly checksModel = getAppContext().checksModel;
 
   constructor() {
     super();
-    subscribe(this, allRunsSelectedPatchset$, x => (this.runs = x));
-    subscribe(this, allResultsSelected$, x => (this.results = x));
     subscribe(
       this,
-      checksSelectedPatchsetNumber$,
+      this.checksModel.allRunsSelectedPatchset$,
+      x => (this.runs = x)
+    );
+    subscribe(
+      this,
+      this.checksModel.allResultsSelected$,
+      x => (this.results = x)
+    );
+    subscribe(
+      this,
+      this.checksModel.checksSelectedPatchsetNumber$,
       x => (this.checksPatchsetNumber = x)
     );
     subscribe(this, latestPatchNum$, x => (this.latestPatchsetNumber = x));
@@ -140,7 +142,7 @@
   }
 
   handleActionTriggered(action: Action, run?: CheckRun) {
-    this.checksService.triggerAction(action, run);
+    this.checksModel.triggerAction(action, run);
   }
 
   handleRunSelected(e: RunSelectedEvent) {
diff --git a/polygerrit-ui/app/elements/plugins/gr-checks-api/gr-checks-api.ts b/polygerrit-ui/app/elements/plugins/gr-checks-api/gr-checks-api.ts
index 6484f92..e1f3d3c 100644
--- a/polygerrit-ui/app/elements/plugins/gr-checks-api/gr-checks-api.ts
+++ b/polygerrit-ui/app/elements/plugins/gr-checks-api/gr-checks-api.ts
@@ -43,7 +43,7 @@
 export class GrChecksApi implements ChecksPluginApi {
   private state = State.NOT_REGISTERED;
 
-  private readonly checksService = getAppContext().checksService;
+  private readonly checksModel = getAppContext().checksModel;
 
   private readonly reporting = getAppContext().reportingService;
 
@@ -53,14 +53,14 @@
 
   announceUpdate() {
     this.reporting.trackApi(this.plugin, 'checks', 'announceUpdate');
-    this.checksService.reload(this.plugin.getPluginName());
+    this.checksModel.reload(this.plugin.getPluginName());
   }
 
   updateResult(run: CheckRun, result: CheckResult) {
     if (result.externalId === undefined) {
       throw new Error('ChecksApi.updateResult() was called without externalId');
     }
-    this.checksService.updateResult(this.plugin.getPluginName(), run, result);
+    this.checksModel.updateResult(this.plugin.getPluginName(), run, result);
   }
 
   register(provider: ChecksProvider, config?: ChecksApiConfig): void {
@@ -68,7 +68,7 @@
     if (this.state === State.REGISTERED)
       throw new Error('Only one provider can be registered per plugin.');
     this.state = State.REGISTERED;
-    this.checksService.register(
+    this.checksModel.register(
       this.plugin.getPluginName(),
       provider,
       config ?? DEFAULT_CONFIG
diff --git a/polygerrit-ui/app/embed/gr-diff-app-context-init.ts b/polygerrit-ui/app/embed/gr-diff-app-context-init.ts
index 17e0994..16308c0 100644
--- a/polygerrit-ui/app/embed/gr-diff-app-context-init.ts
+++ b/polygerrit-ui/app/embed/gr-diff-app-context-init.ts
@@ -83,8 +83,8 @@
     commentsService: (_ctx: Partial<AppContext>) => {
       throw new Error('commentsService is not implemented');
     },
-    checksService: (_ctx: Partial<AppContext>) => {
-      throw new Error('checksService is not implemented');
+    checksModel: (_ctx: Partial<AppContext>) => {
+      throw new Error('checksModel is not implemented');
     },
     jsApiService: (_ctx: Partial<AppContext>) => {
       throw new Error('jsApiService is not implemented');
diff --git a/polygerrit-ui/app/services/app-context-init.ts b/polygerrit-ui/app/services/app-context-init.ts
index bfc56b4..6333934 100644
--- a/polygerrit-ui/app/services/app-context-init.ts
+++ b/polygerrit-ui/app/services/app-context-init.ts
@@ -22,7 +22,7 @@
 import {Auth} from './gr-auth/gr-auth_impl';
 import {GrRestApiInterface} from '../elements/shared/gr-rest-api-interface/gr-rest-api-interface';
 import {ChangeService} from './change/change-service';
-import {ChecksService} from './checks/checks-service';
+import {ChecksModel} from './checks/checks-model';
 import {GrJsApiInterface} from '../elements/shared/gr-js-api-interface/gr-js-api-interface-element';
 import {GrStorageService} from './storage/gr-storage_impl';
 import {UserModel} from './user/user-model';
@@ -61,9 +61,9 @@
       assertIsDefined(ctx.restApiService, 'restApiService');
       return new CommentsService(ctx.restApiService!);
     },
-    checksService: (ctx: Partial<AppContext>) => {
+    checksModel: (ctx: Partial<AppContext>) => {
       assertIsDefined(ctx.reportingService, 'reportingService');
-      return new ChecksService(ctx.reportingService!);
+      return new ChecksModel(ctx.reportingService!);
     },
     jsApiService: (ctx: Partial<AppContext>) => {
       assertIsDefined(ctx.reportingService, 'reportingService');
diff --git a/polygerrit-ui/app/services/app-context.ts b/polygerrit-ui/app/services/app-context.ts
index 53064fa..4488b12 100644
--- a/polygerrit-ui/app/services/app-context.ts
+++ b/polygerrit-ui/app/services/app-context.ts
@@ -21,7 +21,7 @@
 import {AuthService} from './gr-auth/gr-auth';
 import {RestApiService} from './gr-rest-api/gr-rest-api';
 import {ChangeService} from './change/change-service';
-import {ChecksService} from './checks/checks-service';
+import {ChecksModel} from './checks/checks-model';
 import {JsApiService} from '../elements/shared/gr-js-api-interface/gr-js-api-types';
 import {StorageService} from './storage/gr-storage';
 import {UserModel} from './user/user-model';
@@ -38,7 +38,7 @@
   restApiService: RestApiService;
   changeService: ChangeService;
   commentsService: CommentsService;
-  checksService: ChecksService;
+  checksModel: ChecksModel;
   jsApiService: JsApiService;
   storageService: StorageService;
   configModel: ConfigModel;
diff --git a/polygerrit-ui/app/services/checks/checks-fakes.ts b/polygerrit-ui/app/services/checks/checks-fakes.ts
new file mode 100644
index 0000000..09cd2e7
--- /dev/null
+++ b/polygerrit-ui/app/services/checks/checks-fakes.ts
@@ -0,0 +1,418 @@
+/**
+ * @license
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import {
+  Action,
+  Category,
+  Link,
+  LinkIcon,
+  RunStatus,
+  TagColor,
+} from '../../api/checks';
+import {CheckRun} from './checks-model';
+
+// TODO(brohlfs): Eventually these fakes should be removed. But they have proven
+// to be super convenient for testing, debugging and demoing, so I would like to
+// keep them around for a few quarters. Maybe remove by EOY 2022?
+
+export const fakeRun0: CheckRun = {
+  pluginName: 'f0',
+  internalRunId: 'f0',
+  checkName: 'FAKE Error Finder Finder Finder Finder Finder Finder Finder',
+  labelName: 'Presubmit',
+  isSingleAttempt: true,
+  isLatestAttempt: true,
+  attemptDetails: [],
+  results: [
+    {
+      internalResultId: 'f0r0',
+      category: Category.ERROR,
+      summary: 'I would like to point out this error: 1 is not equal to 2!',
+      links: [
+        {primary: true, url: 'https://www.google.com', icon: LinkIcon.EXTERNAL},
+      ],
+      tags: [{name: 'OBSOLETE'}, {name: 'E2E'}],
+    },
+    {
+      internalResultId: 'f0r1',
+      category: Category.ERROR,
+      summary: 'Running the mighty test has failed by crashing.',
+      message: 'Btw, 1 is also not equal to 3. Did you know?',
+      actions: [
+        {
+          name: 'Ignore',
+          tooltip: 'Ignore this result',
+          primary: true,
+          callback: () => Promise.resolve({message: 'fake "ignore" triggered'}),
+        },
+        {
+          name: 'Flag',
+          tooltip: 'Flag this result as totally absolutely really not useful',
+          primary: true,
+          disabled: true,
+          callback: () => Promise.resolve({message: 'flag "flag" triggered'}),
+        },
+        {
+          name: 'Upload',
+          tooltip: 'Upload the result to the super cloud.',
+          primary: false,
+          callback: () => Promise.resolve({message: 'fake "upload" triggered'}),
+        },
+      ],
+      tags: [{name: 'INTERRUPTED', color: TagColor.BROWN}, {name: 'WINDOWS'}],
+      links: [
+        {primary: false, url: 'https://google.com', icon: LinkIcon.EXTERNAL},
+        {primary: true, url: 'https://google.com', icon: LinkIcon.DOWNLOAD},
+        {
+          primary: true,
+          url: 'https://google.com',
+          icon: LinkIcon.DOWNLOAD_MOBILE,
+        },
+        {primary: true, url: 'https://google.com', icon: LinkIcon.IMAGE},
+        {primary: true, url: 'https://google.com', icon: LinkIcon.IMAGE},
+        {primary: false, url: 'https://google.com', icon: LinkIcon.IMAGE},
+        {primary: true, url: 'https://google.com', icon: LinkIcon.REPORT_BUG},
+        {primary: true, url: 'https://google.com', icon: LinkIcon.HELP_PAGE},
+        {primary: true, url: 'https://google.com', icon: LinkIcon.HISTORY},
+      ],
+    },
+  ],
+  status: RunStatus.COMPLETED,
+};
+
+export const fakeRun1: CheckRun = {
+  pluginName: 'f1',
+  internalRunId: 'f1',
+  checkName: 'FAKE Super Check',
+  statusLink: 'https://www.google.com/',
+  patchset: 1,
+  labelName: 'Verified',
+  isSingleAttempt: true,
+  isLatestAttempt: true,
+  attemptDetails: [],
+  results: [
+    {
+      internalResultId: 'f1r0',
+      category: Category.WARNING,
+      summary: 'We think that you could improve this.',
+      message: `There is a lot to be said. A lot. I say, a lot.\n
+                So please keep reading.`,
+      tags: [{name: 'INTERRUPTED', color: TagColor.PURPLE}, {name: 'WINDOWS'}],
+      codePointers: [
+        {
+          path: '/COMMIT_MSG',
+          range: {
+            start_line: 10,
+            start_character: 0,
+            end_line: 10,
+            end_character: 0,
+          },
+        },
+        {
+          path: 'polygerrit-ui/app/api/checks.ts',
+          range: {
+            start_line: 5,
+            start_character: 0,
+            end_line: 7,
+            end_character: 0,
+          },
+        },
+      ],
+      links: [
+        {primary: true, url: 'https://google.com', icon: LinkIcon.EXTERNAL},
+        {primary: true, url: 'https://google.com', icon: LinkIcon.DOWNLOAD},
+        {
+          primary: true,
+          url: 'https://google.com',
+          icon: LinkIcon.DOWNLOAD_MOBILE,
+        },
+        {primary: true, url: 'https://google.com', icon: LinkIcon.IMAGE},
+        {
+          primary: false,
+          url: 'https://google.com',
+          tooltip: 'look at this',
+          icon: LinkIcon.IMAGE,
+        },
+        {
+          primary: false,
+          url: 'https://google.com',
+          tooltip: 'not at this',
+          icon: LinkIcon.IMAGE,
+        },
+      ],
+    },
+  ],
+  status: RunStatus.RUNNING,
+};
+
+export const fakeRun2: CheckRun = {
+  pluginName: 'f2',
+  internalRunId: 'f2',
+  checkName: 'FAKE Mega Analysis',
+  statusDescription: 'This run is nearly completed, but not quite.',
+  statusLink: 'https://www.google.com/',
+  checkDescription:
+    'From what the title says you can tell that this check analyses.',
+  checkLink: 'https://www.google.com/',
+  scheduledTimestamp: new Date('2021-04-01T03:14:15'),
+  startedTimestamp: new Date('2021-04-01T04:24:25'),
+  finishedTimestamp: new Date('2021-04-01T04:44:44'),
+  isSingleAttempt: true,
+  isLatestAttempt: true,
+  attemptDetails: [],
+  actions: [
+    {
+      name: 'Re-Run',
+      tooltip: 'More powerful run than before',
+      primary: true,
+      callback: () => Promise.resolve({message: 'fake "re-run" triggered'}),
+    },
+    {
+      name: 'Monetize',
+      primary: true,
+      disabled: true,
+      callback: () => Promise.resolve({message: 'fake "monetize" triggered'}),
+    },
+    {
+      name: 'Delete',
+      primary: true,
+      callback: () => Promise.resolve({message: 'fake "delete" triggered'}),
+    },
+  ],
+  results: [
+    {
+      internalResultId: 'f2r0',
+      category: Category.INFO,
+      summary: 'This is looking a bit too large.',
+      message: `We are still looking into how large exactly. Stay tuned.
+And have a look at https://www.google.com!
+
+Or have a look at change 30000.
+Example code:
+  const constable = '';
+  var variable = '';`,
+      tags: [{name: 'FLAKY'}, {name: 'MAC-OS'}],
+    },
+  ],
+  status: RunStatus.COMPLETED,
+};
+
+export const fakeRun3: CheckRun = {
+  pluginName: 'f3',
+  internalRunId: 'f3',
+  checkName: 'FAKE Critical Observations',
+  status: RunStatus.RUNNABLE,
+  isSingleAttempt: true,
+  isLatestAttempt: true,
+  attemptDetails: [],
+};
+
+export const fakeRun4_1: CheckRun = {
+  pluginName: 'f4',
+  internalRunId: 'f4',
+  checkName: 'FAKE Elimination Long Long Long Long Long',
+  status: RunStatus.RUNNABLE,
+  attempt: 1,
+  isSingleAttempt: false,
+  isLatestAttempt: false,
+  attemptDetails: [],
+};
+
+export const fakeRun4_2: CheckRun = {
+  pluginName: 'f4',
+  internalRunId: 'f4',
+  checkName: 'FAKE Elimination Long Long Long Long Long',
+  status: RunStatus.COMPLETED,
+  attempt: 2,
+  isSingleAttempt: false,
+  isLatestAttempt: false,
+  attemptDetails: [],
+  results: [
+    {
+      internalResultId: 'f42r0',
+      category: Category.INFO,
+      summary: 'Please eliminate all the TODOs!',
+    },
+  ],
+};
+
+export const fakeRun4_3: CheckRun = {
+  pluginName: 'f4',
+  internalRunId: 'f4',
+  checkName: 'FAKE Elimination Long Long Long Long Long',
+  status: RunStatus.COMPLETED,
+  attempt: 3,
+  isSingleAttempt: false,
+  isLatestAttempt: false,
+  attemptDetails: [],
+  results: [
+    {
+      internalResultId: 'f43r0',
+      category: Category.ERROR,
+      summary: 'Without eliminating all the TODOs your change will break!',
+    },
+  ],
+};
+
+export const fakeRun4_4: CheckRun = {
+  pluginName: 'f4',
+  internalRunId: 'f4',
+  checkName: 'FAKE Elimination Long Long Long Long Long',
+  checkDescription: 'Shows you the possible eliminations.',
+  checkLink: 'https://www.google.com',
+  status: RunStatus.COMPLETED,
+  statusDescription: 'Everything was eliminated already.',
+  statusLink: 'https://www.google.com',
+  attempt: 40,
+  scheduledTimestamp: new Date('2021-04-02T03:14:15'),
+  startedTimestamp: new Date('2021-04-02T04:24:25'),
+  finishedTimestamp: new Date('2021-04-02T04:25:44'),
+  isSingleAttempt: false,
+  isLatestAttempt: true,
+  attemptDetails: [],
+  results: [
+    {
+      internalResultId: 'f44r0',
+      category: Category.INFO,
+      summary: 'Dont be afraid. All TODOs will be eliminated.',
+      actions: [
+        {
+          name: 'Re-Run',
+          tooltip: 'More powerful run than before with a long tooltip, really.',
+          primary: true,
+          callback: () => Promise.resolve({message: 'fake "re-run" triggered'}),
+        },
+      ],
+    },
+  ],
+  actions: [
+    {
+      name: 'Re-Run',
+      tooltip: 'small',
+      primary: true,
+      callback: () => Promise.resolve({message: 'fake "re-run" triggered'}),
+    },
+  ],
+};
+
+export function fakeRun4CreateAttempts(from: number, to: number): CheckRun[] {
+  const runs: CheckRun[] = [];
+  for (let i = from; i < to; i++) {
+    runs.push(fakeRun4CreateAttempt(i));
+  }
+  return runs;
+}
+
+export function fakeRun4CreateAttempt(attempt: number): CheckRun {
+  return {
+    pluginName: 'f4',
+    internalRunId: 'f4',
+    checkName: 'FAKE Elimination Long Long Long Long Long',
+    status: RunStatus.COMPLETED,
+    attempt,
+    isSingleAttempt: false,
+    isLatestAttempt: false,
+    attemptDetails: [],
+    results:
+      attempt % 2 === 0
+        ? [
+            {
+              internalResultId: 'f43r0',
+              category: Category.ERROR,
+              summary:
+                'Without eliminating all the TODOs your change will break!',
+            },
+          ]
+        : [],
+  };
+}
+
+export const fakeRun4Att = [
+  fakeRun4_1,
+  fakeRun4_2,
+  fakeRun4_3,
+  ...fakeRun4CreateAttempts(5, 40),
+  fakeRun4_4,
+];
+
+export const fakeActions: Action[] = [
+  {
+    name: 'Fake Action 1',
+    primary: true,
+    disabled: true,
+    tooltip: 'Tooltip for Fake Action 1',
+    callback: () => Promise.resolve({message: 'fake action 1 triggered'}),
+  },
+  {
+    name: 'Fake Action 2',
+    primary: false,
+    disabled: true,
+    tooltip: 'Tooltip for Fake Action 2',
+    callback: () => Promise.resolve({message: 'fake action 2 triggered'}),
+  },
+  {
+    name: 'Fake Action 3',
+    summary: true,
+    primary: false,
+    tooltip: 'Tooltip for Fake Action 3',
+    callback: () => Promise.resolve({message: 'fake action 3 triggered'}),
+  },
+];
+
+export const fakeLinks: Link[] = [
+  {
+    url: 'https://www.google.com',
+    primary: true,
+    tooltip: 'Fake Bug Report 1',
+    icon: LinkIcon.REPORT_BUG,
+  },
+  {
+    url: 'https://www.google.com',
+    primary: true,
+    tooltip: 'Fake Bug Report 2',
+    icon: LinkIcon.REPORT_BUG,
+  },
+  {
+    url: 'https://www.google.com',
+    primary: true,
+    tooltip: 'Fake Link 1',
+    icon: LinkIcon.EXTERNAL,
+  },
+  {
+    url: 'https://www.google.com',
+    primary: false,
+    tooltip: 'Fake Link 2',
+    icon: LinkIcon.EXTERNAL,
+  },
+  {
+    url: 'https://www.google.com',
+    primary: true,
+    tooltip: 'Fake Code Link',
+    icon: LinkIcon.CODE,
+  },
+  {
+    url: 'https://www.google.com',
+    primary: true,
+    tooltip: 'Fake Image Link',
+    icon: LinkIcon.IMAGE,
+  },
+  {
+    url: 'https://www.google.com',
+    primary: true,
+    tooltip: 'Fake Help Link',
+    icon: LinkIcon.HELP_PAGE,
+  },
+];
diff --git a/polygerrit-ui/app/services/checks/checks-model.ts b/polygerrit-ui/app/services/checks/checks-model.ts
index bb6809f..6036d07 100644
--- a/polygerrit-ui/app/services/checks/checks-model.ts
+++ b/polygerrit-ui/app/services/checks/checks-model.ts
@@ -14,23 +14,48 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-
-import {BehaviorSubject, Observable} from 'rxjs';
+import {AttemptDetail, createAttemptMap} from './checks-util';
+import {assertIsDefined} from '../../utils/common-util';
+import {select} from '../../utils/observable-util';
+import {Finalizable} from '../registry';
+import {
+  BehaviorSubject,
+  combineLatest,
+  from,
+  Observable,
+  of,
+  Subject,
+  Subscription,
+  timer,
+} from 'rxjs';
+import {
+  catchError,
+  filter,
+  switchMap,
+  takeUntil,
+  takeWhile,
+  throttleTime,
+  withLatestFrom,
+} from 'rxjs/operators';
 import {
   Action,
-  Category,
   CheckResult as CheckResultApi,
   CheckRun as CheckRunApi,
   Link,
-  LinkIcon,
-  RunStatus,
-  TagColor,
+  ChangeData,
+  ChecksApiConfig,
+  ChecksProvider,
+  FetchResponse,
+  ResponseCode,
 } from '../../api/checks';
-import {distinctUntilChanged, map} from 'rxjs/operators';
-import {PatchSetNumber} from '../../types/common';
-import {AttemptDetail, createAttemptMap} from './checks-util';
-import {assertIsDefined} from '../../utils/common-util';
-import {deepEqual} from '../../utils/deep-util';
+import {change$, changeNum$, latestPatchNum$} from '../change/change-model';
+import {ChangeInfo, NumericChangeId, PatchSetNumber} from '../../types/common';
+import {getCurrentRevision} from '../../utils/change-util';
+import {getShaByPatchNum} from '../../utils/patch-set-util';
+import {ReportingService} from '../gr-reporting/gr-reporting';
+import {routerPatchNum$} from '../router/router-model';
+import {Execution} from '../../constants/reporting';
+import {fireAlert, fireEvent} from '../../utils/event-util';
 
 /**
  * The checks model maintains the state of checks for two patchsets: the latest
@@ -83,7 +108,7 @@
 // properties. So you can just combine them with {...run, ...result}.
 export type RunResult = CheckRun & CheckResult;
 
-interface ChecksProviderState {
+export interface ChecksProviderState {
   pluginName: string;
   loading: boolean;
   /**
@@ -121,117 +146,101 @@
   };
 }
 
-const initialState: ChecksState = {
-  pluginStateLatest: {},
-  pluginStateSelected: {},
-};
-
-const privateState$ = new BehaviorSubject(initialState);
-
-export function _testOnly_resetState() {
-  // We cannot assign a new subject to privateState$, because all the selectors
-  // have already subscribed to the original subject. So we have to emit the
-  // initial state on the existing subject.
-  privateState$.next({...initialState});
-}
-
-export function _testOnly_setState(state: ChecksState) {
-  privateState$.next(state);
-}
-
-export function _testOnly_getState() {
-  return privateState$.getValue();
-}
-
-// Re-exporting as Observable so that you can only subscribe, but not emit.
-export const checksState$: Observable<ChecksState> = privateState$;
-
-export const checksSelectedPatchsetNumber$ = checksState$.pipe(
-  map(state => state.patchsetNumberSelected),
-  distinctUntilChanged()
-);
-
-export const checksLatest$ = checksState$.pipe(
-  map(state => state.pluginStateLatest),
-  distinctUntilChanged()
-);
-
-export const checksSelected$ = checksState$.pipe(
-  map(state =>
-    state.patchsetNumberSelected
-      ? state.pluginStateSelected
-      : state.pluginStateLatest
-  ),
-  distinctUntilChanged()
-);
-
-export const aPluginHasRegistered$ = checksLatest$.pipe(
-  map(state => Object.keys(state).length > 0),
-  distinctUntilChanged()
-);
-
-export const someProvidersAreLoadingFirstTime$ = checksLatest$.pipe(
-  map(state =>
-    Object.values(state).some(
-      provider => provider.loading && provider.firstTimeLoad
-    )
-  ),
-  distinctUntilChanged()
-);
-
-export const someProvidersAreLoadingLatest$ = checksLatest$.pipe(
-  map(state =>
-    Object.values(state).some(providerState => providerState.loading)
-  ),
-  distinctUntilChanged()
-);
-
-export const someProvidersAreLoadingSelected$ = checksSelected$.pipe(
-  map(state =>
-    Object.values(state).some(providerState => providerState.loading)
-  ),
-  distinctUntilChanged()
-);
-
-export const errorMessageLatest$ = checksLatest$.pipe(
-  map(
-    state =>
-      Object.values(state).find(
-        providerState => providerState.errorMessage !== undefined
-      )?.errorMessage
-  ),
-  distinctUntilChanged()
-);
-
 export interface ErrorMessages {
   /* Maps plugin name to error message. */
   [name: string]: string;
 }
 
-export const errorMessagesLatest$ = checksLatest$.pipe(
-  map(state => {
+export class ChecksModel implements Finalizable {
+  private readonly providers: {[name: string]: ChecksProvider} = {};
+
+  private readonly reloadSubjects: {[name: string]: Subject<void>} = {};
+
+  private checkToPluginMap = new Map<string, string>();
+
+  private changeNum?: NumericChangeId;
+
+  private latestPatchNum?: PatchSetNumber;
+
+  private readonly documentVisibilityChange$ = new BehaviorSubject(undefined);
+
+  private readonly reloadListener: () => void;
+
+  private readonly visibilityChangeListener: () => void;
+
+  private subscriptions: Subscription[] = [];
+
+  private readonly privateState$ = new BehaviorSubject<ChecksState>({
+    pluginStateLatest: {},
+    pluginStateSelected: {},
+  });
+
+  public checksState$: Observable<ChecksState> =
+    this.privateState$.asObservable();
+
+  public checksSelectedPatchsetNumber$ = select(
+    this.checksState$,
+    state => state.patchsetNumberSelected
+  );
+
+  public checksLatest$ = select(
+    this.checksState$,
+    state => state.pluginStateLatest
+  );
+
+  public checksSelected$ = select(this.checksState$, state =>
+    state.patchsetNumberSelected
+      ? state.pluginStateSelected
+      : state.pluginStateLatest
+  );
+
+  public aPluginHasRegistered$ = select(
+    this.checksLatest$,
+    state => Object.keys(state).length > 0
+  );
+
+  public someProvidersAreLoadingFirstTime$ = select(this.checksLatest$, state =>
+    Object.values(state).some(
+      provider => provider.loading && provider.firstTimeLoad
+    )
+  );
+
+  public someProvidersAreLoadingLatest$ = select(this.checksLatest$, state =>
+    Object.values(state).some(providerState => providerState.loading)
+  );
+
+  public someProvidersAreLoadingSelected$ = select(
+    this.checksSelected$,
+    state => Object.values(state).some(providerState => providerState.loading)
+  );
+
+  public errorMessageLatest$ = select(
+    this.checksLatest$,
+
+    state =>
+      Object.values(state).find(
+        providerState => providerState.errorMessage !== undefined
+      )?.errorMessage
+  );
+
+  public errorMessagesLatest$ = select(this.checksLatest$, state => {
     const errorMessages: ErrorMessages = {};
     for (const providerState of Object.values(state)) {
       if (providerState.errorMessage === undefined) continue;
       errorMessages[providerState.pluginName] = providerState.errorMessage;
     }
     return errorMessages;
-  }),
-  distinctUntilChanged(deepEqual)
-);
+  });
 
-export const loginCallbackLatest$ = checksLatest$.pipe(
-  map(
+  public loginCallbackLatest$ = select(
+    this.checksLatest$,
     state =>
       Object.values(state).find(
         providerState => providerState.loginCallback !== undefined
       )?.loginCallback
-  ),
-  distinctUntilChanged()
-);
+  );
 
-export const topLevelActionsLatest$ = checksLatest$.pipe(
-  map(state =>
+  public topLevelActionsLatest$ = select(this.checksLatest$, state =>
     Object.values(state).reduce(
       (allActions: Action[], providerState: ChecksProviderState) => [
         ...allActions,
@@ -239,12 +248,9 @@
       ],
       []
     )
-  ),
-  distinctUntilChanged<Action[]>(deepEqual)
-);
+  );
 
-export const topLevelActionsSelected$ = checksSelected$.pipe(
-  map(state =>
+  public topLevelActionsSelected$ = select(this.checksSelected$, state =>
     Object.values(state).reduce(
       (allActions: Action[], providerState: ChecksProviderState) => [
         ...allActions,
@@ -252,12 +258,9 @@
       ],
       []
     )
-  ),
-  distinctUntilChanged<Action[]>(deepEqual)
-);
+  );
 
-export const topLevelLinksSelected$ = checksSelected$.pipe(
-  map(state =>
+  public topLevelLinksSelected$ = select(this.checksSelected$, state =>
     Object.values(state).reduce(
       (allLinks: Link[], providerState: ChecksProviderState) => [
         ...allLinks,
@@ -265,12 +268,9 @@
       ],
       []
     )
-  ),
-  distinctUntilChanged<Link[]>(deepEqual)
-);
+  );
 
-export const allRunsLatestPatchset$ = checksLatest$.pipe(
-  map(state =>
+  public allRunsLatestPatchset$ = select(this.checksLatest$, state =>
     Object.values(state).reduce(
       (allRuns: CheckRun[], providerState: ChecksProviderState) => [
         ...allRuns,
@@ -278,12 +278,9 @@
       ],
       []
     )
-  ),
-  distinctUntilChanged<CheckRun[]>(deepEqual)
-);
+  );
 
-export const allRunsSelectedPatchset$ = checksSelected$.pipe(
-  map(state =>
+  public allRunsSelectedPatchset$ = select(this.checksSelected$, state =>
     Object.values(state).reduce(
       (allRuns: CheckRun[], providerState: ChecksProviderState) => [
         ...allRuns,
@@ -291,16 +288,14 @@
       ],
       []
     )
-  ),
-  distinctUntilChanged<CheckRun[]>(deepEqual)
-);
+  );
 
-export const allRunsLatestPatchsetLatestAttempt$ = allRunsLatestPatchset$.pipe(
-  map(runs => runs.filter(run => run.isLatestAttempt))
-);
+  public allRunsLatestPatchsetLatestAttempt$ = select(
+    this.allRunsLatestPatchset$,
+    runs => runs.filter(run => run.isLatestAttempt)
+  );
 
-export const checkToPluginMap$ = checksLatest$.pipe(
-  map(state => {
+  public checkToPluginMap$ = select(this.checksLatest$, state => {
     const map = new Map<string, string>();
     for (const [pluginName, providerState] of Object.entries(state)) {
       for (const run of providerState.runs) {
@@ -308,11 +303,9 @@
       }
     }
     return map;
-  })
-);
+  });
 
-export const allResultsSelected$ = checksSelected$.pipe(
-  map(state =>
+  public allResultsSelected$ = select(this.checksSelected$, state =>
     Object.values(state)
       .reduce(
         (allResults: CheckResult[], providerState: ChecksProviderState) => [
@@ -326,569 +319,436 @@
         []
       )
       .filter(r => r !== undefined)
-  )
-);
+  );
 
-// Must only be used by the checks service or whatever is in control of this
-// model.
-export function updateStateSetProvider(
-  pluginName: string,
-  patchset: ChecksPatchset
-) {
-  const nextState = {...privateState$.getValue()};
-  const pluginState = getPluginState(nextState, patchset);
-  pluginState[pluginName] = {
-    pluginName,
-    loading: false,
-    firstTimeLoad: true,
-    runs: [],
-    actions: [],
-    links: [],
-  };
-  privateState$.next(nextState);
-}
-
-// TODO(brohlfs): Remove all fake runs once the Checks UI is fully launched.
-//  They are just making it easier to develop the UI and always see all the
-//  different types/states of runs and results.
-
-export const fakeRun0: CheckRun = {
-  pluginName: 'f0',
-  internalRunId: 'f0',
-  checkName: 'FAKE Error Finder Finder Finder Finder Finder Finder Finder',
-  labelName: 'Presubmit',
-  isSingleAttempt: true,
-  isLatestAttempt: true,
-  attemptDetails: [],
-  results: [
-    {
-      internalResultId: 'f0r0',
-      category: Category.ERROR,
-      summary: 'I would like to point out this error: 1 is not equal to 2!',
-      links: [
-        {primary: true, url: 'https://www.google.com', icon: LinkIcon.EXTERNAL},
-      ],
-      tags: [{name: 'OBSOLETE'}, {name: 'E2E'}],
-    },
-    {
-      internalResultId: 'f0r1',
-      category: Category.ERROR,
-      summary: 'Running the mighty test has failed by crashing.',
-      message: 'Btw, 1 is also not equal to 3. Did you know?',
-      actions: [
-        {
-          name: 'Ignore',
-          tooltip: 'Ignore this result',
-          primary: true,
-          callback: () => Promise.resolve({message: 'fake "ignore" triggered'}),
-        },
-        {
-          name: 'Flag',
-          tooltip: 'Flag this result as totally absolutely really not useful',
-          primary: true,
-          disabled: true,
-          callback: () => Promise.resolve({message: 'flag "flag" triggered'}),
-        },
-        {
-          name: 'Upload',
-          tooltip: 'Upload the result to the super cloud.',
-          primary: false,
-          callback: () => Promise.resolve({message: 'fake "upload" triggered'}),
-        },
-      ],
-      tags: [{name: 'INTERRUPTED', color: TagColor.BROWN}, {name: 'WINDOWS'}],
-      links: [
-        {primary: false, url: 'https://google.com', icon: LinkIcon.EXTERNAL},
-        {primary: true, url: 'https://google.com', icon: LinkIcon.DOWNLOAD},
-        {
-          primary: true,
-          url: 'https://google.com',
-          icon: LinkIcon.DOWNLOAD_MOBILE,
-        },
-        {primary: true, url: 'https://google.com', icon: LinkIcon.IMAGE},
-        {primary: true, url: 'https://google.com', icon: LinkIcon.IMAGE},
-        {primary: false, url: 'https://google.com', icon: LinkIcon.IMAGE},
-        {primary: true, url: 'https://google.com', icon: LinkIcon.REPORT_BUG},
-        {primary: true, url: 'https://google.com', icon: LinkIcon.HELP_PAGE},
-        {primary: true, url: 'https://google.com', icon: LinkIcon.HISTORY},
-      ],
-    },
-  ],
-  status: RunStatus.COMPLETED,
-};
-
-export const fakeRun1: CheckRun = {
-  pluginName: 'f1',
-  internalRunId: 'f1',
-  checkName: 'FAKE Super Check',
-  statusLink: 'https://www.google.com/',
-  patchset: 1,
-  labelName: 'Verified',
-  isSingleAttempt: true,
-  isLatestAttempt: true,
-  attemptDetails: [],
-  results: [
-    {
-      internalResultId: 'f1r0',
-      category: Category.WARNING,
-      summary: 'We think that you could improve this.',
-      message: `There is a lot to be said. A lot. I say, a lot.\n
-                So please keep reading.`,
-      tags: [{name: 'INTERRUPTED', color: TagColor.PURPLE}, {name: 'WINDOWS'}],
-      codePointers: [
-        {
-          path: '/COMMIT_MSG',
-          range: {
-            start_line: 10,
-            start_character: 0,
-            end_line: 10,
-            end_character: 0,
-          },
-        },
-        {
-          path: 'polygerrit-ui/app/api/checks.ts',
-          range: {
-            start_line: 5,
-            start_character: 0,
-            end_line: 7,
-            end_character: 0,
-          },
-        },
-      ],
-      links: [
-        {primary: true, url: 'https://google.com', icon: LinkIcon.EXTERNAL},
-        {primary: true, url: 'https://google.com', icon: LinkIcon.DOWNLOAD},
-        {
-          primary: true,
-          url: 'https://google.com',
-          icon: LinkIcon.DOWNLOAD_MOBILE,
-        },
-        {primary: true, url: 'https://google.com', icon: LinkIcon.IMAGE},
-        {
-          primary: false,
-          url: 'https://google.com',
-          tooltip: 'look at this',
-          icon: LinkIcon.IMAGE,
-        },
-        {
-          primary: false,
-          url: 'https://google.com',
-          tooltip: 'not at this',
-          icon: LinkIcon.IMAGE,
-        },
-      ],
-    },
-  ],
-  status: RunStatus.RUNNING,
-};
-
-export const fakeRun2: CheckRun = {
-  pluginName: 'f2',
-  internalRunId: 'f2',
-  checkName: 'FAKE Mega Analysis',
-  statusDescription: 'This run is nearly completed, but not quite.',
-  statusLink: 'https://www.google.com/',
-  checkDescription:
-    'From what the title says you can tell that this check analyses.',
-  checkLink: 'https://www.google.com/',
-  scheduledTimestamp: new Date('2021-04-01T03:14:15'),
-  startedTimestamp: new Date('2021-04-01T04:24:25'),
-  finishedTimestamp: new Date('2021-04-01T04:44:44'),
-  isSingleAttempt: true,
-  isLatestAttempt: true,
-  attemptDetails: [],
-  actions: [
-    {
-      name: 'Re-Run',
-      tooltip: 'More powerful run than before',
-      primary: true,
-      callback: () => Promise.resolve({message: 'fake "re-run" triggered'}),
-    },
-    {
-      name: 'Monetize',
-      primary: true,
-      disabled: true,
-      callback: () => Promise.resolve({message: 'fake "monetize" triggered'}),
-    },
-    {
-      name: 'Delete',
-      primary: true,
-      callback: () => Promise.resolve({message: 'fake "delete" triggered'}),
-    },
-  ],
-  results: [
-    {
-      internalResultId: 'f2r0',
-      category: Category.INFO,
-      summary: 'This is looking a bit too large.',
-      message: `We are still looking into how large exactly. Stay tuned.
-And have a look at https://www.google.com!
-
-Or have a look at change 30000.
-Example code:
-  const constable = '';
-  var variable = '';`,
-      tags: [{name: 'FLAKY'}, {name: 'MAC-OS'}],
-    },
-  ],
-  status: RunStatus.COMPLETED,
-};
-
-export const fakeRun3: CheckRun = {
-  pluginName: 'f3',
-  internalRunId: 'f3',
-  checkName: 'FAKE Critical Observations',
-  status: RunStatus.RUNNABLE,
-  isSingleAttempt: true,
-  isLatestAttempt: true,
-  attemptDetails: [],
-};
-
-export const fakeRun4_1: CheckRun = {
-  pluginName: 'f4',
-  internalRunId: 'f4',
-  checkName: 'FAKE Elimination Long Long Long Long Long',
-  status: RunStatus.RUNNABLE,
-  attempt: 1,
-  isSingleAttempt: false,
-  isLatestAttempt: false,
-  attemptDetails: [],
-};
-
-export const fakeRun4_2: CheckRun = {
-  pluginName: 'f4',
-  internalRunId: 'f4',
-  checkName: 'FAKE Elimination Long Long Long Long Long',
-  status: RunStatus.COMPLETED,
-  attempt: 2,
-  isSingleAttempt: false,
-  isLatestAttempt: false,
-  attemptDetails: [],
-  results: [
-    {
-      internalResultId: 'f42r0',
-      category: Category.INFO,
-      summary: 'Please eliminate all the TODOs!',
-    },
-  ],
-};
-
-export const fakeRun4_3: CheckRun = {
-  pluginName: 'f4',
-  internalRunId: 'f4',
-  checkName: 'FAKE Elimination Long Long Long Long Long',
-  status: RunStatus.COMPLETED,
-  attempt: 3,
-  isSingleAttempt: false,
-  isLatestAttempt: false,
-  attemptDetails: [],
-  results: [
-    {
-      internalResultId: 'f43r0',
-      category: Category.ERROR,
-      summary: 'Without eliminating all the TODOs your change will break!',
-    },
-  ],
-};
-
-export const fakeRun4_4: CheckRun = {
-  pluginName: 'f4',
-  internalRunId: 'f4',
-  checkName: 'FAKE Elimination Long Long Long Long Long',
-  checkDescription: 'Shows you the possible eliminations.',
-  checkLink: 'https://www.google.com',
-  status: RunStatus.COMPLETED,
-  statusDescription: 'Everything was eliminated already.',
-  statusLink: 'https://www.google.com',
-  attempt: 40,
-  scheduledTimestamp: new Date('2021-04-02T03:14:15'),
-  startedTimestamp: new Date('2021-04-02T04:24:25'),
-  finishedTimestamp: new Date('2021-04-02T04:25:44'),
-  isSingleAttempt: false,
-  isLatestAttempt: true,
-  attemptDetails: [],
-  results: [
-    {
-      internalResultId: 'f44r0',
-      category: Category.INFO,
-      summary: 'Dont be afraid. All TODOs will be eliminated.',
-      actions: [
-        {
-          name: 'Re-Run',
-          tooltip: 'More powerful run than before with a long tooltip, really.',
-          primary: true,
-          callback: () => Promise.resolve({message: 'fake "re-run" triggered'}),
-        },
-      ],
-    },
-  ],
-  actions: [
-    {
-      name: 'Re-Run',
-      tooltip: 'small',
-      primary: true,
-      callback: () => Promise.resolve({message: 'fake "re-run" triggered'}),
-    },
-  ],
-};
-
-export function fakeRun4CreateAttempts(from: number, to: number): CheckRun[] {
-  const runs: CheckRun[] = [];
-  for (let i = from; i < to; i++) {
-    runs.push(fakeRun4CreateAttempt(i));
+  constructor(readonly reporting: ReportingService) {
+    this.subscriptions = [
+      changeNum$.subscribe(x => (this.changeNum = x)),
+      this.checkToPluginMap$.subscribe(map => {
+        this.checkToPluginMap = map;
+      }),
+      combineLatest([routerPatchNum$, latestPatchNum$]).subscribe(
+        ([routerPs, latestPs]) => {
+          this.latestPatchNum = latestPs;
+          if (latestPs === undefined) {
+            this.setPatchset(undefined);
+          } else if (typeof routerPs === 'number') {
+            this.setPatchset(routerPs);
+          } else {
+            this.setPatchset(latestPs);
+          }
+        }
+      ),
+    ];
+    this.visibilityChangeListener = () => {
+      this.documentVisibilityChange$.next(undefined);
+    };
+    document.addEventListener(
+      'visibilitychange',
+      this.visibilityChangeListener
+    );
+    this.reloadListener = () => this.reloadAll();
+    document.addEventListener('reload', this.reloadListener);
   }
-  return runs;
-}
 
-export function fakeRun4CreateAttempt(attempt: number): CheckRun {
-  return {
-    pluginName: 'f4',
-    internalRunId: 'f4',
-    checkName: 'FAKE Elimination Long Long Long Long Long',
-    status: RunStatus.COMPLETED,
-    attempt,
-    isSingleAttempt: false,
-    isLatestAttempt: false,
-    attemptDetails: [],
-    results:
-      attempt % 2 === 0
-        ? [
-            {
-              internalResultId: 'f43r0',
-              category: Category.ERROR,
-              summary:
-                'Without eliminating all the TODOs your change will break!',
-            },
-          ]
-        : [],
-  };
-}
-
-export const fakeRun4Att = [
-  fakeRun4_1,
-  fakeRun4_2,
-  fakeRun4_3,
-  ...fakeRun4CreateAttempts(5, 40),
-  fakeRun4_4,
-];
-
-export const fakeActions: Action[] = [
-  {
-    name: 'Fake Action 1',
-    primary: true,
-    disabled: true,
-    tooltip: 'Tooltip for Fake Action 1',
-    callback: () => Promise.resolve({message: 'fake action 1 triggered'}),
-  },
-  {
-    name: 'Fake Action 2',
-    primary: false,
-    disabled: true,
-    tooltip: 'Tooltip for Fake Action 2',
-    callback: () => Promise.resolve({message: 'fake action 2 triggered'}),
-  },
-  {
-    name: 'Fake Action 3',
-    summary: true,
-    primary: false,
-    tooltip: 'Tooltip for Fake Action 3',
-    callback: () => Promise.resolve({message: 'fake action 3 triggered'}),
-  },
-];
-
-export const fakeLinks: Link[] = [
-  {
-    url: 'https://www.google.com',
-    primary: true,
-    tooltip: 'Fake Bug Report 1',
-    icon: LinkIcon.REPORT_BUG,
-  },
-  {
-    url: 'https://www.google.com',
-    primary: true,
-    tooltip: 'Fake Bug Report 2',
-    icon: LinkIcon.REPORT_BUG,
-  },
-  {
-    url: 'https://www.google.com',
-    primary: true,
-    tooltip: 'Fake Link 1',
-    icon: LinkIcon.EXTERNAL,
-  },
-  {
-    url: 'https://www.google.com',
-    primary: false,
-    tooltip: 'Fake Link 2',
-    icon: LinkIcon.EXTERNAL,
-  },
-  {
-    url: 'https://www.google.com',
-    primary: true,
-    tooltip: 'Fake Code Link',
-    icon: LinkIcon.CODE,
-  },
-  {
-    url: 'https://www.google.com',
-    primary: true,
-    tooltip: 'Fake Image Link',
-    icon: LinkIcon.IMAGE,
-  },
-  {
-    url: 'https://www.google.com',
-    primary: true,
-    tooltip: 'Fake Help Link',
-    icon: LinkIcon.HELP_PAGE,
-  },
-];
-
-export function getPluginState(
-  state: ChecksState,
-  patchset: ChecksPatchset = ChecksPatchset.LATEST
-) {
-  if (patchset === ChecksPatchset.LATEST) {
-    state.pluginStateLatest = {...state.pluginStateLatest};
-    return state.pluginStateLatest;
-  } else {
-    state.pluginStateSelected = {...state.pluginStateSelected};
-    return state.pluginStateSelected;
+  finalize() {
+    document.removeEventListener('reload', this.reloadListener);
+    document.removeEventListener(
+      'visibilitychange',
+      this.visibilityChangeListener
+    );
+    for (const s of this.subscriptions) {
+      s.unsubscribe();
+    }
+    this.subscriptions = [];
+    this.privateState$.complete();
   }
-}
 
-export function updateStateSetLoading(
-  pluginName: string,
-  patchset: ChecksPatchset
-) {
-  const nextState = {...privateState$.getValue()};
-  const pluginState = getPluginState(nextState, patchset);
-  pluginState[pluginName] = {
-    ...pluginState[pluginName],
-    loading: true,
-  };
-  privateState$.next(nextState);
-}
-
-export function updateStateSetError(
-  pluginName: string,
-  errorMessage: string,
-  patchset: ChecksPatchset
-) {
-  const nextState = {...privateState$.getValue()};
-  const pluginState = getPluginState(nextState, patchset);
-  pluginState[pluginName] = {
-    ...pluginState[pluginName],
-    loading: false,
-    firstTimeLoad: false,
-    errorMessage,
-    loginCallback: undefined,
-    runs: [],
-    actions: [],
-  };
-  privateState$.next(nextState);
-}
-
-export function updateStateSetNotLoggedIn(
-  pluginName: string,
-  loginCallback: () => void,
-  patchset: ChecksPatchset
-) {
-  const nextState = {...privateState$.getValue()};
-  const pluginState = getPluginState(nextState, patchset);
-  pluginState[pluginName] = {
-    ...pluginState[pluginName],
-    loading: false,
-    firstTimeLoad: false,
-    errorMessage: undefined,
-    loginCallback,
-    runs: [],
-    actions: [],
-  };
-  privateState$.next(nextState);
-}
-
-export function updateStateSetResults(
-  pluginName: string,
-  runs: CheckRunApi[],
-  actions: Action[] = [],
-  links: Link[] = [],
-  patchset: ChecksPatchset
-) {
-  const attemptMap = createAttemptMap(runs);
-  for (const attemptInfo of attemptMap.values()) {
-    // Per run only one attempt can be undefined, so the '?? -1' is not really
-    // relevant for sorting.
-    attemptInfo.attempts.sort((a, b) => (a.attempt ?? -1) - (b.attempt ?? -1));
+  // Must only be used by the checks service or whatever is in control of this
+  // model.
+  updateStateSetProvider(pluginName: string, patchset: ChecksPatchset) {
+    const nextState = {...this.privateState$.getValue()};
+    const pluginState = this.getPluginState(nextState, patchset);
+    pluginState[pluginName] = {
+      pluginName,
+      loading: false,
+      firstTimeLoad: true,
+      runs: [],
+      actions: [],
+      links: [],
+    };
+    this.privateState$.next(nextState);
   }
-  const nextState = {...privateState$.getValue()};
-  const pluginState = getPluginState(nextState, patchset);
-  pluginState[pluginName] = {
-    ...pluginState[pluginName],
-    loading: false,
-    firstTimeLoad: false,
-    errorMessage: undefined,
-    loginCallback: undefined,
-    runs: runs.map(run => {
-      const runId = `${run.checkName}-${run.change}-${run.patchset}-${run.attempt}`;
-      const attemptInfo = attemptMap.get(run.checkName);
-      assertIsDefined(attemptInfo, 'attemptInfo');
-      return {
-        ...run,
-        pluginName,
-        internalRunId: runId,
-        isLatestAttempt: attemptInfo.latestAttempt === run.attempt,
-        isSingleAttempt: attemptInfo.isSingleAttempt,
-        attemptDetails: attemptInfo.attempts,
-        results: (run.results ?? []).map((result, i) => {
-          return {
-            ...result,
-            internalResultId: `${runId}-${i}`,
-          };
-        }),
-      };
-    }),
-    actions: [...actions],
-    links: [...links],
-  };
-  privateState$.next(nextState);
-}
 
-export function updateStateUpdateResult(
-  pluginName: string,
-  updatedRun: CheckRunApi,
-  updatedResult: CheckResultApi,
-  patchset: ChecksPatchset
-) {
-  const nextState = {...privateState$.getValue()};
-  const pluginState = getPluginState(nextState, patchset);
-  let runUpdated = false;
-  const runs: CheckRun[] = pluginState[pluginName].runs.map(run => {
-    if (run.change !== updatedRun.change) return run;
-    if (run.patchset !== updatedRun.patchset) return run;
-    if (run.attempt !== updatedRun.attempt) return run;
-    if (run.checkName !== updatedRun.checkName) return run;
-    let resultUpdated = false;
-    const results: CheckResult[] = (run.results ?? []).map(result => {
-      if (result.externalId && result.externalId === updatedResult.externalId) {
-        runUpdated = true;
-        resultUpdated = true;
+  getPluginState(
+    state: ChecksState,
+    patchset: ChecksPatchset = ChecksPatchset.LATEST
+  ) {
+    if (patchset === ChecksPatchset.LATEST) {
+      state.pluginStateLatest = {...state.pluginStateLatest};
+      return state.pluginStateLatest;
+    } else {
+      state.pluginStateSelected = {...state.pluginStateSelected};
+      return state.pluginStateSelected;
+    }
+  }
+
+  updateStateSetLoading(pluginName: string, patchset: ChecksPatchset) {
+    const nextState = {...this.privateState$.getValue()};
+    const pluginState = this.getPluginState(nextState, patchset);
+    pluginState[pluginName] = {
+      ...pluginState[pluginName],
+      loading: true,
+    };
+    this.privateState$.next(nextState);
+  }
+
+  updateStateSetError(
+    pluginName: string,
+    errorMessage: string,
+    patchset: ChecksPatchset
+  ) {
+    const nextState = {...this.privateState$.getValue()};
+    const pluginState = this.getPluginState(nextState, patchset);
+    pluginState[pluginName] = {
+      ...pluginState[pluginName],
+      loading: false,
+      firstTimeLoad: false,
+      errorMessage,
+      loginCallback: undefined,
+      runs: [],
+      actions: [],
+    };
+    this.privateState$.next(nextState);
+  }
+
+  updateStateSetNotLoggedIn(
+    pluginName: string,
+    loginCallback: () => void,
+    patchset: ChecksPatchset
+  ) {
+    const nextState = {...this.privateState$.getValue()};
+    const pluginState = this.getPluginState(nextState, patchset);
+    pluginState[pluginName] = {
+      ...pluginState[pluginName],
+      loading: false,
+      firstTimeLoad: false,
+      errorMessage: undefined,
+      loginCallback,
+      runs: [],
+      actions: [],
+    };
+    this.privateState$.next(nextState);
+  }
+
+  updateStateSetResults(
+    pluginName: string,
+    runs: CheckRunApi[],
+    actions: Action[] = [],
+    links: Link[] = [],
+    patchset: ChecksPatchset
+  ) {
+    const attemptMap = createAttemptMap(runs);
+    for (const attemptInfo of attemptMap.values()) {
+      // Per run only one attempt can be undefined, so the '?? -1' is not really
+      // relevant for sorting.
+      attemptInfo.attempts.sort(
+        (a, b) => (a.attempt ?? -1) - (b.attempt ?? -1)
+      );
+    }
+    const nextState = {...this.privateState$.getValue()};
+    const pluginState = this.getPluginState(nextState, patchset);
+    pluginState[pluginName] = {
+      ...pluginState[pluginName],
+      loading: false,
+      firstTimeLoad: false,
+      errorMessage: undefined,
+      loginCallback: undefined,
+      runs: runs.map(run => {
+        const runId = `${run.checkName}-${run.change}-${run.patchset}-${run.attempt}`;
+        const attemptInfo = attemptMap.get(run.checkName);
+        assertIsDefined(attemptInfo, 'attemptInfo');
         return {
-          ...updatedResult,
-          internalResultId: result.internalResultId,
+          ...run,
+          pluginName,
+          internalRunId: runId,
+          isLatestAttempt: attemptInfo.latestAttempt === run.attempt,
+          isSingleAttempt: attemptInfo.isSingleAttempt,
+          attemptDetails: attemptInfo.attempts,
+          results: (run.results ?? []).map((result, i) => {
+            return {
+              ...result,
+              internalResultId: `${runId}-${i}`,
+            };
+          }),
         };
-      }
-      return result;
-    });
-    return resultUpdated ? {...run, results} : run;
-  });
-  if (!runUpdated) return;
-  pluginState[pluginName] = {
-    ...pluginState[pluginName],
-    runs,
-  };
-  privateState$.next(nextState);
-}
+      }),
+      actions: [...actions],
+      links: [...links],
+    };
+    this.privateState$.next(nextState);
+  }
 
-export function updateStateSetPatchset(patchsetNumber?: PatchSetNumber) {
-  const nextState = {...privateState$.getValue()};
-  nextState.patchsetNumberSelected = patchsetNumber;
-  privateState$.next(nextState);
+  updateStateUpdateResult(
+    pluginName: string,
+    updatedRun: CheckRunApi,
+    updatedResult: CheckResultApi,
+    patchset: ChecksPatchset
+  ) {
+    const nextState = {...this.privateState$.getValue()};
+    const pluginState = this.getPluginState(nextState, patchset);
+    let runUpdated = false;
+    const runs: CheckRun[] = pluginState[pluginName].runs.map(run => {
+      if (run.change !== updatedRun.change) return run;
+      if (run.patchset !== updatedRun.patchset) return run;
+      if (run.attempt !== updatedRun.attempt) return run;
+      if (run.checkName !== updatedRun.checkName) return run;
+      let resultUpdated = false;
+      const results: CheckResult[] = (run.results ?? []).map(result => {
+        if (
+          result.externalId &&
+          result.externalId === updatedResult.externalId
+        ) {
+          runUpdated = true;
+          resultUpdated = true;
+          return {
+            ...updatedResult,
+            internalResultId: result.internalResultId,
+          };
+        }
+        return result;
+      });
+      return resultUpdated ? {...run, results} : run;
+    });
+    if (!runUpdated) return;
+    pluginState[pluginName] = {
+      ...pluginState[pluginName],
+      runs,
+    };
+    this.privateState$.next(nextState);
+  }
+
+  updateStateSetPatchset(patchsetNumber?: PatchSetNumber) {
+    const nextState = {...this.privateState$.getValue()};
+    nextState.patchsetNumberSelected = patchsetNumber;
+    this.privateState$.next(nextState);
+  }
+
+  setPatchset(num?: PatchSetNumber) {
+    this.updateStateSetPatchset(num === this.latestPatchNum ? undefined : num);
+  }
+
+  reload(pluginName: string) {
+    this.reloadSubjects[pluginName].next();
+  }
+
+  reloadAll() {
+    for (const key of Object.keys(this.providers)) {
+      this.reload(key);
+    }
+  }
+
+  reloadForCheck(checkName?: string) {
+    if (!checkName) return;
+    const plugin = this.checkToPluginMap.get(checkName);
+    if (plugin) this.reload(plugin);
+  }
+
+  updateResult(pluginName: string, run: CheckRunApi, result: CheckResultApi) {
+    this.updateStateUpdateResult(
+      pluginName,
+      run,
+      result,
+      ChecksPatchset.LATEST
+    );
+    this.updateStateUpdateResult(
+      pluginName,
+      run,
+      result,
+      ChecksPatchset.SELECTED
+    );
+  }
+
+  triggerAction(action?: Action, run?: CheckRun) {
+    if (!action?.callback) return;
+    if (!this.changeNum) return;
+    const patchSet = run?.patchset ?? this.latestPatchNum;
+    if (!patchSet) return;
+    const promise = action.callback(
+      this.changeNum,
+      patchSet,
+      run?.attempt,
+      run?.externalId,
+      run?.checkName,
+      action.name
+    );
+    // If plugins return undefined or not a promise, then show no toast.
+    if (!promise?.then) return;
+
+    fireAlert(document, `Triggering action '${action.name}' ...`);
+    from(promise)
+      // If the action takes longer than 5 seconds, then most likely the
+      // user is either not interested or the result not relevant anymore.
+      .pipe(takeUntil(timer(5000)))
+      .subscribe(result => {
+        if (result.errorMessage || result.message) {
+          fireAlert(document, `${result.message ?? result.errorMessage}`);
+        } else {
+          fireEvent(document, 'hide-alert');
+        }
+        if (result.shouldReload) {
+          this.reloadForCheck(run?.checkName);
+        }
+      });
+  }
+
+  register(
+    pluginName: string,
+    provider: ChecksProvider,
+    config: ChecksApiConfig
+  ) {
+    if (this.providers[pluginName]) {
+      console.warn(
+        `Plugin '${pluginName}' was trying to register twice as a Checks UI provider. Ignored.`
+      );
+      return;
+    }
+    this.providers[pluginName] = provider;
+    this.reloadSubjects[pluginName] = new BehaviorSubject<void>(undefined);
+    this.updateStateSetProvider(pluginName, ChecksPatchset.LATEST);
+    this.updateStateSetProvider(pluginName, ChecksPatchset.SELECTED);
+    this.initFetchingOfData(pluginName, config, ChecksPatchset.LATEST);
+    this.initFetchingOfData(pluginName, config, ChecksPatchset.SELECTED);
+  }
+
+  initFetchingOfData(
+    pluginName: string,
+    config: ChecksApiConfig,
+    patchset: ChecksPatchset
+  ) {
+    const pollIntervalMs = (config?.fetchPollingIntervalSeconds ?? 60) * 1000;
+    // Various events should trigger fetching checks from the provider:
+    // 1. Change number and patchset number changes.
+    // 2. Specific reload requests.
+    // 3. Regular polling starting with an initial fetch right now.
+    // 4. A hidden Gerrit tab becoming visible.
+    this.subscriptions.push(
+      combineLatest([
+        changeNum$,
+        patchset === ChecksPatchset.LATEST
+          ? latestPatchNum$
+          : this.checksSelectedPatchsetNumber$,
+        this.reloadSubjects[pluginName].pipe(throttleTime(1000)),
+        timer(0, pollIntervalMs),
+        this.documentVisibilityChange$,
+      ])
+        .pipe(
+          takeWhile(_ => !!this.providers[pluginName]),
+          filter(_ => document.visibilityState !== 'hidden'),
+          withLatestFrom(change$),
+          switchMap(
+            ([[changeNum, patchNum], change]): Observable<FetchResponse> => {
+              if (!change || !changeNum || !patchNum) return of(this.empty());
+              if (typeof patchNum !== 'number') return of(this.empty());
+              assertIsDefined(change.revisions, 'change.revisions');
+              const patchsetSha = getShaByPatchNum(change.revisions, patchNum);
+              // Sometimes patchNum is updated earlier than change, so change
+              // revisions don't have patchNum yet
+              if (!patchsetSha) return of(this.empty());
+              const data: ChangeData = {
+                changeNumber: changeNum,
+                patchsetNumber: patchNum,
+                patchsetSha,
+                repo: change.project,
+                commitMessage: getCurrentRevision(change)?.commit?.message,
+                changeInfo: change as ChangeInfo,
+              };
+              return this.fetchResults(pluginName, data, patchset);
+            }
+          ),
+          catchError(e => {
+            // This should not happen and is really severe, because it means that
+            // the Observable has terminated and we won't recover from that. No
+            // further attempts to fetch results for this plugin will be made.
+            this.reporting.error(e, `checks-model crash for ${pluginName}`);
+            return of(this.createErrorResponse(pluginName, e));
+          })
+        )
+        .subscribe(response => {
+          switch (response.responseCode) {
+            case ResponseCode.ERROR: {
+              const message = response.errorMessage ?? '-';
+              this.reporting.reportExecution(Execution.CHECKS_API_ERROR, {
+                plugin: pluginName,
+                message,
+              });
+              this.updateStateSetError(pluginName, message, patchset);
+              break;
+            }
+            case ResponseCode.NOT_LOGGED_IN: {
+              assertIsDefined(response.loginCallback, 'loginCallback');
+              this.reporting.reportExecution(
+                Execution.CHECKS_API_NOT_LOGGED_IN,
+                {
+                  plugin: pluginName,
+                }
+              );
+              this.updateStateSetNotLoggedIn(
+                pluginName,
+                response.loginCallback,
+                patchset
+              );
+              break;
+            }
+            case ResponseCode.OK: {
+              this.updateStateSetResults(
+                pluginName,
+                response.runs ?? [],
+                response.actions ?? [],
+                response.links ?? [],
+                patchset
+              );
+              break;
+            }
+          }
+        })
+    );
+  }
+
+  private empty(): FetchResponse {
+    return {
+      responseCode: ResponseCode.OK,
+      runs: [],
+    };
+  }
+
+  private createErrorResponse(
+    pluginName: string,
+    message: object
+  ): FetchResponse {
+    return {
+      responseCode: ResponseCode.ERROR,
+      errorMessage:
+        `Error message from plugin '${pluginName}':` +
+        ` ${JSON.stringify(message)}`,
+    };
+  }
+
+  private fetchResults(
+    pluginName: string,
+    data: ChangeData,
+    patchset: ChecksPatchset
+  ): Observable<FetchResponse> {
+    this.updateStateSetLoading(pluginName, patchset);
+    const timer = this.reporting.getTimer('ChecksPluginFetch');
+    const fetchPromise = this.providers[pluginName]
+      .fetch(data)
+      .then(response => {
+        timer.end({pluginName});
+        return response;
+      });
+    return from(fetchPromise).pipe(
+      catchError(e => of(this.createErrorResponse(pluginName, e)))
+    );
+  }
 }
diff --git a/polygerrit-ui/app/services/checks/checks-model_test.ts b/polygerrit-ui/app/services/checks/checks-model_test.ts
index 0be0451..390f6ad 100644
--- a/polygerrit-ui/app/services/checks/checks-model_test.ts
+++ b/polygerrit-ui/app/services/checks/checks-model_test.ts
@@ -16,15 +16,9 @@
  */
 import '../../test/common-test-setup-karma';
 import './checks-model';
-import {
-  _testOnly_getState,
-  ChecksPatchset,
-  updateStateSetLoading,
-  updateStateSetProvider,
-  updateStateSetResults,
-  updateStateUpdateResult,
-} from './checks-model';
+import {ChecksModel, ChecksPatchset, ChecksProviderState} from './checks-model';
 import {Category, CheckRun, RunStatus} from '../../api/checks';
+import {grReportingMock} from '../gr-reporting/gr-reporting_mock';
 
 const PLUGIN_NAME = 'test-plugin';
 
@@ -45,14 +39,23 @@
   },
 ];
 
-function current() {
-  return _testOnly_getState().pluginStateLatest[PLUGIN_NAME];
-}
-
 suite('checks-model tests', () => {
-  test('updateStateSetProvider', () => {
-    updateStateSetProvider(PLUGIN_NAME, ChecksPatchset.LATEST);
-    assert.deepEqual(current(), {
+  let model: ChecksModel;
+
+  let current: ChecksProviderState;
+
+  setup(() => {
+    model = new ChecksModel(grReportingMock);
+    model.checksLatest$.subscribe(c => (current = c[PLUGIN_NAME]));
+  });
+
+  teardown(() => {
+    model.finalize();
+  });
+
+  test('model.updateStateSetProvider', () => {
+    model.updateStateSetProvider(PLUGIN_NAME, ChecksPatchset.LATEST);
+    assert.deepEqual(current, {
       pluginName: PLUGIN_NAME,
       loading: false,
       firstTimeLoad: true,
@@ -63,45 +66,69 @@
   });
 
   test('loading and first time load', () => {
-    updateStateSetProvider(PLUGIN_NAME, ChecksPatchset.LATEST);
-    assert.isFalse(current().loading);
-    assert.isTrue(current().firstTimeLoad);
-    updateStateSetLoading(PLUGIN_NAME, ChecksPatchset.LATEST);
-    assert.isTrue(current().loading);
-    assert.isTrue(current().firstTimeLoad);
-    updateStateSetResults(PLUGIN_NAME, RUNS, [], [], ChecksPatchset.LATEST);
-    assert.isFalse(current().loading);
-    assert.isFalse(current().firstTimeLoad);
-    updateStateSetLoading(PLUGIN_NAME, ChecksPatchset.LATEST);
-    assert.isTrue(current().loading);
-    assert.isFalse(current().firstTimeLoad);
-    updateStateSetResults(PLUGIN_NAME, RUNS, [], [], ChecksPatchset.LATEST);
-    assert.isFalse(current().loading);
-    assert.isFalse(current().firstTimeLoad);
+    model.updateStateSetProvider(PLUGIN_NAME, ChecksPatchset.LATEST);
+    assert.isFalse(current.loading);
+    assert.isTrue(current.firstTimeLoad);
+    model.updateStateSetLoading(PLUGIN_NAME, ChecksPatchset.LATEST);
+    assert.isTrue(current.loading);
+    assert.isTrue(current.firstTimeLoad);
+    model.updateStateSetResults(
+      PLUGIN_NAME,
+      RUNS,
+      [],
+      [],
+      ChecksPatchset.LATEST
+    );
+    assert.isFalse(current.loading);
+    assert.isFalse(current.firstTimeLoad);
+    model.updateStateSetLoading(PLUGIN_NAME, ChecksPatchset.LATEST);
+    assert.isTrue(current.loading);
+    assert.isFalse(current.firstTimeLoad);
+    model.updateStateSetResults(
+      PLUGIN_NAME,
+      RUNS,
+      [],
+      [],
+      ChecksPatchset.LATEST
+    );
+    assert.isFalse(current.loading);
+    assert.isFalse(current.firstTimeLoad);
   });
 
-  test('updateStateSetResults', () => {
-    updateStateSetResults(PLUGIN_NAME, RUNS, [], [], ChecksPatchset.LATEST);
-    assert.lengthOf(current().runs, 1);
-    assert.lengthOf(current().runs[0].results!, 1);
+  test('model.updateStateSetResults', () => {
+    model.updateStateSetResults(
+      PLUGIN_NAME,
+      RUNS,
+      [],
+      [],
+      ChecksPatchset.LATEST
+    );
+    assert.lengthOf(current.runs, 1);
+    assert.lengthOf(current.runs[0].results!, 1);
   });
 
-  test('updateStateUpdateResult', () => {
-    updateStateSetResults(PLUGIN_NAME, RUNS, [], [], ChecksPatchset.LATEST);
+  test('model.updateStateUpdateResult', () => {
+    model.updateStateSetResults(
+      PLUGIN_NAME,
+      RUNS,
+      [],
+      [],
+      ChecksPatchset.LATEST
+    );
     assert.equal(
-      current().runs[0].results![0].summary,
+      current.runs[0].results![0].summary,
       RUNS[0]!.results![0].summary
     );
     const result = RUNS[0].results![0];
     const updatedResult = {...result, summary: 'new'};
-    updateStateUpdateResult(
+    model.updateStateUpdateResult(
       PLUGIN_NAME,
       RUNS[0],
       updatedResult,
       ChecksPatchset.LATEST
     );
-    assert.lengthOf(current().runs, 1);
-    assert.lengthOf(current().runs[0].results!, 1);
-    assert.equal(current().runs[0].results![0].summary, 'new');
+    assert.lengthOf(current.runs, 1);
+    assert.lengthOf(current.runs[0].results!, 1);
+    assert.equal(current.runs[0].results![0].summary, 'new');
   });
 });
diff --git a/polygerrit-ui/app/services/checks/checks-service.ts b/polygerrit-ui/app/services/checks/checks-service.ts
deleted file mode 100644
index 111036c..0000000
--- a/polygerrit-ui/app/services/checks/checks-service.ts
+++ /dev/null
@@ -1,337 +0,0 @@
-/**
- * @license
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-import {
-  BehaviorSubject,
-  combineLatest,
-  from,
-  Observable,
-  of,
-  Subject,
-  Subscription,
-  timer,
-} from 'rxjs';
-import {
-  catchError,
-  filter,
-  switchMap,
-  takeUntil,
-  takeWhile,
-  throttleTime,
-  withLatestFrom,
-} from 'rxjs/operators';
-import {
-  Action,
-  ChangeData,
-  CheckResult,
-  CheckRun,
-  ChecksApiConfig,
-  ChecksProvider,
-  FetchResponse,
-  ResponseCode,
-} from '../../api/checks';
-import {change$, changeNum$, latestPatchNum$} from '../change/change-model';
-import {
-  ChecksPatchset,
-  checksSelectedPatchsetNumber$,
-  checkToPluginMap$,
-  updateStateSetError,
-  updateStateSetLoading,
-  updateStateSetNotLoggedIn,
-  updateStateSetPatchset,
-  updateStateSetProvider,
-  updateStateSetResults,
-  updateStateUpdateResult,
-} from './checks-model';
-import {ChangeInfo, NumericChangeId, PatchSetNumber} from '../../types/common';
-import {Finalizable} from '../registry';
-import {getCurrentRevision} from '../../utils/change-util';
-import {getShaByPatchNum} from '../../utils/patch-set-util';
-import {assertIsDefined} from '../../utils/common-util';
-import {ReportingService} from '../gr-reporting/gr-reporting';
-import {routerPatchNum$} from '../router/router-model';
-import {Execution} from '../../constants/reporting';
-import {fireAlert, fireEvent} from '../../utils/event-util';
-
-export class ChecksService implements Finalizable {
-  private readonly providers: {[name: string]: ChecksProvider} = {};
-
-  private readonly reloadSubjects: {[name: string]: Subject<void>} = {};
-
-  private checkToPluginMap = new Map<string, string>();
-
-  private changeNum?: NumericChangeId;
-
-  private latestPatchNum?: PatchSetNumber;
-
-  private readonly documentVisibilityChange$ = new BehaviorSubject(undefined);
-
-  private readonly reloadListener: () => void;
-
-  private readonly subscriptions: Subscription[] = [];
-
-  private readonly visibilityChangeListener: () => void;
-
-  constructor(readonly reporting: ReportingService) {
-    this.subscriptions.push(changeNum$.subscribe(x => (this.changeNum = x)));
-    this.subscriptions.push(
-      checkToPluginMap$.subscribe(map => {
-        this.checkToPluginMap = map;
-      })
-    );
-    this.subscriptions.push(
-      combineLatest([routerPatchNum$, latestPatchNum$]).subscribe(
-        ([routerPs, latestPs]) => {
-          this.latestPatchNum = latestPs;
-          if (latestPs === undefined) {
-            this.setPatchset(undefined);
-          } else if (typeof routerPs === 'number') {
-            this.setPatchset(routerPs);
-          } else {
-            this.setPatchset(latestPs);
-          }
-        }
-      )
-    );
-    this.visibilityChangeListener = () => {
-      this.documentVisibilityChange$.next(undefined);
-    };
-    document.addEventListener(
-      'visibilitychange',
-      this.visibilityChangeListener
-    );
-    this.reloadListener = () => this.reloadAll();
-    document.addEventListener('reload', this.reloadListener);
-  }
-
-  finalize() {
-    document.removeEventListener('reload', this.reloadListener);
-    document.removeEventListener(
-      'visibilitychange',
-      this.visibilityChangeListener
-    );
-    for (const s of this.subscriptions) {
-      s.unsubscribe();
-    }
-    this.subscriptions.splice(0, this.subscriptions.length);
-  }
-
-  setPatchset(num?: PatchSetNumber) {
-    updateStateSetPatchset(num === this.latestPatchNum ? undefined : num);
-  }
-
-  reload(pluginName: string) {
-    this.reloadSubjects[pluginName].next();
-  }
-
-  reloadAll() {
-    Object.keys(this.providers).forEach(key => this.reload(key));
-  }
-
-  reloadForCheck(checkName?: string) {
-    if (!checkName) return;
-    const plugin = this.checkToPluginMap.get(checkName);
-    if (plugin) this.reload(plugin);
-  }
-
-  updateResult(pluginName: string, run: CheckRun, result: CheckResult) {
-    updateStateUpdateResult(pluginName, run, result, ChecksPatchset.LATEST);
-    updateStateUpdateResult(pluginName, run, result, ChecksPatchset.SELECTED);
-  }
-
-  triggerAction(action?: Action, run?: CheckRun) {
-    if (!action?.callback) return;
-    if (!this.changeNum) return;
-    const patchSet = run?.patchset ?? this.latestPatchNum;
-    if (!patchSet) return;
-    const promise = action.callback(
-      this.changeNum,
-      patchSet,
-      run?.attempt,
-      run?.externalId,
-      run?.checkName,
-      action.name
-    );
-    // If plugins return undefined or not a promise, then show no toast.
-    if (!promise?.then) return;
-
-    fireAlert(document, `Triggering action '${action.name}' ...`);
-    from(promise)
-      // If the action takes longer than 5 seconds, then most likely the
-      // user is either not interested or the result not relevant anymore.
-      .pipe(takeUntil(timer(5000)))
-      .subscribe(result => {
-        if (result.errorMessage || result.message) {
-          fireAlert(document, `${result.message ?? result.errorMessage}`);
-        } else {
-          fireEvent(document, 'hide-alert');
-        }
-        if (result.shouldReload) {
-          this.reloadForCheck(run?.checkName);
-        }
-      });
-  }
-
-  register(
-    pluginName: string,
-    provider: ChecksProvider,
-    config: ChecksApiConfig
-  ) {
-    if (this.providers[pluginName]) {
-      console.warn(
-        `Plugin '${pluginName}' was trying to register twice as a Checks UI provider. Ignored.`
-      );
-      return;
-    }
-    this.providers[pluginName] = provider;
-    this.reloadSubjects[pluginName] = new BehaviorSubject<void>(undefined);
-    updateStateSetProvider(pluginName, ChecksPatchset.LATEST);
-    updateStateSetProvider(pluginName, ChecksPatchset.SELECTED);
-    this.initFetchingOfData(pluginName, config, ChecksPatchset.LATEST);
-    this.initFetchingOfData(pluginName, config, ChecksPatchset.SELECTED);
-  }
-
-  initFetchingOfData(
-    pluginName: string,
-    config: ChecksApiConfig,
-    patchset: ChecksPatchset
-  ) {
-    const pollIntervalMs = (config?.fetchPollingIntervalSeconds ?? 60) * 1000;
-    // Various events should trigger fetching checks from the provider:
-    // 1. Change number and patchset number changes.
-    // 2. Specific reload requests.
-    // 3. Regular polling starting with an initial fetch right now.
-    // 4. A hidden Gerrit tab becoming visible.
-    this.subscriptions.push(
-      combineLatest([
-        changeNum$,
-        patchset === ChecksPatchset.LATEST
-          ? latestPatchNum$
-          : checksSelectedPatchsetNumber$,
-        this.reloadSubjects[pluginName].pipe(throttleTime(1000)),
-        timer(0, pollIntervalMs),
-        this.documentVisibilityChange$,
-      ])
-        .pipe(
-          takeWhile(_ => !!this.providers[pluginName]),
-          filter(_ => document.visibilityState !== 'hidden'),
-          withLatestFrom(change$),
-          switchMap(
-            ([[changeNum, patchNum], change]): Observable<FetchResponse> => {
-              if (!change || !changeNum || !patchNum) return of(this.empty());
-              if (typeof patchNum !== 'number') return of(this.empty());
-              assertIsDefined(change.revisions, 'change.revisions');
-              const patchsetSha = getShaByPatchNum(change.revisions, patchNum);
-              // Sometimes patchNum is updated earlier than change, so change
-              // revisions don't have patchNum yet
-              if (!patchsetSha) return of(this.empty());
-              const data: ChangeData = {
-                changeNumber: changeNum,
-                patchsetNumber: patchNum,
-                patchsetSha,
-                repo: change.project,
-                commitMessage: getCurrentRevision(change)?.commit?.message,
-                changeInfo: change as ChangeInfo,
-              };
-              return this.fetchResults(pluginName, data, patchset);
-            }
-          ),
-          catchError(e => {
-            // This should not happen and is really severe, because it means that
-            // the Observable has terminated and we won't recover from that. No
-            // further attempts to fetch results for this plugin will be made.
-            this.reporting.error(e, `checks-service crash for ${pluginName}`);
-            return of(this.createErrorResponse(pluginName, e));
-          })
-        )
-        .subscribe(response => {
-          switch (response.responseCode) {
-            case ResponseCode.ERROR: {
-              const message = response.errorMessage ?? '-';
-              this.reporting.reportExecution(Execution.CHECKS_API_ERROR, {
-                plugin: pluginName,
-                message,
-              });
-              updateStateSetError(pluginName, message, patchset);
-              break;
-            }
-            case ResponseCode.NOT_LOGGED_IN: {
-              assertIsDefined(response.loginCallback, 'loginCallback');
-              this.reporting.reportExecution(
-                Execution.CHECKS_API_NOT_LOGGED_IN,
-                {
-                  plugin: pluginName,
-                }
-              );
-              updateStateSetNotLoggedIn(
-                pluginName,
-                response.loginCallback,
-                patchset
-              );
-              break;
-            }
-            case ResponseCode.OK: {
-              updateStateSetResults(
-                pluginName,
-                response.runs ?? [],
-                response.actions ?? [],
-                response.links ?? [],
-                patchset
-              );
-              break;
-            }
-          }
-        })
-    );
-  }
-
-  private empty(): FetchResponse {
-    return {
-      responseCode: ResponseCode.OK,
-      runs: [],
-    };
-  }
-
-  private createErrorResponse(
-    pluginName: string,
-    message: object
-  ): FetchResponse {
-    return {
-      responseCode: ResponseCode.ERROR,
-      errorMessage:
-        `Error message from plugin '${pluginName}':` +
-        ` ${JSON.stringify(message)}`,
-    };
-  }
-
-  private fetchResults(
-    pluginName: string,
-    data: ChangeData,
-    patchset: ChecksPatchset
-  ): Observable<FetchResponse> {
-    updateStateSetLoading(pluginName, patchset);
-    const timer = this.reporting.getTimer('ChecksPluginFetch');
-    const fetchPromise = this.providers[pluginName]
-      .fetch(data)
-      .then(response => {
-        timer.end({pluginName});
-        return response;
-      });
-    return from(fetchPromise).pipe(
-      catchError(e => of(this.createErrorResponse(pluginName, e)))
-    );
-  }
-}
diff --git a/polygerrit-ui/app/test/common-test-setup.ts b/polygerrit-ui/app/test/common-test-setup.ts
index f041354..46067c1 100644
--- a/polygerrit-ui/app/test/common-test-setup.ts
+++ b/polygerrit-ui/app/test/common-test-setup.ts
@@ -49,7 +49,6 @@
 
 import {getAppContext} from '../services/app-context';
 import {_testOnly_resetState as resetChangeState} from '../services/change/change-model';
-import {_testOnly_resetState as resetChecksState} from '../services/checks/checks-model';
 import {_testOnly_resetState as resetCommentsState} from '../services/comments/comments-model';
 import {_testOnly_resetState as resetRouterState} from '../services/router/router-model';
 
@@ -117,7 +116,6 @@
   _testOnly_initGerritPluginApi();
 
   resetChangeState();
-  resetChecksState();
   resetCommentsState();
   resetRouterState();
 
diff --git a/polygerrit-ui/app/test/test-app-context-init.ts b/polygerrit-ui/app/test/test-app-context-init.ts
index 8bcd395b..160b543 100644
--- a/polygerrit-ui/app/test/test-app-context-init.ts
+++ b/polygerrit-ui/app/test/test-app-context-init.ts
@@ -26,7 +26,7 @@
 import {FlagsServiceImplementation} from '../services/flags/flags_impl';
 import {EventEmitter} from '../services/gr-event-interface/gr-event-interface_impl';
 import {ChangeService} from '../services/change/change-service';
-import {ChecksService} from '../services/checks/checks-service';
+import {ChecksModel} from '../services/checks/checks-model';
 import {GrJsApiInterface} from '../elements/shared/gr-js-api-interface/gr-js-api-interface-element';
 import {UserModel} from '../services/user/user-model';
 import {CommentsService} from '../services/comments/comments-service';
@@ -55,9 +55,9 @@
       assertIsDefined(ctx.restApiService, 'restApiService');
       return new CommentsService(ctx.restApiService!);
     },
-    checksService: (ctx: Partial<AppContext>) => {
+    checksModel: (ctx: Partial<AppContext>) => {
       assertIsDefined(ctx.reportingService, 'reportingService');
-      return new ChecksService(ctx.reportingService!);
+      return new ChecksModel(ctx.reportingService!);
     },
     jsApiService: (ctx: Partial<AppContext>) => {
       assertIsDefined(ctx.reportingService, 'reportingService');